现代dump技术及保护措施(上)
引自: 看雪论坛,忘了地址
现代dump技术及保护措施
本文目的
我们都喜欢使用免费的软件,这也就意味着需要有人对它们进行破解。而破解的时候就要对付各种各样的壳和protectors。壳的工作原理和脱壳的基本方法在《Packer终结篇》(NEOx, Volodya)一文中有不错的讲解。在此文中详细的讲解了PE文件格式以及protectors对它的利用方法。无疑,脱壳过程中一个重要的部分就是取得dump。关于dump的取得以及反dump的方法在那些文章中也有介绍,而所有讲到的这些PE Tools中的anti-dumping的方法都有绕过的办法。但是,遗憾的是,所有这些只能用于简单的保护,而更为复杂的protectors(eXtreme Protector, Armadillo)都使用了完全不同的反dump的方法,PE Tools已经是相形见绌了。在本文中,我想来研究一下现代的反dump方法以及绕过的办法。这对于那些想学习如何脱掉比ASPack更复杂的壳的人们来说无疑是有用的。
ring 0下的anti-dump
所有的进程 dumpers 都是由 OpenProcess/ReadProcessMemory/VirtualQueryEx 等函数来实现的。为了取得进程加载的模块列表通常要使用 ToolHelp API,ToolHelp API函数使用 ReadProcessMemory 来读取进程的内存。在 NativeAPI 级别上调用的是函数 ZwOpenProcess 和 ZwReadVirtualMemory。一个很显然的反 dump 的方法就是创建驱动程序,利用驱动在内核模式下拦截这些函数并禁止它们对所保护的进程进行访问。
最简单的办法就是只拦截 ZwOpenProcess 函数,因为要读取进程的内存首先得把进程打开。HOOK的handler一般是这样的:
NTSTATUS NewNtOpenProcess (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
if ((ULONG)ClientId > *MmUserProbeAddress) return STATUS_INVALID_PARAMETER;
__try
{
ProcessId = ClientId->UniqueProcess;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DPRINT("Exception");
return STATUS_INVALID_PARAMETER;
}
if (IsAdded(wLastItem, ProcessId))
{
DPRINT("Access Denied!");
return STATUS_ACCESS_DENIED;
} else
return TrueNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}
这段代码首先用保险的办法取出要打开的进程的 ProcessID,然后判断是该传递控制权还是返回STATUS_ACCESS_DENIED。因为系统中受保护的进程可能不止一个,所以必须建立进程的列表并向其中添加新的进程并删除已经搞定的进程。为了建立这个列表我们来讲一下用来描述受保护进程的结构体:
typedef struct _ProcessList
{
PVOID NextItem;
HANDLE Pid;
} TProcessList, *PProcessList;
下面是管理结构体链表的代码:
BOOLEAN IsAdded(PProcessList List, HANDLE Pid)
{
PProcessList Item = List;
while (Item)
{
if (Pid == Item->Pid) return TRUE;
Item = Item->NextItem;
}
return FALSE;
}
void DelItem(PProcessList *List, HANDLE Pid)
{
PProcessList Item = *List;
PProcessList Prev = NULL;
while (Item)
{
if (Pid == Item->Pid)
{
if (Prev) Prev->NextItem =
Item->NextItem; else *List = Item->NextItem;
ExFreePool(Item);
return;
}
Prev = Item;
Item = Item->NextItem;
}
return;
}
void FreePointers(PProcessList List)
{
PProcessList Item = List;
PVOID Mem;
while (Item)
{
Mem = Item;
Item = Item->NextItem;
ExFreePool(Mem);
}
return;
}
void AddItem(PProcessList *List, HANDLE Pid)
{
PProcessList wNewItem;
wNewItem = ExAllocatePool(NonPagedPool, sizeof(TProcessList));
wNewItem->NextItem = *List;
*List = wNewItem;
wNewItem->Pid = Pid;
return;
}
当发现新进程时,为了向链表中添加此进程我们将使用IOCTL向驱动程序发出请求,而完成后则用同样的办法将其删除。实现这种 anti-dump 的驱动程序的完整代码位于本文的附件程序中。
更为可靠的保护就是拦截 ZwReadVirtualMemory/ZwWriteVirtualMemory 和 ZwCreateThread ,但是这样就必须通过进程的句柄来取得它的 ProcessID。为此可以使用 ZwQueryInformationProcess 函数,但是对于那些有PROCESS_QUERY_INFORMATION 访问标志的句柄来说这招就不灵了,
所以最好是使用 ObReferenceObjectByHandle 来取得 EPROCESS 结构体指针,然后直接从 EPROCESS 结构体中取出ProcessID。代码如下:
ULONG GetPid(HANDLE PHanlde)
{
NTSTATUS st = 15;
PEPROCESS process = 0;
ULONG pId;
st = ObReferenceObjectByHandle(PHanlde, 0, NULL, UserMode, &process, NULL);
if (st == STATUS_SUCCESS)
{
pId = *(PULONG)(((ULONG)process) + pIdOffset);
ObDereferenceObject(process);
return pId;
}
return 0;
}
pIdOffset - ProcessId 在EPROCESS结构体中的偏移量,这个 constant 在不同版本系统的内核中是不一样的,所以在驱动启动的时候要检查内核的版本并相应的为变量赋值。在写handler时要考虑到,如果进程试图用ZwOpenProcess来打开自己时也应该能工作(比较PsGetCurrentProcessId),而在处理接收句柄的函数时要考虑到用于表示当前进程的伪句柄(-1)的存在性。
说起理论来都挺容易,但实际实现起来却是一路的磕磕绊绊。比如,在Windows XP中有style服务。为了实现它需要允许子系统服务进程(csrss.exe)访问受保护进程的内存。由此就产生了问题——如何确定这个进程的Id?这个问题初看上去好像简单,其实不然。用进程名来确定是不行的,因为系统中可能有不止一个的csrss.exe进程(例如,cracker可以给自己的dumper也取这个名字),所以还需要更为可靠的唯一确定进程的方法。为此我决定使用这样的办法,子系统sever拥有某些命名对象,通过这些命名对象可以确定这个进程。例如,我们取LPC端口/Windows/ApiPort,在所有版本的Windows NT上它都是由csrss创建的。为了确定它需要使用ZwQuerySystemInformation枚举所有已打开的句柄,将它们拷贝到内核句柄表中,调用ZwQueryObject并比较所取得的名称与所寻找的是否一致。若一致,则拥有此句柄的进程的Id就是csrss的。下面是实现代码:
PVOID GetInfoTable(ULONG ATableType)
{
ULONG mSize = 0x4000;
PVOID mPtr = NULL;
NTSTATUS St;
do
{
mPtr = ExAllocatePool(PagedPool, mSize);
if (mPtr != NULL)
{
St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);
} else return NULL;
if (St == STATUS_INFO_LENGTH_MISMATCH)
{
ExFreePool(mPtr);
mSize = mSize * 2;
}
} while (St == STATUS_INFO_LENGTH_MISMATCH);
if (St == STATUS_SUCCESS)
{
DPRINT("GetInfoTable Success!");
DPRINT("Info table in memory size - %d", mSize);
return mPtr;
} else ExFreePool(mPtr);
DPRINT("Error on GetInfoTable %X", St);
return NULL;
}
ULONG GetCsrPid()
{
int r;
HANDLE Process, hObject;
NTSTATUS St;
ULONG CsrId = 0;
OBJECT_ATTRIBUTES obj;
CLIENT_ID cid;
POBJECT_NAME_INFORMATION ObjName;
UNICODE_STRING ApiPortName;
PSYSTEM_HANDLE_INFORMATION_EX Handles;
RtlInitUnicodeString(&ApiPortName, L"//Windows//ApiPort");
DPRINT("Get handles info");
Handles = GetInfoTable(SystemHandleInformation);
if (Handles == NULL) return 0;
ObjName = ExAllocatePool(PagedPool, 0x2000);
DPRINT("Number of handles %d", Handles->NumberOfHandles);
for (r = 0; r != Handles->NumberOfHandles; r++)
{
if (Handles->Information[r].ObjectTypeNumber == 21) //Port object
{
InitializeObjectAttributes(&obj, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
cid.UniqueProcess = (HANDLE)Handles->Information[r].ProcessId;
cid.UniqueThread = 0;
if (ZwOpenProcess(&Process, PROCESS_DUP_HANDLE, &obj, &cid)
== STATUS_SUCCESS)
{
if (ZwDuplicateObject(Process,
(HANDLE)Handles->Information[r].Handle,
NtCurrentProcess(), &hObject,
0, 0, DUPLICATE_SAME_ACCESS) == STATUS_SUCCESS)
{
if (ZwQueryObject(hObject, ObjectNameInformation,
ObjName, 0x2000, NULL) == STATUS_SUCCESS)
{
if (ObjName->Name.Buffer != NULL)
if (wcsncmp(ApiPortName.Buffer, ObjName->Name.Buffer, 20) == 0)
{
DPRINT("Csrss %d", Handles->Information[r].ProcessId);
DPRINT("csr port - %ws", ObjName->Name.Buffer);
CsrId = Handles->Information[r].ProcessId;
ZwClose(Process);
ZwClose(hObject);
CsrId = Handles->Information[r].ProcessId;
ExFreePool(Handles);
ExFreePool(ObjName);
return CsrId;
}
} else DPRINT("Error in Query Object");
ZwClose(hObject);
} else DPRINT("Error on duplicating object");
ZwClose(Process);
} else DPRINT("Could not open process");
}
}
ExFreePool(Handles);
ExFreePool(ObjName);
return 0;
}
Themida (Extreme Protector) 使用的就是这种方法,在早期版本中为了处理好GUI的问题,使用的就是上面介绍的办法,但在后来的版本中却放弃了它(使得保护效果变差),新版的Themida并未禁止对受保护进程全部地址空间的访问,而只是禁止了对EXE文件所驻留的内存范围的访问,而所有加载到内存里的DLL未收到任何的保护。
在本文所附的程序中可以找到编写好的驱动程序,这个驱动程序可以保护进程不会被用这种方法dump出来。如果有人想把这个驱动添加到自己的protector里并开始大卖自己的Xprot的话,我会感到悲哀的——这种方法已经过时了,这里讲到它只是为了让文章完整。
当然,这种方法看上去还是相当好的,因为大多数cracker都不在内核模式下进行创作。摘除这种保护的办法之一就是找到原始处理程序的地址,patch掉SDT摘除HOOK。问题在于,这种方法并不通用,因为HOOK不只是能通过SDT建立,还可以通过拦截int 2Eh (win2000)或是修改sysenter (win XP)的处理程序,再有就是可以对原始处理程序的代码进行splicing。摘除protector的HOOK并不是最好的办法,因为protector可以检查HOOK是否还存在,而且还会采取措施来阻止HOOK被摘除(StarForce就是这样),所以我给出另一种方法——不在驱动程序中使用ZwReadVirtualMermory来进行dump。
在Windows NT内核中有两个未公开的函数KeAttachProcess/KeDetachProcess,驱动程序可以调用这两个函数来改变当前地址空间。函数原型如下:
extern
void KeAttachProcess(PEPROCESS Process);
extern
void KeDetachProcess(void);
通过这两个函数可以attach到受保护的进程并读取它的内存,但遗憾的是最新版的Themida对这俩个函数进行了patch,禁止用这种方法来dump自己的进程。但是它的保护并不全面,因为能attach到别的进程的函数还有KeStackAttachProcess/KeUnstackDetachProcess,而这两个函数却常被protectors遗忘(也容易产生问题,因为系统驱动程序会用到这两个函数),所以我们就用它们来读取内存。以下是函数原型和用到的结构体:
typedef struct _KAPC_STATE
{
LIST_ENTRY ApcListHead[2];
PVOID Process;
BOOLEAN KernelApcInProgress;
BOOLEAN KernelApcPending;
BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE;
extern
NTKERNELAPI void KeStackAttachProcess(PVOID Process, PKAPC_STATE ApcState);
extern
NTKERNELAPI void KeUnstackDetachProcess(PKAPC_STATE ApcState);
即使下一版的Themida实现了对这两个函数的拦截,attach到所需的进程也不难,手动修改cr3寄存器就行了。
为了读取进程的内存,我们需要使用IOCTL向我们的驱动程序传递进程的句柄、要读取的内存地址、当前进程缓冲区的地址以及要读取的字节数,在驱动获得dump进程的EPROCESS指针之后就会读取请求的数据。也就是说,实际上我们自行手工实现了ZwReadVirtualMemory。代码如下:
void CopyProcessMem(HANDLE hProcess, PVOID SrcAddr, PVOID DstAddr, ULONG *Size)
{
PEPROCESS process = NULL;
NTSTATUS st;
PUCHAR pMem = NULL;
ULONG Addr, Bytes;
PUCHAR cPtr, dPtr;
KAPC_STATE ApcState;
st = ObReferenceObjectByHandle(hProcess, 0, NULL, UserMode, &process, NULL);
if (NT_SUCCESS(st))
{
Bytes = *Size;
pMem = ExAllocatePool(NonPagedPool, Bytes);
dPtr = pMem;
cPtr = (PUCHAR)SrcAddr;
KeStackAttachProcess(process, &ApcState);
__try
{
while (Bytes)
{
*dPtr = *cPtr;
cPtr++;
dPtr++;
Bytes --;
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
KeUnstackDetachProcess(&ApcState);
Bytes = *Size - Bytes;
__try
{
memcpy(DstAddr, pMem, Bytes);
*Size = Bytes;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
ExFreePool(pMem);
ObDereferenceObject(process);
}
return;
}
对于驱动方式的dumping来说,要建立针对它的保护措施就要难多了,因为实际上来讲很难确定传递的信息究竟是来自于dumper还是来自于系统。但也不是不可能做到,您不要以为这招可以对付所有使用驱动dump的protectors(尽管目前还是有用的)。
遗憾的是,自行替换ZwWriteVirtualMemory在很多情况下是不够的,因为有些protectors还拦截了ZwOpenProcess。要绕过它,可以不去打开进程,而是通过在子系统的server进程里搜索来获得它的句柄(参见《使用Code Injection拦截Windows NT API函数》)。在我的advApiHook库里,对应这项功能的函数叫OpenProcessEx。有些保护程序会修改进程的security marker,以此来禁止打开内存读取。对这种保护,这个方法也可以对付。如果这种获取进程句柄的方法流行起来的话,保护程序的作者们就会最先在子系统server中把句柄关闭。要在驱动程序中获取进程句柄,只需使用PsLookupProcessByProcessId来取得指向其EPROCESS的指针,此后可以用ObOpenObjectByPointer把它加入到我们进程的句柄表中。实现代码如下:
HANDLE MyOpenProcess(HANDLE ProcessId)
{
PEPROCESS Process;
NTSTATUS St;
HANDLE hProcess = NULL;
PsLookupProcessByProcessId(ProcessId, &Process);
ObOpenObjectByPointer(Process, 0, NULL, 0, NULL, UserMode, &hProcess);
ObDereferenceObject(Process);
return hProcess;
}
保护程序的作者们当然可以拦截PsLookupProcessByProcessId,但这对于获取dump只能起到5分钟的拖延作用,因为句柄的获取还可以通过手工挖掘内核结构体来完成,或者干脆就不用句柄也不用API。
ring0下的保护手段就丰富多彩了(想象无限,创意无限)。例如,一个非常不错的anti-dump的方法就是破坏受保护进程的页表。为此需要干预调度器的工作并拦截某些SwapContext的未导出函数,这些函数都是在线程切换时要调用的。在拦截处理程序中,在切换入受保护进程时需要恢复页表,而在切换出时就把页表破坏掉。更为简单的是可以对抗dumper驱动。绕过这种保护也不困难,既可以在内核中进行(拦截中断处理程序),也可以在用户模式下进行(Code Injection)。我想,只要肯动脑,大家都能想出进行dump的自己原创的方法。