浅谈脱壳中的Dump技术

自己的总结:

dump原理: 1.CreateToolHelp32Snap,Process32First,Process32Next之类的函数获取到要dump进程.

                     2.CreateToolHelp32Snap,Module32First,Module32Next获取到镜像的大小。

                     3.ReadProcessMemory读取镜像内容,写入文件。

                     4.利用PE结构,调整块表的内容。

 

anti 手段:   1.隐藏镜像文件大小

 ////////////////////////////////////////////////////////////////////////////////////
  //这里的几个代码是修改PEB的关键
  __asm
  {
    mov eax,fs:[30h]                //获得PEB地址
    mov eax,[eax+0ch]               // +00c struct   _PEB_LDR_DATA *Ldr
    mov eax,[eax+0ch]               // _LDR_MODULE的首地址
    mov dword ptr [eax+20h],1000h   //eax+20是保存image size的地方

  }
////////////////////////////////////////////////////////////////////////////////////

解决方案:LoadPE 的 correct image size(从文件中读取镜像大小)。

 

               2.利用VirtualProtect修改镜像块读属性。

解决方案:利用OD加载来改回属性.

时机: 代码和IAT解压完毕后dump.

标 题: 浅谈脱壳中的Dump技术
作 者: Lenus
时 间: 2005-10-15,20:55:55
链 接: http://bbs.pediy.com/showthread.php?t=17624

前言:
    “知者不言,言者不知”
                                     ----老子 《道德经》
   你会dump吗?
   你还是只在OEP处dump吗?
   你知道dump的原理吗?
   你遇到过anti-dump的壳吗,你知道如何对付它吗?
   你明白几种dump工具的优劣吗?
   请原谅我,一开始就这么不识趣的抛出了这几个无里头的问题。我记得龙应台说过:正因为我那时什么都不懂,所以才会写下那些文字。我也是一个言者不知的人,这篇文章我将尝试的帮助大家去理解一个dump的原理和在脱壳中dump的技巧,如果这篇文章让你明白了一些东西,那么这篇文章的目的算是达到了。如果你是高手,也不用嘲笑我的无知,毕竟我们都是这么过来的,指出我的错误和不足是我更愿意看到的。
   此篇文章共分为五章。在前三章中,我试图在阐释一些dump的原理,但这需要一些C语言和windows编程的功底。相信这些部分会让很多人头疼。在后面的两章也许你会更加感兴趣,在第四章中我举了三个例子进行说明dump的技巧,而在第五章中我还会针对几种常用的dump工具发表一下自己不成熟的看法。
   好了,带着上面的问题,让我们开始我们的dump之旅。

一章.  Dump的原理
Knowledge is a treasure, but practice is the key to it.
 
知识是一宝库,而实践就是开启宝库大门的钥匙。
                                                          ――Fuller
如果说要讲dump的原理,那么我们最简单的办法就是自己写一个象LordPE那样的dump程序。下面我就来详细说明一下,一个dump程序是如何工作的,如何把内存中的数据保存到文件中的。

一.分析
    对于dump来说,他的英文翻译就是“转存”。也就是说把内存中或者其他的输入转存到另一个位置,当然对于我们现在说的dump就是把内存中运行的PE进程的数据,从内存中抓取出来,然后在用文件的形式保存下来。

   根据上面的分析我们基本上得到了一个这样的思维。Dump程序要做的事分几个基本的步骤:
1.  在系统中找到目标进程
2.  在进程中确定进程的大小imagesize
3.  把进程中的数据保存到文件

二.实现
好了,现在我们就可以具体来实现各个部分的功能了。

1.  在系统中找到目标进程就是实现一个基本的任务管理器的把进程列出的功能。
在这个实现中我们采用了BOOL GetProcessListFunc(HWND hDlg,HWND hWindList);这个函数里面调用了几个基本的函数:CreateToolhelp32Snapshot,Process32First, Process32Next
这个列出进程的功能就轻松的实现了。(具体的请看我的代码Lenus_dump_1.cpp)

2.  在进程中确定要dump的大小是一件很有意思的事情,我打算慢慢的讲解寻找这数据的办法,这一节我们先用一个基本的方法。
我们先用一个笨一点的办法,根据PE文件格式可以知道在距离PE文件头(IMAGE_NT_SIGNATURE)0x50的位置是存放imageofsize的地方。所以在int GetSizeOfImage(HWND hDlg,HANDLE hProcess);这个函数中,我们找到了进程以后打开目标进程,然后用ReadProcessMemory来读取imageofsize就可以了。
在这里我还用BOOL CheckPEFunc(HWND hDlg,HANDLE hProcess);函数简单的检查了了两个PE文件的标志(IMAGE_DOS_SIGNATURE和IMAGE_NT_SIGNATURE)
3.  在把进程中的数据保存到硬盘的文件的步骤中,我使用了一个比较笨的办法。
首先,我们用LPCTSTR SaveAsFunc(HWND hDlg);函数把打开一个save as的通用控件,获得要保存的文件名,默认的是dumped.exe。接着,使用前面在获得imageofsize的同时已经用GlobalAlloc函数申请了同样大小的堆空间,把目标进程的数据读到这个空间里面。这里面就有一个空间大小的问题,如果进程过大,我怀疑会申请失败。在这里还有尤其的注意一个地方,就是我们在申请空间的时候用了sizeoffile而不是sizeofimage,这是因为,有的时候sizeofimage的大小并不是一个文件对齐度的大小,而为了让他对齐,我使用下面的算法。
代码:
  if(!(sizeofimage%0x1000))                          //如果是文件对齐度的整数倍的时候就不处理
    sizeoffile=sizeofimage;
  else
    sizeoffile=(sizeofimage/0x1000+1)*0x1000;     //如果不是就增加一个文件对齐度

     这样就能保证,imageofsize转化为一个具有文件对齐度的数值。
     最后,是使用BOOL CreateDumpFile(HWND hDlg,LPCTSTR Dump_Name,HGLOBAL hMem);这个函数,这个函数的作用是在磁盘中申请一个文件,把从刚刚读到内存空间的数据又全部的写到磁盘上,所以就用了两个函数CreateFile和WriteFile。

三.试验
   写好了程序就如下图一样
下面用他来实战使用一下吧
运行用upx压缩过的win98的notepad

浅谈脱壳中的Dump技术_第1张图片


让我把他在oep处dump下来

嘿嘿,dump下来才发现,图表都没有了。呵呵,这是因为我们没有把相对虚拟地址(RVA)和文件地址对齐(RA)。
科普一下:在文件中要节约空间,把数据紧密的存储在一切,而靠节表在load到内存中的时候,把在文件中的不同数据分别隐射到不空内存空间中,而不足的地方就用0填充。但是当我把这些数据从内存中完完整整的dump下来的时候,我们同样把这些0也dump了出来。所以我们要做的首先应该是调整节表,让他的RA=RVA。这样我们实际上做的就是把RA这个指针移动过了若干个0的空间,从而指向正确的数据。

下面我们还是用LoadPE打开它,把RA和SIZE修改过来吧!

浅谈脱壳中的Dump技术_第2张图片

这么样,回到桌面刷新一下就会发现图标又回来了吧!
好了,现在运行dumped.exe文件,还是不行的。对了,IAT还没有修复呢,我们停在OEP的时候文件的IAT已经全部释放了出来,而IAT只是对于本计算机的函数地址,每一个计算机都可能不一样。这样我们就要用ImportRECl来重新根据这个IAT地址反向构造一个输入表。OK,使用ImportREC修复了以后就可以运行了,不过dump出来的文件大出了好多。我们可以知道原因了。
1.  因为文件对齐的原因我们把残留的0也dump出来了。
2.  因为使用ImportREC他帮我们重新创建了一个区段存放输入表
所以,我们使用LordPE的ReBuiltPe其中的一个功能就是可以把这些残留的0,自动的删除,然后修改节表让他们对齐。

四.小结
经过了一番的讲解,大家应该知道,LordPE的dump功能大概是怎么运作的了吧,当然从我们的程序从还有很多的不足,从上面例子也能知道了。LordPE在dump下程序的同时,直接将节表中的RA=RVA ,RS=RVS。那么在以后我们将继续改进我们的dump程序,让它增加这些功能。万事开头难,如果你有足够的耐心读完了这一章那么下面的章节就很简单了,不要怕继续吧!

第二章.    Dump程序的改进
“发展才是硬道理”
                                                   ----邓小平
   正如我们上面一节所做的,一个dump的雏形已经形成了,现在要做的是给它在原来的基础上做进一步的改进。

一.  获取Imageofsize

相信大家也感觉到了,在前面一章介绍的获取ImageofSize的办法实在很不可靠,对于一个加壳程序随意的修改他载入了内存中的PE头,是很稀疏平常的事情。所以用读取目标进程中的PE头并不是一个好主义。
现在我就来介绍一下一般的办法。对于LordPE他的dump是采用什么方法呢?它采用了对Module32Next来获取dump的进程的基本信息的。

BOOL WINAPI Module32First(
HANDLE hSnapshot, //这是先前的CreateToolhelp32Snapshot函数返回的快照
  LPMODULEENTRY32 lpme //这个是指向MODULEENTRY32结构的指针
);

下面是MUDULEENTRY32结构:
typedef struct tagMODULEENTRY32 {
  DWORD dwSize;
  DWORD th32ModuleID;
  DWORD th32ProcessID;
  DWORD GlblcntUsage;
  DWORD ProccntUsage;
  BYTE *modBaseAddr;
  DWORD modBaseSize;    //这个是是我们要获取的关键
  HMODULE hModule;
  TCHAR szModule[MAX_PATH];
  TCHAR szExePath[MAX_PATH];
  DWORD dwFlags;
} MODULEENTRY32, *PMODULEENTRY32, *LPMODULEENTRY32;

下面就给出重新写的新的GetSizeOfImage的函数
int GetSizeOfImage(HWND hDlg,DWORD IDProcess)
{
  //这个函数的作用是获取SizeOfImage的数值
  //当函数执行失败返回的是0
  //成功返回的是非0
  HANDLE hModuleSnap = NULL;
  MODULEENTRY32 stModE  = {0};
  stModE.dwSize = sizeof(MODULEENTRY32);
  hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,IDProcess);  //快照,对本进程中所有的模块进行snap

  if (hModuleSnap == INVALID_HANDLE_VALUE)
  {
    MessageBox(hDlg,TEXT("The Module snapshot can't get!"),TEXT("Error!"),MB_OK | MB_ICONSTOP);
      return FALSE;    //返回0
  }
  if (!Module32First(hModuleSnap, &stModE))
  {
     MessageBox(hDlg,TEXT("The Module32First can't work!"),TEXT("Error!"),MB_OK | MB_ICONSTOP);
     CloseHandle (hModuleSnap);
     return FALSE;
  }
  CloseHandle (hModuleSnap);
  return stModE.modBaseSize;//初始化为0
}

二.  对齐节表

这个问题在上一节我们已经提出了,我们的程序不完善,现在要自动的实现RA=RVA ,RS=RVS这个功能。那么我们就使用一个函数来完成它吧!我定义了下面这个函数:
BOOL ModifySectionFunc(HWND hDlg,LPCTSTR Dump_Name)
{
  //此函数的将修改dump下来的exe,使其RA=RVA ,RS=RVS
  //首先是打开dump文件
  HANDLE hFile=CreateFile(Dump_Name,GENERIC_WRITE | GENERIC_READ,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
  if(hFile==INVALID_HANDLE_VALUE)
  {

    MessageBox(hDlg,TEXT("I can open the dump file..."),TEXT("Error!!"),MB_OK | MB_ICONWARNING);
    return FALSE;
  }
    //下面移动到节表前面
  IMAGE_DOS_HEADER myDosHeader;
  DWORD NumberOfBytesRead;   
  ReadFile(hFile,(LPVOID)&myDosHeader,sizeof(IMAGE_DOS_HEADER),&NumberOfBytesRead,NULL);
  SetFilePointer(hFile,myDosHeader.e_lfanew+sizeof(DWORD),NULL,FILE_BEGIN);
    IMAGE_FILE_HEADER myNtHeader;
  ReadFile(hFile,(LPVOID)&myNtHeader,sizeof(IMAGE_FILE_HEADER),&NumberOfBytesRead,NULL);
  int nSectionCount;
  nSectionCount = myNtHeader.NumberOfSections;             // 保存Section个数
  // 过了IMAGE_NT_HEADERS结构就是IMAGE_SECTION_HEADER结构数组了,注意是结构数组,有几个Section该结构就有几个元素
  // 这里动态开辟NumberOfSections个内存来存储不同的Section信息
  IMAGE_SECTION_HEADER *pmySectionHeader = (IMAGE_SECTION_HEADER *)calloc(nSectionCount, sizeof(IMAGE_SECTION_HEADER));
  SetFilePointer(hFile,myDosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS),NULL,FILE_BEGIN);
    ReadFile(hFile,(LPVOID)pmySectionHeader,sizeof(IMAGE_SECTION_HEADER)*nSectionCount,
         &NumberOfBytesRead,NULL);
  //移动回到节表的开始,准备写入
  SetFilePointer(hFile,myDosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS),NULL,FILE_BEGIN);
  for (int i = 0; i < nSectionCount; i++, pmySectionHeader++)
  {
    //将RA=RVA ,RS=RVS
    pmySectionHeader->SizeOfRawData=pmySectionHeader->Misc.VirtualSize;
    pmySectionHeader->PointerToRawData=pmySectionHeader->VirtualAddress;
    //将修改好的数值写回
    WriteFile(hFile,(LPVOID)pmySectionHeader,sizeof(IMAGE_SECTION_HEADER),&NumberOfBytesRead,NULL);
  }
  // 恢复指针
  pmySectionHeader -=nSectionCount;

  if (pmySectionHeader != NULL)          // 释放内存
  {
    free(pmySectionHeader);
    pmySectionHeader = NULL;
  }

  // 最后不要忘记关闭文件
  CloseHandle(hFile);
  return TRUE;
}

这个函数是在生成了一个新的dump文件以后,然后在打开它,找到节表。把里面的数据修改过来。

三.  小节
这样,一个改进型的程序就搞定了。现在当我们用他dump下新的程序的时候得到的就是一个有图标的程序了。因为我们已经把节表修改过来了。下面一章我们将介绍anti-dump的技巧。

第三章.Anti-dump的原理
"我们踏进又踏不进同一条河,我们存在又不存在。"
                                                     ----赫拉克利特
    事物往往就是这样,它在发展中需要正面的也需要反面的,他们是对立统一在一起的。在前面我们的dump已经基本和一个LordPE的功能差不多了,现在我们要分析一下,一个程序是如何anti-dump的,同时我们也会给出一些简单的解决办法。

注意:下面所说的方法都是在NT内核下的执行的,并不涉及9x下的。

一.纠正imagesize
这里我们首先讨论一下关于LordPE的其中一个correct ImageSize的功能。

浅谈脱壳中的Dump技术_第3张图片

首先我们要知道,这个anti是如何产生的。根据上面的介绍我们知道,我们获得的image size是通过MODULEENTRY32的结构的快照,但是这个结构是从那里获得这些数据的呢?
答案是通过PEB(Process Environment Block)进程环境模块获得的。下面是PEB的结构。
struct   _PEB (sizeof=488)
+000 byte     InheritedAddressSpace
+001 byte     ReadImageFileExecOptions
+002 byte     BeingDebugged
+003 byte     SpareBool
+004 void     *Mutant
+008 void     *ImageBaseAddress
+00c struct   _PEB_LDR_DATA *Ldr
+010 struct   _RTL_USER_PROCESS_PARAMETERS *ProcessParameters
+014 void     *SubSystemData
+018 void     *ProcessHeap
+01c void     *FastPebLock
+020 void     *FastPebLockRoutine
+024 void     *FastPebUnlockRoutine
+028 uint32   EnvironmentUpdateCount
+02c void     *KernelCallbackTable
+030 uint32   SystemReserved[2]
+038 struct   _PEB_FREE_BLOCK *FreeList
+03c uint32   TlsExpansionCounter
+040 void     *TlsBitmap
+044 uint32   TlsBitmapBits[2]
+04c void     *ReadOnlySharedMemoryBase
+050 void     *ReadOnlySharedMemoryHeap
+054 void     **ReadOnlyStaticServerData
+058 void     *AnsiCodePageData
+05c void     *OemCodePageData
+060 void     *UnicodeCaseTableData
+064 uint32   NumberOfProcessors
+068 uint32   NtGlobalFlag
+070 union    _LARGE_INTEGER CriticalSectionTimeout
+070    uint32   LowPart
+074    int32    HighPart
+070    struct   __unnamed3 u
+070       uint32   LowPart
+074       int32    HighPart
+070    int64    QuadPart
+078 uint32   HeapSegmentReserve
+07c uint32   HeapSegmentCommit
+080 uint32   HeapDeCommitTotalFreeThreshold
+084 uint32   HeapDeCommitFreeBlockThreshold
+088 uint32   NumberOfHeaps
+08c uint32   MaximumNumberOfHeaps
+090 void     **ProcessHeaps
+094 void     *GdiSharedHandleTable
+098 void     *ProcessStarterHelper
+09c uint32   GdiDCAttributeList
+0a0 void     *LoaderLock
+0a4 uint32   OSMajorVersion
+0a8 uint32   OSMinorVersion
+0ac uint16   OSBuildNumber
+0ae uint16   OSCSDVersion
+0b0 uint32   OSPlatformId
+0b4 uint32   ImageSubsystem
+0b8 uint32   ImageSubsystemMajorVersion
+0bc uint32   ImageSubsystemMinorVersion
+0c0 uint32   ImageProcessAffinityMask
+0c4 uint32   GdiHandleBuffer[34]
+14c function *PostProcessInitRoutine
+150 void     *TlsExpansionBitmap
+154 uint32   TlsExpansionBitmapBits[32]
+1d4 uint32   SessionId
+1d8 void     *AppCompatInfo
+1dc struct   _UNICODE_STRING CSDVersion
+1dc    uint16   Length
+1de    uint16   MaximumLength
+1e0    uint16   *Buffer

我们从FS:[30]就可以获得这个PEB的首地址。然后在0C处的_PEB_LDR_DATA *Ldr是一个关键通过它,我们能访问到

typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

该结构的后三个成员是指向LDR_MODULE链表结构中相应三条双向链表头的指针,分别是按照加载顺序、在内存中的地址顺序和初始化顺序排列的模块信息结构的指针。于是通过它,我们能访问到_LDR_MODULE结构,而这里面包括了本进程的SizeOfImage。
_LDR_MODULE结构如下:
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;                   //进程的image size
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;

所以,我们得到关键的代码就是:
////////////////////////////////////////////////////////////////////////////////////
  //这里的几个代码是修改PEB的关键
  __asm
  {
    mov eax,fs:[30h]                //获得PEB地址
    mov eax,[eax+0ch]               // +00c struct   _PEB_LDR_DATA *Ldr
    mov eax,[eax+0ch]               // _LDR_MODULE的首地址
    mov dword ptr [eax+20h],1000h   //eax+20是保存image size的地方

  }
////////////////////////////////////////////////////////////////////////////////////
上面的代码的作用就是把image size的大小改为了1000h,这样我们用MODULEENTRY32得到的大小是不准确的。所以大家在跟踪壳的时候特别要小心上面的几行代码。如果看见它就直接跳过吧。

好了,知道他是怎么anti的以后看看LordPE是怎么使用correct ImageSize的,我稍微跟踪了一下它的的代码。发现了下面的API函数:
1.  GetProcessBaseSize
2.  GetProcessPath
3.  CreateFileA
4.  ReadFile
看到这里想必大家也知道,怎么回事了吧。他在获得了绝对路径以后打开了磁盘上的PE文件,读取里面PE头的ImageSize,来纠正这个错误了的image size。恩,或许你会说,那么我写一个anti来修改PE文件头的ImageSize,让它获得的还是错误的。呵呵,我不得不提醒你的是,任何对文件的操作都是危险的。我们来设想一下这种情形:当你把原来的PE文件打开了以后,把一个错误的ImageSize写入了这个PE文件里面,使LordPE的correct ImageSize仍然无功而反,而打算在某一个地方,再次打开文件把正确的信息写回PE文件。(否则你下次就别想打开它了)且不说这样的办法很烦琐,我们设想一下这个时候,你的机器死机了!你不得不重起机器。而你的收尾工作还没结束。于是你的PE文件除了anti的作者,别人在不知道为什么的情况下,再也别想打开它了。

   所以一个聪明的程序员,都不会做这种危险的事情。好了,来看看我是如何实现这个功能的吧。我在原来的基础上填加了一个按钮“纠正大小”的按钮。那再重新定义了一个函数来实现这个功能:

BOOL CorrectSizeFunc(HWND hDlg,HWND hWindList)
{
  //函数能获取文件的PE头部的SizeOfImage,作为正确的SizeOfImage
    LPCTSTR File_Name=NULL;                  
    WPARAM tmp=(WPARAM)SendMessage(hWindList,LB_GETCURSEL,0,0);
  if (tmp==LB_ERR)
  {
    MessageBox(hDlg,TEXT("Please choose a process..."),TEXT("oh...no,no,no..."),MB_OK);
    return FALSE;
  }
  DWORD IDProcess=SendMessage(hWindList,LB_GETITEMDATA,tmp,0); //获得此列单里面的进程ID
  ID=IDProcess;//全局变量ID的作用是控制在不同的进程的切换
  File_Name=GetFilePath(hDlg,IDProcess);
  if(!File_Name)
    return FALSE;
    //打开文件
  HANDLE  hFile;
  hFile=CreateFile(File_Name,GENERIC_READ,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

  if (hFile == INVALID_HANDLE_VALUE )
  {
    return FALSE ;
  }

  //创建文件映射内核对象
  HANDLE hMapping;
  hMapping =  CreateFileMapping (hFile, NULL, PAGE_READONLY,0,0,NULL);
  if (hMapping == NULL )
  {
    CloseHandle (hFile ) ;
    return FALSE;
  }
  //创建文件视图
  LPVOID ImageBase ;
  ImageBase =MapViewOfFile(hMapping,FILE_MAP_READ,0,0,0) ;
  if (ImageBase == NULL)
  {
    CloseHandle (hMapping) ;
    return FALSE;
  }
    //下面的代码就是从文件的PE头找到SizeOfImage的
  PIMAGE_DOS_HEADER DosHead = NULL ;
  PIMAGE_NT_HEADERS32 pNtHeader = NULL ;
    PIMAGE_FILE_HEADER pFileHeader = NULL ;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL ;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL ;
  DosHead=(PIMAGE_DOS_HEADER)ImageBase;
  pNtHeader = ( PIMAGE_NT_HEADERS32 ) ((DWORD)ImageBase + DosHead->e_lfanew ) ;
  pOptionalHeader = &pNtHeader->OptionalHeader; 
    sizeofimage=(int)pOptionalHeader->SizeOfImage;
    //找到了以后,输出结果
  char szBuffer[100];
  char szMsg[]="原来的image size是:%08X\n修整的image size是:%08X";
  wsprintf(szBuffer,szMsg,GetSizeOfImage(hDlg,IDProcess),sizeofimage);
  MessageBox(hDlg,szBuffer,TEXT("纠正结果"),MB_OK );
    CloseHandle (hMapping);
    CloseHandle (hFile) ;
  Sleep(200);
  return TRUE;
}

这个功能实现到是很简单,但是同时要强调的一点是,在我在协调纠正的进程和dump进程是不一样的进程的时候,出了一些小问题。我采用了一个很愚蠢的办法,定义了一个全局变量ID来控制这种事情的发生。

二.实战

   这个modify_module_imagesize程序包括了上面的修改PEB里面image size的代码。运行它:

  浅谈脱壳中的Dump技术_第4张图片


可以看到原来的image size已经被修改了。

好了,现在用我们的dump来试试吧。

浅谈脱壳中的Dump技术_第5张图片

浅谈脱壳中的Dump技术_第6张图片


这样我们再dump它,就能得到原来C000大小的进程了。


二.其他anti方式
  根据上面的MODULEENTRY32结构,我们能得到很多启发。毕竟懂得了原理,那么寻找anti的办法是不难的。首先是基地址,我们能不能修改PEB中的基地址呢?
   事实上,这种方法会产生不必要的麻烦。也就是说不可行。
   另一种办法是修改文件的路径,这种办法很好。但是只是能在9x中使用,因为目前为止,不知道在NT内核下的这种办法。所以在这里我也不介绍了。有兴趣的读者可以去看看《软件加密技术内幕》P204-P207,在那里hying有完整的介绍。
  下面要介绍的是一种非常普遍,也很常用的办法。就是利用内存属性进行anti。
  我们知道,如果我们打开了一个进程,那么要读写进程里面的内容,必须要获得应有的权限读权利。实际上,当一个exe文件被load到内存里面的时候,他的所有取段都具有读的权利,而正是这样我们才能把他从内存中读出来,写到磁盘文件上面。所以,我们的anti来了,如果我们将部分不用区段的权限修改一下,把他的读权限取消。会发生什么结果呢?
  让我们来做一个简单的实验吧!
/*------------------------------------------------------------------------
  Save as Modify_the_read_right.cpp
                         (c) Lenus Margin 2005 10.15
-------------------------------------------------------------------------*/

#include <windows.h>
#include <iostream.h>
#include <stdio.h>

void main()
{
  DWORD dwOldProtect; 
  HMODULE ImageBase=GetModuleHandle(NULL);
  VirtualProtect(ImageBase,1000,PAGE_NOACCESS,&dwOldProtect);//将PE头改为不可访问的的属性   
  cout<<"The PE HEAD read right change to no_access!!"<<endl;
  cout<<"U can try to dump at here!"<<endl;
  getchar();
}

等运行了以后,我们用LordPE来dump它吧,发现了什么没有?一个经典的对话框!

浅谈脱壳中的Dump技术_第7张图片

这个东西,相信大家一定见过。让我们看看,在这个进程里面的400000处吧!

浅谈脱壳中的Dump技术_第8张图片

选中进程---右键---dump region就可以看到上面的窗口了。果然已经变成了NOACCESS的属性了。
那么应该怎么解决呢?这个我们就要请出更强的工具来了。现在我们就用OD的ollydump来dump它!
用OD载入以后,F9运行!好了,这时我们可以用LordPE还是看到上面的NOACCESS的属性,但是我们到OD的内存镜象里面看看是什么结果呢?

浅谈脱壳中的Dump技术_第9张图片

我们可以看到,他仍然是可读,可写,可执行的权限。
为什么会这样呢?
因为OD这个调试器需要编辑这个进程的空间中的数据,而如果一个区段因为设置了NOACCESS而得不到访问,不能编辑实在是不可想象的。所以他在我们需要使用这段空间的时候就将它的属性临时的修改到了完整的权限,而当我们使用完毕以后呢?再修改回来,可以说,ring3的调试器老是在干这种事情,比如说int3的放置也是这样的。从这些侧面我们也能体会到OD的强大。

好了,现在用ollydump就可以dump下来了。

四.小结
小结一下。关于anti这一章,以上的这些只是一些常见的手段。事实上,矛盾的较量永远不会停止,正如开篇的赫拉克利特所说"我们踏进又踏不进同一条河,我们存在又不存在。"在哲学上事物是矛盾的统一,没有了此消彼长的竞争,也没有了任何存在的可能。而在加密解密的世界anti-dump也是一个大学问,从而我们在避开anti的同时也会发现很多很巧妙的方法。好了原理就说到这里,下面一章我们将转入实战,让你感受另类的脱壳!

 

第四章.Dump的位置
The Golden Rule is that there are no golden rules.
                真正的金科玉律就是世上并无金科玉律。――G.B.Shaw
   现在,你是不是还认为要dump就一定只能在OEP处呢?你现在是不是还觉得dump是一件没有技术含量的工作呢?好,下面我将详细的说明一下在什么地方dump将会得到事半功倍的效果!
一.  在OEP处dump
    一般来说,入门及的菜鸟都知道,在oep处dump下进程是正确的,当OD停在push ebp的时候,我们冲动的神情就连显示器也会害怕。在LordPE,Ollydump…..十八般兵器全用上以后,如果我们得到的是一个不能dump的对话框,那么一切就完了,此壳也因此打入冷宫!
   那么,我们首先要知道。为什么我们要在OEP处dump呢?
   一般来说,一个简单的压缩壳(比如UPX),它在运行的时候会做什么事情呢?
1.  它要把压缩了的全部的代码数据释放到内存中。
2.  它要把IAT的地址写到适当位置
   那么,它什么时候完成这些工作呢?
   答案是最后一刻!
   对,让我们来看看UPX的最后几句!
0040EA8D    09C0           or eax,eax
0040EA8F    74 07          je short NOTEPAD.0040EA98
0040EA91    8903           mov dword ptr ds:[ebx],eax                   ; kernel32._lwrite
0040EA93    83C3 04        add ebx,4
0040EA96  ^ EB E1          jmp short NOTEPAD.0040EA79
0040EA98    FF96 A8EC0000  call dword ptr ds:[esi+ECA8]
0040EA9E    61             popad
0040EA9F  - E9 2826FFFF    jmp NOTEPAD.004010CC
明显它在做“写”的工作,当写完了也就JMP OEP了。所以当它完全解压缩了以后,当它把IAT都写好了以后。我们就可以dump它了。而我们更多的选择在OEP的地方就是这个原因。因为在OEP处,它的代码和IAT一般是会解压完毕了的。
二.  不在OEP处dump
现在我们知道了,dump的原则了以后。那么很好办,我们不一定要在OEP处dump了。好,我们可以在jmp NOTEPAD.004010CC这里dump,也可以在0040EA9E    61             popad这里dump。当然还可以在
004010CC    55             push ebp
004010CD    8BEC           mov ebp,esp   //在这里
004010CF    83EC 44        sub esp,44  //在这里
哈哈,什么位置都可以dump!为什么一定要在OEP处呢?!
三.  他山之石
下面我们来看看两篇高手的范文,让我们更深刻的理解一下dump的技巧,dump的花样。^-^

1.  首先是fly的《Thinstall V2.501脱壳――Win98的Notepad》
http://bbs.pediy.com/showthread.php?s=&threadid=8158
下面是引用部分

二、获取加壳前程序的PE Header等数据


Ctrl+S 搜索命令序列:
mov eax,dword ptr ds:[eax+3C]
mov ecx,dword ptr ss:[ebp-118]
找到在7FF427BA处,直接F4过去

7FF427A5     8B85 D4FDFFFF     mov eax,dword ptr ss:[ebp-22C]
7FF427AB     8B40 18           mov eax,dword ptr ds:[eax+18]
7FF427AE     8985 E8FEFFFF     mov dword ptr ss:[ebp-118],eax
7FF427B4     8B85 E8FEFFFF     mov eax,dword ptr ss:[ebp-118]
7FF427BA     8B40 3C           mov eax,dword ptr ds:[eax+3C]
//F4到这里
7FF427BD     8B8D E8FEFFFF     mov ecx,dword ptr ss:[ebp-118]
7FF427C3     8D4401 18         lea eax,dword ptr ds:[ecx+eax+18]
7FF427C7     8985 D0FDFFFF     mov dword ptr ss:[ebp-230],eax  ; Notepad.004000E8
//在这里可以得到PE Header和The Section Table数据 ★
7FF427CD     C705 D05DF97F 020>mov dword ptr ds:[7FF95DD0],2

PE Header:
004000D0  50 45 00 00 4C 01 01 00 D6 57 5A 35 00 00 00 00  PE..L  .肿Z5....
004000E0  00 00 00 00 E0 00 0E 01 0B 01 03 0A 00 40 00 00  ....?
   ..@..
004000F0  00 70 00 00 00 00 00 00 CC 10 00 00 00 10 00 00  .p......?... ..
00400100  00 50 00 00 00 00 40 00 00 10 00 00 00 10 00 00  .P....@.. ... ..
00400110  04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00   ....... .......
00400120  C6 CA 00 00 00 04 00 00 00 00 00 00 02 00 00 00  剖... ...... ...
00400130  00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00  .. .. .... .. ..
00400140  00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00  .... ...........
00400150  00 60 00 00 8C 00 00 00 00 70 00 00 C8 42 00 00  .`..?...p..嚷..
…… ……

毕竟只是捆绑壳,此时代码已经解压,现在dump则IAT等信息都是未加密的,正是脱壳的最佳时机!

  我们可以看到,文中提到在跟踪到7FF427BA的时候整个IAT的信息已经解压完毕,而且还没加密,并且这时的代码也释放完毕了。所以在这里是完美的dump时机。而如果我们在寻找到OEP处的时候,那么一切都已经完了。狡猾的壳,把该隐藏的都隐藏起来了。那时我们再去dump,得到的只是作者想要我们得到的东西,那只能说我们中了圈套了!

2.  接着是csjwaman的《Steel Box 脱壳》
http://bbs.pediy.com/showthread.php?s=&threadid=8356
下面是引用部分
共有4处要修复。其中第4处是程序退出时运行的。004041D5处下断,运行程序,然后退出程序。程序就会断在004041D5处。此时要修复的代码都已解开:
00404100    55              PUSH EBP
00404101    8BEC            MOV EBP,ESP
00404103    53              PUSH EBX
00404104    56              PUSH ESI
00404105    57              PUSH EDI
00404106    BB 00704000     MOV EBX,anota2.00407000
0040410B    66:2E:F705 8E47>TEST WORD PTR CS:[40478E],4
00404115    0F85 DB000000   JNZ anota2.004041F6
0040411B    6A 00           PUSH 0
0040411D    FF15 D0824000   CALL NEAR DWORD PTR DS:[4082D0]
00404123    E8 9C020000     CALL anota2.004043C4
00404128    C783 08010000 0>MOV DWORD PTR DS:[EBX+108],1////已修复处1
00404132    8D83 94020000   LEA EAX,DWORD PTR DS:[EBX+294]
00404138    50              PUSH EAX
00404139    FF15 84824000   CALL NEAR DWORD PTR DS:[408284]
0040413F    83EC 44         SUB ESP,44
00404142    C70424 44000000 MOV DWORD PTR SS:[ESP],44
00404149    C74424 2C 00000>MOV DWORD PTR SS:[ESP+2C],0
00404151    54              PUSH ESP
00404152    FF15 7C824000   CALL NEAR DWORD PTR DS:[40827C]
00404158    B8 0A000000     MOV EAX,0A
0040415D    F74424 2C 01000>TEST DWORD PTR SS:[ESP+2C],1
00404165    74 05           JE SHORT anota2.0040416C
00404167    0FB74424 30     MOVZX EAX,WORD PTR SS:[ESP+30]
0040416C    83C4 44         ADD ESP,44
0040416F    8983 FE000000   MOV DWORD PTR DS:[EBX+FE],EAX
00404175    FF15 60824000   CALL NEAR DWORD PTR DS:[408260]
0040417B    E8 21040000     CALL anota2.004045A1
00404180    8983 F4000000   MOV DWORD PTR DS:[EBX+F4],EAX
00404186    FF15 68824000   CALL NEAR DWORD PTR DS:[408268]
0040418C    8983 F0000000   MOV DWORD PTR DS:[EBX+F0],EAX
00404192    6A 00           PUSH 0
00404194    FF15 78824000   CALL NEAR DWORD PTR DS:[408278]
0040419A    8983 04010000   MOV DWORD PTR DS:[EBX+104],EAX
004041A0    E8 07010000     CALL anota2.004042AC
004041A5    B8 FF000000     MOV EAX,0FF////已修复处2
004041AA    72 36           JB SHORT anota2.004041E2
004041AC    E8 5E020000     CALL anota2.0040440F
004041B1    E8 3B040000     CALL anota2.004045F1
004041B6    B8 FF000000     MOV EAX,0FF////已修复处3
004041BB    72 07           JB SHORT anota2.004041C4
004041BD    53              PUSH EBX
004041BE    E8 57010000     CALL anota2.0040431A
004041C3    5B              POP EBX
004041C4    50              PUSH EAX
004041C5    E8 55010000     CALL anota2.0040431F
004041CA    E8 37010000     CALL anota2.00404306
004041CF    FFB3 F0000000   PUSH DWORD PTR DS:[EBX+F0]////已修复处4
004041D5    FF15 5C824000   CALL NEAR DWORD PTR DS:[40825C]

把这些代码复制后覆盖到主程序404100-4041D4处就完成了。
上面的这段是在说明这么修复代码的段落。这壳在到达OEP处的时候有4个地方,并没有将代码完全的解压。所以我们在OEP处dump就得到4处没有解压的代码,而文中提到的办法是,先把程序dump下。等待程序运行把这些代码解压以后再进行粘贴。
“把这些代码复制后覆盖到主程序404100-4041D4处就完成了。”
这当然是个很好的办法,符合我们脱壳的逻辑。而我们在懂得他的原理以后大可大胆的想象。我们在等待这次处接压完毕以后再dump会如何呢?
四.  实战
下面我们来脱一个比较有趣的壳吧!听说这个壳在9x是秒脱,但是在NT内核下的就不是那么简单的了。^-^
目标:VBExplorer
加壳方式:Shrinker 3.4
OD载入以后
寻找OEP的过程很简单,在找到最后的异常以后
00554E7B    8B1F           mov ebx,dword ptr ds:[edi]//最后的异常
00554E7D    85DB           test ebx,ebx
00554E7F    74 6C          je short VBExplor.00554EED
00554E81    837D F4 00     cmp dword ptr ss:[ebp-C],0
00554E85    74 08          je short VBExplor.00554E8F

   我们用F8走几步就能到达JMP OEP的地方
005546AE    8945 E0        mov dword ptr ss:[ebp-20],eax
005546B1    FF75 10        push dword ptr ss:[ebp+10]
005546B4    FF75 0C        push dword ptr ss:[ebp+C]
005546B7    FF75 08        push dword ptr ss:[ebp+8]
005546BA    FF55 E0        call dword ptr ss:[ebp-20]                   ; VBExplor.00425342
//上面的就是JMP OEP的地方

下来我们看看OEP是什么东西:
00425342    0000           add byte ptr ds:[eax],al         //OEP
00425344    0000           add byte ptr ds:[eax],al
00425346    0000           add byte ptr ds:[eax],al
00425348    0000           add byte ptr ds:[eax],al
0042534A    0000           add byte ptr ds:[eax],al
0042534C    0000           add byte ptr ds:[eax],al
0042534E    0000           add byte ptr ds:[eax],al
00425350    0000           add byte ptr ds:[eax],al
00425352    0000           add byte ptr ds:[eax],al
00425354    0000           add byte ptr ds:[eax],al
00425356    0000           add byte ptr ds:[eax],al

这就是我们得到的OEP,怎么回事呢?怎么没有数据呢?
   这个壳在之前并没有解压这个壳的代码和数据,而当我们在运行的时候执行或者读取这些指令那么就会产生异常,而这时壳代码再试图将他们解压。哈哈…现在知道了吧,如果我们在OEP处dump能得到什么呢?!
   好了,我们下断点BP ExitProcess。对!我们要在它退出的时候断下来!
   OK,我们忽略掉所有的异常,运行它吧!让主界面出现在我们的机器上!这时我们要做的是让他的代码完全的从内存中释放出来!如果你不放心还可以使用一下这个软件的功能^-^

浅谈脱壳中的Dump技术_第10张图片

   好了,退出吧,让它中断下来!
7C81CAA2 >  8BFF           mov edi,edi    //断在这里
7C81CAA4    55             push ebp
7C81CAA5    8BEC           mov ebp,esp
7C81CAA7    6A FF          push -1

   行了,我们可以dump了….等等,在这里dump大家都明白它的原理了吧!使用LordPE来看看!出错了,不行吧!好看看是怎么回事呢?

浅谈脱壳中的Dump技术_第11张图片

   发现了什么没有,原来它在释放数据的过程中,只释放了必要的一部分而另一部分并没有改变它的属性仍然是NOACCESS的属性。那么好吧!用OllyDump上吧。可是你会发现还是不行啊!

浅谈脱壳中的Dump技术_第12张图片
 
   确定以后虽然能dump下来,但是却是0K的大小!
呵呵,为什么我们以前举的例子里面就可以,现在就不可以了呢?
答案很简单,我们以前是把整个区段从开头都设置成了不可访问的区段,而现在的只是把其中的一块设置成了NOACCESS,也就是说这个技巧欺骗了OD!
现在怎么办!
Fellow Me!!
用Alt+M打开内存镜象!

浅谈脱壳中的Dump技术_第13张图片


看到“完整权限”的字样了没有?我们将每个区段重新都设置它为完整的权限一次!
好了,现在我们再用LordPE看看吧!

浅谈脱壳中的Dump技术_第14张图片


     看到了吧,不得不佩服OD的强大功能啊!

现在你可以用任何的工具将它dump下来了!注意OEP=425342
用ImportREC可以简单的修复它的输入表。但是遗憾的是,事情还没有结束,在修复完了以后还有些毛病!
那就是运行以后,所有的资源好象都不见了,其实它只是有部分代码没有执行而已。OK,关于dump的我就讲这么多了。以后的完美脱壳,就留给大家去努力吧!
五.  小结
这是一个站在巨人肩膀上的时代,多东西需要借助别人的经验继续工作。而吸收和改进将是这个工作必要的阶段。
也许看了上面的文字,你会问我2个问题:
1.  我怎么知道代码或者IAT什么时候解压完毕?
2.  我怎么确定这个是最佳的dump的时机?
说实在的,这个是一个经验和辛苦的工作,作为菜鸟加懒鸟的我实在不愿意一步一步的跟踪,把整个壳的思想弄清楚。如果你能作到把整个壳的思路弄明白,把这个壳逆向一遍。那么我想上面的问题也是自然而然的解决了。所以,我们要对那些把整个壳的代码逆向的高手们致敬。同时我也要告诉各位,只要大家有时间,应该试这向这个方面努力。毕竟,只有这个方向才是一个真正的cracker应该做的,因为逆向学习才是cracker的第一精神,而破解的成果是第二位的。

第五章.Dump工具的比较
“今天将要结束,明天也将结束,难以结束的是昨天。”
                                                    ---安东尼奥•波契亚
在论坛上经常会看到一些人问:什么工具dump好用?为什么要用ollydump来dump呢?等等。下面我来总结一下几种dump工具的优缺点。

注意:下面的文字只是我个人的感觉,由于水平问题很难保持客观性。请三思!

一.  比较

1.  ProcDump
  这个工具我现在也不太用了,他是使用MODULEENTRY32来获取进程信息的工具,它会读取目标进程的IMAGE_DOS_SIGHATURE和IMAGE_NT_SIGNATURE标志。其他的就不检验了。如果不完整,他会打开进程的原始文件,读取它的原始文件,读取它的文件头以取代进程的文件头。
浅谈脱壳中的Dump技术_第15张图片

2.  LordPE
  这个工具我使用得最多,但并不是说它是最好的。只是一个习惯问题。它基本上也和ProcDump一样使用MODULEENTRY32来获取进程信息。然而我们可以看到LordPE的Options里面的设置
浅谈脱壳中的Dump技术_第16张图片

  在这里面就有直接从磁盘获取PE Head的默认选项,也就是说基本上他的PE Head都是从磁盘获得的。

  同时,在观察区域属性和获取其他的区域数据的时候LordPE的dump region功能非常强大,这点是OllyDump不足的。在以壳解壳的技巧中,LordPE的此功能将是决定意义的。但是它的不足前面也提到过,有些进程在权限上不可读的时候,将是它致命的伤!
浅谈脱壳中的Dump技术_第17张图片
3.  OllyDump
  这是一个OD的插件,基本上的原理和功能差不多。但是,正是由于它是OD的插件,它能通过OD获得更多的权限。在前面的例子中我们能看到,对于内存属性的问题,依靠OD强大的功能OllyDump能dump下一些LordPE所不能dump的进程。而它的“获取EIP值作为OEP”也是一个亮点。
浅谈脱壳中的Dump技术_第18张图片 
  至于缺点那么,最最突出的就是上面的默认的重建输入表选项。我们知道,对于一些加密壳来说,他们的输入表就连ImportREC这样专业的输入表修复工具,它的三个等级的修复也未必能修复出来,而这两个方式1和方式2的确有点添足的意思。其实我并不是反对它增加功能,我只是希望不要做成默认的选项!当然如果还是不改进,有空还是要DIY一下的。^-^
第二个缺点,就是它不能dump下其他区域的数据,这一点很是无奈。这也是为什么我比较习惯使用LordPE的原因。
二.小结
  工具的好坏,只是思想和工作程度的问题。只要我们掌握了工具工作的原理和思想,那么加以时日,我们也可以写出自己用着顺手的工具来。“真正的cracker高手的工具都是自己写的,内部使用的”对于前半句,我双手赞成,对于后半句嘛….我觉得cracker的精神里面除了学习(learn)的第一精神以外,还应该是共享(share)的精神。

附件:第五章Word文档及实例
-------------------------------------------
总结:
Dump技术在脱壳中的重要地位不言而喻,事实上这是由于这个from memory to disk的过程,我们才会把这个“脱壳”这两个字理解得如此形象。
在基本的脱壳概念中,我们似乎一直被公式化,教条化的东西是束缚着,在这里我觉得我这篇文章要教会大家的不仅仅是dump的技巧,而是要在原理的层次上进一步的探索,对于只能照本宣科的人来说,创新是永远不可能的事情。而如果不能真正的理解原理,理解为什么那么我们只能永远的停留在应用的阶段。
由于时间关系,在文中有大量的代码和一些概念由于时间关系没有解释清楚,而对于内存属性的保护,以及模拟LordPE的更多的功能将是以后和各位的工作了。这一点我觉得“砖”能这样抛,相信“玉”也能令人期待了。

致谢:
在国庆期间,我决定写这篇文章的时候。我得到了很多人的帮助和鼓励。首先,要感谢的是朋友,他们是love8909和sen两位兄弟,而发表在成员区的时候也得到了论坛成员的的鼓励。当然,你可能说这些东西都是别人的。没错,基本上我上面所学习到的知识并不新颖,是《软件加密技术内幕》中hying教会了我决大部分东西,要好好的谢谢他(也许他并不这么认为)。当然还有KanXue大哥,在他的支持下我能顺利的发表这篇文章。而在编写代码的同时,对于进程模块的exe确定的问题,也捆饶了我很久,在这里我要谢谢forgot,是他的提醒,我才能坚定了我的做法!还有很多要感谢的人,但是在最后我却想把这篇文章送给一个我最爱的女孩,经管也许她永远不会看到这些话….

参考文献:

《软件加密技术内幕》--- 看雪学院
《windows环境下32位汇编语言程序设计》--- 罗云彬
《Windows 程序设计》---- Charles Petzold
《JIURL玩玩Win2k进程线程篇 PEB》--- JIURL
《病毒编程技术-API函数地址的获取》--- 温玉洁
《PE结构分析软件--SDK开发实践》--- 北极星2003
《PE文件之旅》 ---  prince

个人简介:
马骏(Lenus Margin),生于1981.10.12,广西柳州 ,在度过了痛苦6年小学,比较痛苦的6年中学,现在在南京邮电大学度过非常痛苦的7年大学生活。本科专业是电子科学与技术(基本上和电子没有关系,关心的只是光子)。目前于南邮研究生二年级,继续研究“光学工程”专业“光通信与光信息处理” 方向。
于2004年7月跨入密界,经过一年的潜水终于混成了菜鸟。不想上站一看,发现廉颇老矣。密界新人倍出,而老将背影还未曾望见。也罢,写此小文聊以慰籍。望各位看官走过路过,千万不要骂过….阿弥陀佛,善哉,善哉。

你可能感兴趣的:(浅谈脱壳中的Dump技术)