现代dump技术及保护措施[Ms-Rem](下)

严易安
2023-12-01

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

董岩 译

 类似资料: