现代dump技术及保护措施[Ms-Rem](下)
Dynamic unpacking
另一种对付dump的常用方法就是的dynamic packing。其思想就是,protector并不将受保护的程序完全unpack,而只是unpack一部分。首先unpack第一页,当快要进行完时protector对异常进行拦截并unpack所请求的页,这时它就可以将上一页从内存中删除。这样受保护进程的image在内存里从来都不曾完整过,因此一般的dumper是没法进行dump的。这种办法在protector程序Armadillo中被广泛使用,被其称为CopyMem。为了拦截异常并对代码进行解码,程序建立了独立的进程,这个进程使用DebugAPI来调试受护进程。
对于摘除这种保护,大多数cracker采用的方法都不是最优的。他们的方法就是reverse并patch掉protector的代码,以此来迫使它解出完整的代码。这实在是太费事了,更让人难以接受的是,在每个新版本中,作者们都会修改这种保护机制的代码,这样旧的dump法就不再有效,从而就被人们毫不犹豫地扔到博物馆里。但幸运的是,我们还有更简单的办法——从内部来dump进程。为此需要将dump的代码注入到受保护进程的地址空间中并读取它的内存。在此过程中将会出现异常,处理异常的时候protector会向我们呈现全部解密后的代码。这种摘除CopyMem保护的方法已经是可以想出的最简单的办法了。CopyMem至今仍在被使用,原因就是这种方法还没有广泛流行。
获取kernel module的dump
在逆向ring 0下的protector的时候经常需要dump已加载到核心内存的PE文件。例如,如果protector对系统内核进行了patch,我们可以将内核的dump与磁盘文件做个比较,从而很容易地发现这个情况。抑或是dump出protector的驱动程序并研究它的解密方法。就是做到这些就已经不太容易了,因为所有现在的protectors早已经学会了在所有系统结构体中删除对自身驱动的引用,以此来实现驱动的隐藏,所以dump这种驱动的最现实的办法就是dump所有核心内存,之后在此dump中查找我们需要的东西。但是这个办法常常又不是很有效,因为保护程序的设计者们已经停止了对驱动的pack(比如StarForce 3),而是转向了更有前景的保护方法,比如多态代码以及虚拟机。
初看上去,dump内核模块只需要用copymem进行拷贝就行了,但实际上并没有这么简单,因为这里存在着一个陷阱。若要试图从头到尾读取模块内存的话,所能得到的就只是个蓝屏。若要尝试为所需内存段创建MDL并执行MmProbeAndLockPages,则会引发异常,而如果调用MmBuildMdlForNonPagedPool,则MDL会被建立,但要读取它会再一次引发蓝屏。与此相关的是,在native PE文件里,section可以有个“Discardable as needed”属性,这样的section在Driver Entry结束后会被立即删除。这样的系统可以节约NonPaged Pool,因为NonPaged Pool在系统里可是稀缺的。于是尝试访问这个地址就会引发异常,这个异常SEH不予处理,于是引起系统的崩溃。所以在读取内存之前我们需要用MmIsAddressValid来验证地址的有效性。以下是安全读取核心内存的代码:
NTSTATUS DumpKernelMemory(PVOID SrcAddr, PVOID DstAddr, ULONG Size)
{
PMDL pSrcMdl, pDstMdl;
PUCHAR pAddress, pDstDddress;
NTSTATUS st = STATUS_UNSUCCESSFUL;
ULONG r;
pSrcMdl = IoAllocateMdl(SrcAddr, Size, FALSE, FALSE, NULL);
if (pSrcMdl)
{
MmBuildMdlForNonPagedPool(pSrcMdl);
pAddress = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority);
if (pAddress)
{
pDstMdl = IoAllocateMdl(DstAddr, Size, FALSE, FALSE, NULL);
if (pDstMdl)
{
__try
{
MmProbeAndLockPages(pDstMdl, UserMode, IoWriteAccess);
pDstDddress = MmGetSystemAddressForMdlSafe(
pDstMdl, NormalPagePriority);
if (pDstDddress)
{
memset(pDstDddress, 0, Size);
for (r = 1; r < Size; r++)
{
if (MmIsAddressValid(pAddress)) *pDstDddress = *pAddress;
pAddress++;
pDstDddress++;
}
st = STATUS_SUCCESS;
}
MmUnlockPages(pDstMdl);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
IoFreeMdl(pDstMdl);
}
}
IoFreeMdl(pSrcMdl);
}
return st;
}
除内存读取之外,我们还需要获取模块的信息来知道读取的地址。获取此信息的方法将在后面介绍。
使dump过程更为便利
编写自己的dumper绝对是个费事的活儿,所以我决定使用PE Tools,因为它考虑了许多PE文件的细节并能方便快捷地进行dump。所以我写了一个叫eXtreme dumper(与Extreme Protector相对)的plugin,这个plugin可以实现驱动dump和使用DllInjection的dump。为此我在PE Tools中拦截了ZwOpenProcess和ZwReadVirtualMemory函数并用自己的函数做了替换,它们都是用上面介绍的方法写成的。作为例子,我这里给出ZwReadVirtualMemory处理程序的代码:
function NewZwReadVirtualMemory(ProcessHandle: dword; BaseAddress: pointer;
Buffer: pointer; BufferLength: dword;
ReturnLength: pdword): NTStatus; stdcall;
var
hPipe, Bytes, Len: dword;
Req: TXDumpRequest;
PipeName: array [0..128] of Char;
sPid: array [0..8] of Char;
ProcessId: dword;
Query: TDriverQuery;
begin
if DriverMethod then
begin
Result := dword(-1);
Len := BufferLength;
Query.QueryType := 2;
Query.Param1 := ProcessHandle;
Query.Param2 := dword(@Len);
Query.Param3 := dword(BaseAddress);
Query.Param4 := dword(Buffer);
WriteFile(hDriver, Query, SizeOf(TDriverQuery), Bytes, nil);
if ReturnLength <> nil then ReturnLength^ := Len;
if Len > 0 then Result := STATUS_SUCCESS;
end else
begin
ProcessId := GetPid(ProcessHandle);
StrCpy(PipeName, bPipeName);
ToHex(ProcessId, 8, sPid);
StrCat(PipeName, sPid);
hPipe := CreateFile(@PipeName,
GENERIC_WRITE or GENERIC_READ,
0, nil, OPEN_EXISTING, 0, 0);
if hPipe <> INVALID_HANDLE_VALUE then
begin
Req.Address := BaseAddress;
Req.Length := BufferLength;
WriteFile(hPipe, Req, SizeOf(TXDumpRequest), Bytes, nil);
ReadFile(hPipe, Len, SizeOf(dword), Bytes, nil);
ReadFile(hPipe, Buffer^, Len, Bytes, nil);
if ReturnLength <> nil then ReturnLength^ := Len;
Result := 0;
CloseHandle(hPipe);
end else Result := TrueZwReadVirtualMemory(ProcessHandle,
BaseAddress, Buffer,
BufferLength, ReturnLength);
end;
end;
我们看到,为了使dumper与注入的DLL互动在受保护的进程中使用了named pipes,其名字就是字符串eXtremeDumper和进程Id的HEX代码。读取内存的请求发向被dump的进程,在那里server端读取请求的内存并将数据和读取的字节数返回。dumper的server端的代码如下:
library xDump;
uses
Windows,
advApiHook,
NativeApi;
{$include string.inc}
type
PXDumpRequest = ^TXDumpRequest;
TXDumpRequest = packed record
ReqType: dword;
Address: pointer;
Length: dword;
end;
const
bPipeName = '\\.\pipe\eXtremeDumper'#0;
function SafeReadMemory(Addr, Buff: pointer; Size: dword): dword;
asm
push ebx
push edx
push ecx
push esi
push edi
push ebp
push offset @Handler
push fs:[0]
mov fs:[0], esp
mov esi, eax
mov edi, edx
rep movsb
mov eax, ecx
pop fs:[0]
add esp, 4
pop ebp
pop edi
pop esi
pop ecx
pop edx
pop ebx
ret
@handler:
mov ecx, [esp + $0C]
add [ecx + $B8], 2
ret
end;
function PipeThread(hPipe: dword): dword; stdcall;
var
Req: TXDumpRequest;
Bytes, Len: dword;
pBuff: pointer;
begin
ReadFile(hPipe, Req, SizeOf(TXDumpRequest), Bytes, nil);
GetMem(pBuff, Req.Length);
Len := Req.Length - SafeReadMemory(Req.Address, pBuff, Req.Length);
WriteFile(hPipe, Len, SizeOf(dword), Bytes, nil);
WriteFile(hPipe, pBuff^, Req.Length, Bytes, nil);
FreeMem(pBuff);
CloseHandle(hPipe);
end;
var
TrId: dword;
procedure PipeServerThread();
var
hPipe: dword;
PipeName: array [0..128] of Char;
sPid: array [0..8] of Char;
begin
StrCpy(PipeName, bPipeName);
ToHex(GetCurrentProcessId(), 8, sPid);
StrCat(PipeName, sPid);
repeat
hPipe := CreateNamedPipe(PipeName,
PIPE_ACCESS_DUPLEX or WRITE_DAC,
PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES, 1024, 1024, 5000, nil);
if hPipe = INVALID_HANDLE_VALUE then Exit;
if ConnectNamedPipe(hPipe, nil) then
CreateThread(nil, 0, @PipeThread, pointer(hPipe), 0, TrId);
until false;
end;
begin
CreateThread(nil, 0, @PipeServerThread, nil, 0, TrId);
end.
在client端连接pipe时会创建一个线程,该线程在SEH处理程序内部读取请求的数据,此后数据就流向client端。此代码唯一的缺点就是效率比较低,因为每个请求都是由单独的线程服务的。但是,实际上太高的效率也是多余,因为重要的是dump的获取,尽管有人会不满足,会编写更好的版本,但我这里就费点傻劲吧 :)。
现在我们回到内核模块信息的获取上来。为了实现这个目的,我将使用函数ZwQuerySystemInformation,class为SystemModuleInformation,这样函数会向我返回以下类型的结构体数组:
PSYSTEM_MODULE_INFORMATION = ^SYSTEM_MODULE_INFORMATION;
SYSTEM_MODULE_INFORMATION = packed record // Information Class 11
Reserved: array[0..1] of ULONG;
Base: PVOID;
Size: ULONG;
Flags: ULONG;
Index: USHORT;
Unknown: USHORT;
LoadCount: USHORT;
ModuleNameOffset: USHORT;
ImageName: array [0..255] of Char;
end;
PSYSTEM_MODULE_INFORMATION_EX = ^SYSTEM_MODULE_INFORMATION_EX;
SYSTEM_MODULE_INFORMATION_EX = packed record
ModulesCount: dword;
Modules: array[0..0] of SYSTEM_MODULE_INFORMATION;
end;
我们感兴趣的域是ImageName、Base和Size。我发现,在KernelMode下还可以通过链表PsLoadedModulesList来获取这项信息,但是用在这里就没什么意义了。
为了枚举进程加载的模块,PE Tools使用了Procs32.dll中的函数GetModuleFirst/GetModuleNext。为了映射自己的列表,我们将在System进程zhong拦截并替换成我们的版本。处理这些函数的代码如下:
function NewGetModuleNext(dwPID: dword; mEntry: PMODULE_ENTRY): bool; stdcall;
begin
if dwPID = SystemPid then
begin
Result := false;
lstrcpy(mEntry^.lpFileName, Modules^.Modules[CurrentModule].ImageName);
mEntry^.dwImageBase := dword(Modules^.Modules[CurrentModule].Base);
mEntry^.dwImageSize := Modules^.Modules[CurrentModule].Size;
mEntry^.bSystemProcess := true;
Inc(CurrentModule);
if CurrentModule > Modules^.ModulesCount then
ReleaseModulesInfo() else Result := true;
end else Result := TrueGetModuleNext(dwPID, mEntry);
end;
function NewGetModuleFirst(dwPID: dword; mEntry: PMODULE_ENTRY): bool; stdcall;
begin
if dwPID = SystemPid then
begin
if CurrentModule > 0 then ReleaseModulesInfo();
Modules := GetInfoTable(SystemModuleInformation);
Result := NewGetModuleNext(dwPID, mEntry);
end else Result := TrueGetModuleFirst(dwPID, mEntry);
end;
为了进行dump,PE Tools使用了NDump.dll中的DumpProcess函数,我们需要拦截这个函数并将dump请求发向我们自己的驱动里。处理程序的代码如下:
function NewDumpProcess(dwProcessId: dword; pStartAddr: pointer;
dwcBytes: dword; pDumpedBytes: pointer
 
bool; stdcall;
var
Query: TDriverQuery;
Bytes: dword;
begin
if dwProcessId = SystemPid then
begin
Query.QueryType := IOCTL_DUMP_KERNEL_MEM;
Query.Param1 := dword(pStartAddr);
Query.Param2 := dwcBytes;
Query.Param3 := dword(pDumpedBytes);
Result := WriteFile(hDriver, Query, SizeOf(TDriverQuery), Bytes, nil);
end else Result := TrueDumpProcess(dwProcessId, pStartAddr,
dwcBytes, pDumpedBytes);
end;
--------------------------------------------------------------------------------
eXtreme dumper可以分为以下部分:
两种dump的方法(Dll-Injection法和驱动法)
获取句柄的隐式方法(OpenProcessEx)
防止PE Tools受其它进程影响(Protect PE Tools)
对内核模块的dump(Enable kernel modules dumping)
简言之,这个plugin无疑是很有用的。
--------------------------------------------------------------------------------
我想提醒的是,写破解程序这类文章的作者们请不要进行违反俄联邦法规的活动。本文内容只能用于教学目的,对任何由此引起的非法行为,本作者不负有任何责任(对使用本译文进行违反中华人民共和国法规的行为,译者亦不负任何责任哦)。
[C] Ms-Rem
董岩 译
http://greatdong.blog.edu.cn