注:题目来自于以下链接地址:http://www.pediy.com/kssd/
目录:第13篇 论坛活动 金山杯2007逆向分析挑战赛 第一阶段 第二题 题目 [第一阶段 第二题]
题目描述:
己知是一个 PE 格式 EXE 文件,其三个(section)区块的数据文件依次如下:(详见附件)
_text,_rdata,_data
1. 将 _text, _rdata, _data 合并成一个 EXE 文件,重建一个 PE 头,一些关键参数,如 EntryPoint,ImportTable 的 RVA,请自己分析文件获得。合并成功后,程序即可运行。
2. 请在第1步获得的EXE文件基础上,增加菜单。具体见图:
3. 执行菜单 Help / About 弹出如下图所示的 MessageBox 窗口:
题目分析和解答:
(一)拼接可执行文件:
首先下载题目的附件,附件中已经有三个文件,分别是 PE 文件的三个 section,可以看到三个 section 文件已经按照 0x1000 大小对齐。这样我们只需要把这三个文件依次连接在一起,接在一个正确的 PE 文件头后面就可以了。
可以先用 VC (我采用 VS2005)创建一个 Windows 窗口程序(它将提供一些主要样本,所以称这个程序为样本程序),把程序写的尽可能和题目中的程序类似,然后编译,即首先得到了一个 PE 文件头的原型,再次基础上进行修改,也就是根据题目给出的 section,适当调整 PE 文件头中的需要修改的字段。
在本题求解过程中,我严重依赖于我从前写的一个展示 PE 文件格式的应用程序,此程序最近经过我的调整和改进,它的优点是由于此程序基于扩展 TreeView 控件,因此帮助快速理解 PE 文件头的结构,其效果见以下截图:
关于此程序的更多信息,请参见我的博客文章:《[VC6] 图像文件格式数据查看器》。
BmpFileView 的可执行文件的下载链接(不敢说它是最好的,但作为帮助学习PE文件格式的辅助工具而强烈推荐):
http://files.cnblogs.com/hoodlum1980/BmpFileView_V2_Bin.zip
观察题目给出的三个 section 文件,可以给出这三个 section 的基本信息如下:
SectionName | VirtualAddress | RawDataSize | VirtualSize |
---|---|---|---|
.text | 1000h | 6000h | 5B73h |
.rdata | 7000h | 1000h | 0C6Eh |
.data | 8000h | 3000h | 4000h |
.rsrc | B000h |
其中,.rsrc 是需要在稍后插入的资源 section,将在稍后讲解。
这里需要特别注意的是,.data 的虚拟内存尺寸,必须要比文件尺寸(RawDataSize)更大一些,关于这一点我还暂时不能给出详细的解释,有待于在将来做进一步研究。如果把 .data 的 VirtualSize 设置为和 RawDataSize 一样大(3000h),则程序无法运行,会弹出一个消息框提示这不是一个有效的 Win32 程序。所以这一步我也是反复尝试是否是其他字段的问题,纠结了半天才发现原来问题卡在这个地方。
对于 PE 文件头的 IMAGE_OPTINAL_HEADER.CheckSum,Windows 看起来完全忽略这个字段的值,所以这个字段可以不用管。
明确了以上问题,现在可以把这三个 section 和文件头链接成一个新的 PE 文件了,把样本程序 pediy02.exe 和三个 section 文件放在同一个目录下,通过一个辅助的 Console 项目(pediy02_helper 项目)来完成这些工作,生成的新的 PE 文件名为 pediy02_new.exe,使用的辅助函数如下(为了简单明了起见,代码中并没有插入繁琐的检测性代码,例如申请的缓冲区大小,已经根据需要,在编码时被静态的确定了):
Code 1.1 将三个 Section 拼接成 PE 文件的 C++ 代码:
void WriteToFile(FILE *fp, void* pBuf, DWORD nSize); int CreateNewPe() { //PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL; PIMAGE_DOS_HEADER pDosHdr = NULL; PIMAGE_NT_HEADERS pNtHdrs = NULL; PIMAGE_SECTION_HEADER pSectionHdr = NULL; FILE *fp1, *fp2, *fp3; TCHAR szPath[MAX_PATH]; LPCTSTR szNames[3] = { _T("_text"), _T("_rdata"), _T("_data") }; _stprintf_s(szPath, _T("%s\pediy02.exe"), THE_DIR); _tfopen_s(&fp1, szPath, _T("rb")); _stprintf_s(szPath, _T("%s\pediy02_new.exe"), THE_DIR); _tfopen_s(&fp2, szPath, _T("wb")); //读取文件头部 void* buf = malloc(0xD000); fread(buf, 1, 0x1000, fp1); pDosHdr = (PIMAGE_DOS_HEADER)buf; pNtHdrs = (PIMAGE_NT_HEADERS)((DWORD)buf + pDosHdr->e_lfanew); pSectionHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHdrs + sizeof(IMAGE_NT_HEADERS)); /* ---------------------------------------------- | section | addr | RawDataSize | VirtualSize | |---------+-------+--------------+-------------| | .text | 1000h | 6000h | 5B73h | | .rdata | 7000h | 1000h | 0C6Eh | | .data | 8000h | 3000h | 4000h | | .rsrc | B000h | 1000h | 1000h | ---------------------------------------------- */ pNtHdrs->FileHeader.NumberOfSections = 4; pNtHdrs->OptionalHeader.BaseOfCode = 0x1000; pNtHdrs->OptionalHeader.BaseOfData = 0x8000; //+1000h 的 .rsrc pNtHdrs->OptionalHeader.SizeOfCode = 0x6000; pNtHdrs->OptionalHeader.SizeOfImage = 0xD000; pNtHdrs->OptionalHeader.SizeOfInitializedData = 0x5000; pNtHdrs->OptionalHeader.SizeOfUninitializedData = 0; pNtHdrs->OptionalHeader.AddressOfEntryPoint = 0x1527; //入口点 //IMAGE_DIRECTORY_ENTRY_IMPORT 需要进一步调整, kernel32.dll, gdi32.dll, user32.dll 加上一个结尾 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = 0x7618; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = sizeof(IMAGE_IMPORT_DESCRIPTOR) * (3 + 1); //资源表 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = 0xC000; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = 0x011C; // IMAGE_DIRECTORY_ENTRY_DEBUG 6 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress = 0; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size = 0; // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress = 0; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].Size = 0; //IMAGE_DIRECTORY_ENTRY_IAT 12; (import address table), IMAGE_IMPORT_DESCRIPTOR.FirstTrunk 中的最小值 //IAT 地址需要在修改后找,需要进一步调整 //IAT 的地址通常就是 .rdata 的起始地址 //Size 是 FirstTrunk 中的最大地址 - IAT 起始地址) + 8; //(其中 +4 是最后一个元素占用的空间,再 +4 是一个NULL元素,表示结尾) pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress = 0x7000; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size = 0x012C; //section_headers //.text pSectionHdr[0].VirtualAddress = 0x1000; pSectionHdr[0].SizeOfRawData = 0x6000; pSectionHdr[0].PointerToRawData = 0x1000; pSectionHdr[0].Misc.VirtualSize = 0x5B73; //.rdata pSectionHdr[1].VirtualAddress = 0x7000; pSectionHdr[1].SizeOfRawData = 0x1000; pSectionHdr[1].PointerToRawData = 0x7000; pSectionHdr[1].Misc.VirtualSize = 0x1000; //.data //.data 的虚拟内存大小(VirtualSize)必须比文件中更大,否则无法启动,现在我也不知道为什么 pSectionHdr[2].VirtualAddress = 0x8000; pSectionHdr[2].SizeOfRawData = 0x3000; pSectionHdr[2].PointerToRawData = 0x8000; pSectionHdr[2].Misc.VirtualSize = 0x4000; //【重要!】必须比 SizeofRawData 大一些 //.rsrc (resource) 因为.data 比文件中大,所以.rsrc 相应的要像高地址移动 pSectionHdr[3].VirtualAddress = 0xC000; pSectionHdr[3].SizeOfRawData = 0x1000; pSectionHdr[3].PointerToRawData = 0xB000; //文件中的地址还是紧靠.data pSectionHdr[3].Misc.VirtualSize = 0x011C; //从范本文件中得到该值 fwrite(buf, 1, 0x1000, fp2); fflush(fp2); int i; DWORD dwFileSize; for(i = 0; i < 3; i++) { _stprintf_s(szPath, _T("%s\%s"), THE_DIR, szNames[i]); _tfopen_s(&fp3, szPath, _T("rb")); fseek(fp3, 0, SEEK_END); dwFileSize = ftell(fp3); fseek(fp3, 0, SEEK_SET); fread(buf, 1, dwFileSize, fp3); fclose(fp3); WriteToFile(fp2, buf, dwFileSize); } //从已有的范本复制 .rsrc 节 fseek(fp1, 0xB000, SEEK_SET); fread(buf, 1, 0x1000, fp1); WriteToFile(fp2, buf, 0x1000); fclose(fp1); fclose(fp2); free(buf); return 0; }
//写入文件,以 1KB 为单位 void WriteToFile(FILE *fp, void* pBuf, DWORD nSize) { //以1KB为基本单位,逐次写入 char* pos = (char*)pBuf; size_t BytesToWrite; while(nSize > 0) { BytesToWrite = min(nSize, 0x400); fwrite(pos, 1, BytesToWrite, fp); fflush(fp); nSize -= BytesToWrite; pos += BytesToWrite; } }
上面的函数已经是最终版本的函数,它已经完成了以下工作:
(1)确定 AddressOfEntryPoint 的地址。
(2)确定 DataDirectory[1]: ImportTable (导入表)的地址和尺寸。
(3)确定 DataDirectory[12]: Import Address Table (绑定导入函数地址表)的地址和尺寸。
(4)从样本程序 pediy02.exe 中插入资源 (.rsrc) section,并确定 DataDirectory[2]: resource Table (资源表)的地址和尺寸。
当然很显然上面的工作并不是一步到位完成的,下面简要介绍上面的工作是如何完成的:
(1)确定入口点地址:
该工作相对简单容易,先把 EntryPoint 设置为 .text (代码段)的起始地址:0x1000,然后生成文件后,加载到 IDA 中分析代码段的内容,就可以很容易的找到以下函数的地址(以下地址为 VA,即加上了 ImageBase 后的地址):
0x00401527: __tmainCRTStartup,是 PE 文件的实际入口点。
0x004011EC: WinMain,高级语言编程时的程序入口点。
0x004012D5: WndProc, 当前的窗口过程(稍后将会被子类化)
0x004059C4: sub_4059C4,基本等价于 MessageBoxA,很重要,称它为 ___crtMessageBoxA。
现在只要知道,在文件头中把入口地址设置到 __tmainCRTStartup 函数即可,文件头要求的是 RVA,因此在代码中设置入口点:
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint = 0x1527;
这样入口点地址就确定好了。
(2)确定 DataDirectory [1] 导入表的地址和大小:
这一步也相对比较简单,导入表位于 .rdata 中(位于中部)。在此之前,必须了解导入表的结构,导入表是一个由多个 IMAGE_IMPORT_DESCRIPTOR 元素组成的数组,以 NULL 元素(内容全部是 0 )标识结尾(IMAGE_IMPORT_DESCRIPTOR 的数据结构定义参见 winnt.h)。每个元素由 5 个 DWORD 组成,其中倒数第二个 DWORD 是 Name 字段(字符串指针),它的值是一个 RVA(即相对于 ImageBase 的偏移),指向了 Dll 名字(ASCII)字符串(该字符串同样位于 .rdata 中)。
导入表的示意结构如下图所示(图中展示的是两个 Thunk 数组并行情况,因此 FirstThunk 也是字符串指针的大多数情况,图中的字符串虽然位于整齐的矩形格子之内,这只是为了图形外观,应该强调的是这些字符串的长度是不固定的,长度有长有短,所以它们在空间中的分布是参差不齐的):
上图表示了 pediy2_new.exe 的实际导入表,共导入了 3 个 DLL,每个导入 DLL 是导入表中的一个元素,在这个数组中的每个元素大小为 20 Bytes,如果引用了 3 个 DLL,则这个数组一共为 (3 + 1) * 20 = 80 Bytes (最后有一个 null terminator element)。下面是单个元素 descriptor 大小:
sizeof ( IMAGE_IMPORT_DESCRIPTOR ) = sizeof ( DWORD ) * 5 = 20 Bytes;
每个元素的 OriginalFirstTrunk 和 FirstTrunk 是两个指针,指向了两个 并行的指针数组,通常情况下(即没有在链接时事先绑定)这两个数组的内容是相同的(即两个数组的所有元素的值相同),在静态 PE 文件中,都指向相同的长度不固定的函数名称字符串(或者是被导入函数的 Ordinal)。
补充说明:在没有经过事先绑定时,OriginalFirstTrunk 和 FirstTrunk 指向的数组内容在加载之前都指向 .rdata 中的一些长度不固定的 Ascii 编码的字符串,在加载时 FirstTrunk 指向的数组被系统绑定成映射到本进程的 DLL 的实际函数地址(因此该数组称为 IAT),所以这些元素称为 Trunk (意味着其身份的可变性,这些元素在加载后其身份发生了变化),因为指向的是数组头部,所以称之为 First(IMAGE_IMPORT_DESCRIPTOR.(Original)FirstTrunk 表示某个 DLL 被本模块导入的首个函数的 Trunk 的位置,后面还有更多的函数 Trunk,以 NULL 表征结束)。OriginalFirstTrunk 在加载后保持不变(所以称为 Original),所以相当于存储着导入函数名称的一份副本。在模块被加载后,可以通过 OriginalFirstTrunk 数组了解到该模块导入了哪些函数(名称),通过 FirstTrunk 数组的内容可了解到导入函数的运行时虚拟地址。导入函数的实际地址是在加载时绑定的(无法在编译时确定),编译器可能为每个 dll 函数调用生成一个很小的函数体,称为 j_XXX, 该函数体负责 jmp 到 FirstTrunk 数组中的元素给出的运行时函数地址,也可以直接调用 IAT 元素内容指向的 VA 地址。
虽然应用程序可以通过序号导入函数,并具有极高效率,但是这样会导致看不到导入函数的名字,对程序和系统的维护造成障碍。所以除非成本太高(例如 MFC 类库的导出函数过多,且面向对象的 C++ 函数名称也很长,所以 MFC 类库的函数以 Ordinal 方式被导入),按名称导入是普遍做法,显然按名称导入,需要线性搜索模块的导出函数表,这就会消耗一定的加载时间成本。为了提高程序加载时效率,应用程序可以通过 “事先 Rebase” (将程序需要导入的模块自身建议的 ImageBase 进行精心调整,从而避免在加载时重定向) 和 “事先绑定” 提高程序在客户运行环境的加载速度,系统通过时间戳判定绑定信息是否有效,如果时间戳不一致,或者发生重定向,系统则必须再次进行加载时绑定。
OriginalFirstTrunk 和 FirstTrunk 指向的这两个指针数组位于 .rdata 的不同位置,其中 FirstTrunk 指向的数组位于 .rdata 的起始位置(稍后可以看到这就是 IAT),OriginalFirstTrunk 指向的数组位于稍微靠后的位置。两个 Trunk 在 PE 文件中的值都指向相同的 IMAGE_IMPORT_BY_NAME (由 Hint 和 函数名称字符串 组成的数据结构)。IAT 所在的页面将在加载时被临时设定为可写,绑定之后再恢复为只读。有关这部分的细节请参考我的博客文章:《读取PE文件的导入表》。
关于导入表和 IAT 的在内存空间中的位置布局,请参考本文的补充讨论(2)。
了解了导入表结构,就可以很快找到导入表的位置了,首先在 .rdata 中查找 DLL 名称字符串,可以找到如下的字符串:
FA: 0x000077AC: "KERNEL32.dll"; (这里使用的是文件地址 FA,或者说是 RVA)
找到附近指向该位置的指针,即在附近的文件内容中搜索 "AC 77 00 00" 片段,可以找到文件地址:
FA: 0x00007624: AC 77 00 00
这里就是一个 IMAGE_IMPORT_DESCRIPTOR 元素,把该地址减去 3 个 DWORD 值,即得到该元素的起始地址为 0x00007618。由于导入表元素内容非常有特点,很容易就可以判断导入表的两端边界,因此可以很快确定导入表的起始地址(RVA)和 Size 如下:
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].VirtualAddress = 0x7618;
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].Size = sizeof ( IMAGE_IMPORT_DESCRIPTOR ) * 4;
(3)确定 DataDirectory [12],IAT的地址和大小:
IAT 的地址比较简单,它就是所有 DLL 的 FirstTrunk 字段的最小值,通常就是 .rdata 的起始位置(那些常量字符串位于 IAT 和 ImportTable 的后面),也就是 0x7000 (可以看到这里是从 Gdi32.dll 的导入的第一个函数 DeleteObject)。
要计算 IAT 的大小,需要遍历导入表,找到导入的所有 Dll 的 FirstTrunk 的最后一个元素的位置,同时还要考虑到结尾还需要一个 NULL 指针作为结束标志,所以:
IAT.Size = max ( 所有 DLL 的 FirstTrunk 数组元素所在的地址(RVA) ) - IAT.VirtualAddress (RVA) + 8 。
有关如何遍历导入表的更多内容,请参考我的博客文章(在此就不再详细叙述了):《读取PE文件的导入表》。
本题目中所有的 Trunk 的最大地址(RVA)是 0x7124(从 USER32.dll 导入的 DispatchMessageA),可得:
DataDirectory[12].VirualAddress = 0x7000; // RVA (Relative to ImageBase )
DataDirectory[12].Size = 0x012C;
经过以上修改,可以通过 CreateNewPe 函数,生成一个可以执行的 PE 文件了。题目的前半部分要求此时完成。接下来考虑后半部分要求,为程序添加菜单和相关的命令处理函数。
(二)添加菜单 和 处理函数。
(1)添加 .rsrc section (菜单资源)
添加资源,同样通过在样本程序中实现。在样本程序中,添加题目要求一样的资源(只保留菜单,删除所有其他种类资源,这样可以使 .rsrc 最小,仅占用 1000h 大小),然后可以从样本程序中拷贝 .rsrc 段,追加到我们已经得到的 PE 文件的尾部。同时调整 PE 文件头中的相关字段。
注意:由于 .data 节在加载到虚拟内存中时被扩大了 1000h,所以位于最后的 .rsrc 的文件地址(FA)和虚拟地址(VA)将会偏差 1000h。即:
VA = FA + 1000h;
众所周知,窗口的菜单通常是在注册窗口类时指定的。因此为了添加菜单,在 IDA 中观察 WinMain 函数的代码:
Code 2.1 由 .text 提供的 WinMain 函数的汇编代码:
.text:004011EC ; int __stdcall WinMain(int,int,int,int nCmdShow) .text:004011EC WinMain proc near ; CODE XREF: start+C9p .text:004011EC .text:004011EC WndClass = WNDCLASSA ptr -50h .text:004011EC Msg = MSG ptr -28h .text:004011EC var_C = dword ptr -0Ch .text:004011EC arg_0 = dword ptr 8 .text:004011EC nCmdShow = dword ptr 14h .text:004011EC .text:004011EC push ebp .text:004011ED mov ebp, esp .text:004011EF sub esp, 50h .text:004011F2 push ebx .text:004011F3 push esi .text:004011F4 push edi .text:004011F5 mov esi, offset aPediy_com ; "pediy.com" .text:004011FA lea edi, [ebp+var_C] .text:004011FD mov ebx, [ebp+arg_0] .text:00401200 movsd ; char var_C[] = "pediy.com"; 【重要暗示!!!】 .text:00401201 movsd .text:00401202 movsw .text:00401204 mov edi, 7F00h .text:00401209 xor esi, esi .text:0040120B push edi ; lpIconName .text:0040120C push esi ; hInstance .text:0040120D mov dword_40ABAC, ebx .text:00401213 mov [ebp+WndClass.style], 3 .text:0040121A mov [ebp+WndClass.lpfnWndProc], offset sub_406B80 .text:00401221 mov [ebp+WndClass.cbClsExtra], esi .text:00401224 mov [ebp+WndClass.cbWndExtra], esi .text:00401227 mov [ebp+WndClass.hInstance], ebx .text:0040122A call ds:LoadIconA .text:00401230 push edi ; lpCursorName .text:00401231 push esi ; hInstance .text:00401232 mov [ebp+WndClass.hIcon], eax .text:00401235 call ds:LoadCursorA .text:0040123B push esi ; int .text:0040123C mov [ebp+WndClass.hCursor], eax .text:0040123F call ds:GetStockObject .text:00401245 mov [ebp+WndClass.hbrBackground], eax .text:00401248 lea eax, [ebp+var_C] .text:0040124B mov [ebp+WndClass.lpszMenuName], eax ; lpszMenuName = var_C; .text:0040124E lea eax, [ebp+WndClass] .text:00401251 mov edi, offset aPediy_com_0 ; "pediy.com" .text:00401256 push eax ; lpWndClass .text:00401257 mov [ebp+WndClass.lpszClassName], edi .text:0040125A call ds:RegisterClassA .text:00401260 test ax, ax .text:00401263 jnz short loc_401269 .text:00401265 xor eax, eax 。。。
菜单资源可以采用数字来标识,也可以采用字符串标识。如果在 VC 中添加菜单,默认为以数字标识。如果要以数字标识菜单,第一个想法是需要 hack 上面的代码。
但上面的代码实际上不需要做任何改动,因为它给了我们一个强烈暗示,上面的汇编代码翻译到 C 语言如下:
Code 2.2 将 WinMain 从汇编代码翻译到 C++ 的代码(得到 Menu Name):
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { char var_C[] = "pediy.com"; // ---- 重要暗示!!!---- MSG msg; WNDCLASSA wndCls; //保存到全局变量 hInst = hInstance; wndCls.style = CS_HREDRAW | CS_VREDRAW; wndCls.lpfnWndProc = WndProc; wndCls.cbClsExtra = 0; wndCls.cbWndExtra = 0; wndCls.hInstance = hInst; wndCls.hIcon = LoadIconA(NULL, IDI_APPLICATION); wndCls.hCursor = LoadCursorA(NULL, IDC_ARROW); wndCls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndCls.lpszMenuName = var_C; // ---- 重要暗示!!!---- wndCls.lpszClassName = "pediy.com"; if (!RegisterClassA(&wndCls)) return FALSE; HWND hWnd = CreateWindowExA( 0, // EXStyle "pediy.com", // wndClass WS_BORDER | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP | WS_TABSTOP,
//style CW_USEDEFAULT, // X CW_USEDEFAULT, // Y CW_USEDEFAULT, // nWidth CW_USEDEFAULT, // nHeight NULL, // hWndParent NULL, // hMenu hInst, // hInstance 0); // lParam ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessageA(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; }
就是窗口类的菜单是由 var_C 指定的,var_C 是栈上的临时变量,内容被加载为”pediy.com“。即菜单的字符串标识是 ”pediy.com“。所以任务就简单了,在样本程序中,把菜单的 ID 改为字符串”pediy.com“,然后把编译好的样本程序的 .rsrc 追加到 PE 文件中,菜单就加好了!
补充说明:资源 ID 以字符串标识时,是不论大小写,且字符串的大写形式,以 Unicode 编码存储于 .rsrc 段中的。例如本题目,菜单的 ID 在 .rsrc 中被存储为 "PEDIY.COM"。
有关资源表的结构的更多信息,请参考我的博客文章(这里不做更多说明):《读取PE文件的资源表》。
(2)添加菜单处理函数(子类化窗口):
菜单加好以后,现在点击菜单还没有任何反应。接下来为菜单添加命令处理函数,因此观察窗口过程 WndProc 的汇编代码,可以发现:WndProc 没有为 WM_COMMAND 留出任何空隙和空间供我们插入自己的代码,即没有办法 hack 已有代码来完成这个功能。因此只能在 .text 中追加新的代码。
方法是,在 .text 尾部追加一个函数作为新的窗口过程,这个过程和在 MFC 中子类化一个控件的本质相同,也类似于通常所说的 Hook,即挂钩一个新的函数,由新的 Hook 函数添加自己的处理逻辑,然后再把控制权交回到原来的函数。
这里还需要说明另一个问题,题目要求点击菜单时弹出 MessageBox。但是在现有的导入表中可以看到,程序并没有导入 MessageBoxA 这个函数,所以如果直接调用 MessageBoxA,则需要调整导入表。这样相对的比较麻烦。这时候前面我们找到的那个非常有趣的函数(sub_4059C4: ___crtMessageBoxA)就有用了,观察那个函数,其汇编代码如下:
Code 2.3 代码段中的函数: 004059C4: ___crtMessageBoxA 的汇编代码(MessageBoxA 的动态链接版本):
.text:004059C4 sub_4059C4 proc near ; .text:004059C4 arg_0 = dword ptr 8 .text:004059C4 arg_4 = dword ptr 0Ch .text:004059C4 .text:004059C4 push ebx .text:004059C5 xor ebx, ebx .text:004059C7 cmp dword_40AB70, ebx .text:004059CD push esi .text:004059CE push edi .text:004059CF jnz short loc_405A13 .text:004059D1 push offset LibFileName ; "user32.dll" .text:004059D6 call ds:LoadLibraryA .text:004059DC mov edi, eax .text:004059DE cmp edi, ebx .text:004059E0 jz short loc_405A49 .text:004059E2 mov esi, ds:GetProcAddress .text:004059E8 push offset aMessageboxa ; "MessageBoxA" .text:004059ED push edi ; hModule .text:004059EE call esi ; GetProcAddress .text:004059F0 test eax, eax .text:004059F2 mov dword_40AB70, eax .text:004059F7 jz short loc_405A49 .text:004059F9 push offset aGetactivewindo ; "GetActiveWindow" .text:004059FE push edi ; hModule .text:004059FF call esi ; GetProcAddress .text:00405A01 push offset aGetlastactivep ; "GetLastActivePopup" .text:00405A06 push edi ; hModule .text:00405A07 mov dword_40AB74, eax .text:00405A0C call esi ; GetProcAddress .text:00405A0E mov dword_40AB78, eax .text:00405A13 .text:00405A13 loc_405A13: ; CODE XREF: sub_4059C4+Bj .text:00405A13 mov eax, dword_40AB74 .text:00405A18 test eax, eax .text:00405A1A jz short loc_405A32 .text:00405A1C call eax .text:00405A1E mov ebx, eax .text:00405A20 test ebx, ebx .text:00405A22 jz short loc_405A32 .text:00405A24 mov eax, dword_40AB78 .text:00405A29 test eax, eax .text:00405A2B jz short loc_405A32 .text:00405A2D push ebx .text:00405A2E call eax .text:00405A30 mov ebx, eax .text:00405A32 .text:00405A32 loc_405A32: ; CODE XREF: sub_4059C4+56j .text:00405A32 ; sub_4059C4+5Ej ... .text:00405A32 push [esp+0Ch+arg_4] .text:00405A36 push [esp+10h+arg_0] .text:00405A3A push dword ptr [esp+18h] .text:00405A3E push ebx .text:00405A3F call dword_40AB70 .text:00405A45 .text:00405A45 loc_405A45: ; CODE XREF: sub_4059C4+87j .text:00405A45 pop edi .text:00405A46 pop esi .text:00405A47 pop ebx .text:00405A48 retn .text:00405A49 ; .text:00405A49 .text:00405A49 loc_405A49: ; CODE XREF: sub_4059C4+1Cj .text:00405A49 ; sub_4059C4+33j .text:00405A49 xor eax, eax .text:00405A4B jmp short loc_405A45 .text:00405A4B sub_4059C4 endp
这个函数内容非常简单,内容注释就不写了,总之,这个函数的功能是动态获取 MessageBoxA 的地址并调用。原型相当于:
int ___crtMessageBoxA(const char* pText, const char* pTitle, UINT nType);
由于函数没有复原 ESP,所以是默认的 C 调用约定。这个函数和 MessageBoxA 的区别是:
(a)调用约定不同,MessageBoxA 为 __stdcall 。
(b)只比 MessageBoxA 少了第一个参数: HWND hWnd。该函数在内部获取了一个 HWND 作为 Owner 窗口弹出 MessageBox。
因此,不需要调整导入表,只需要在新的窗口过程中去调用这个函数即可完成弹出 MessageBox 的功能。
同时可以看到,MessageBox 的文本内容,在 .rdata 中并没有可供使用的现成字符串,所以需要插入常量字符串,只需要在 .rdata 的尾部插入即可,通过以下函数即可完成(注意插入新的常量字符串后,需要相应的调整 IMAGE_SECTION_HEADER.VirtualSize,以容纳新的字符串内容):
Code 2.4 向 .rdata 段尾部插入常量字符串的 C++ 代码(作为 ___crtMessageBoxA 的参数):
//在PE文件中插入常量字符串 void InsertString(LPCTSTR pFileName, int InsertPos, const char *pStr) { int BytesToWrite = strlen(pStr) + 1; FILE *fp = NULL; _tfopen_s(&fp, pFileName, _T("r+b")); fseek(fp, InsertPos, SEEK_SET); fwrite(pStr, 1, BytesToWrite, fp); fclose(fp); }
上面的 InsertString 是为了修改 PE 文件而临时写成,所以比较简单,因此其不够易用,局限性在于,1 需要手工调整 section header; 2 需要手工调整 section header 里的 VirtualSize; 3 只考虑了 ASCII 字符串。因此可以多花一定时间把它写的更加通用一点。参见本文末尾的补充部分。
为了得到新的窗口过程,在样本程序中写出新的窗口过程函数,并以 debug 选项编译(之所以采用 debug,是因为 release 优化幅度过大,其结果不利于我们利用。例如我在 release 下编译出挂钩后的结果,编译结果显示原有的窗口过程的第一个参数 hWnd 被优化掉了,因为它已经不再作为窗口过程使用,而仅仅是被新的窗口过程调用的一个普通函数,所以编译器可以按照自己的喜好对它做任何等效变换!)。
Code 2.5 为了子类化窗口,新窗口过程的 C++ 代码(用于得到其汇编代码):
BOOL ___crtMessageBoxA(const char* szText, const char* szTitle, UINT nType) { HMODULE hModule = LoadLibraryA("user32.dll"); int (__stdcall *pFunc)(HWND, LPCSTR, LPCSTR, UINT uType); pFunc = (int (__stdcall*)(HWND, LPCSTR, LPCSTR,UINT uType))
GetProcAddress(hModule, "MessageBoxA"); pFunc(NULL, szText, szTitle, nType); return TRUE; } LRESULT CALLBACK NewWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if(message == WM_COMMAND && LOWORD(wParam) == IDM_ABOUT) { ___crtMessageBoxA( "看雪论坛.珠海金山2007逆向分析挑战赛 http://www.pediy.com", //text "pediy", //caption MB_ICONINFORMATION); return TRUE; } return WndProc(hWnd, message, wParam, lParam); }
其中上面代码中的 ___crtMessageBoxA 函数只是对实际函数的一个简单模拟,这样产生的窗口过程的代码,只需要计算出一些偏移值即可。接下来反汇编上面的样本代码的 debug 编译结果,把 debug 版本中做简要处理,去掉 debug 版本特有的那些填充 INT3 和 ESP 校验 那些没什么用处的代码,就可以得到需要插入的汇编代码了,通过以下函数,把新的窗口过程代码插入到 PE 文件中(由于段在内存中对齐到 4KB,所以每个段的结尾基本上都有相当大的空间剩余,可以插入一些新的内容),如下所示:
Code 2.6 用于向 .text 尾部插入新的窗口过程的 C++ 代码(用于窗口子类化):
//返回插入的字节数 void InsertNewWndProc(LPCTSTR pFileName, int InsertPos) {
//int __stdcall NewWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
//[EBP+ 8]: hWnd //[EBP+0Ch]: nMsg //[EBP+10h]: wParam //[EBP+14h]: lParam
BYTE _code[] = { 0x55, //00: push EBP 0x8B, 0xEC, //01: mov EBP, ESP 0x81, 0xEC, 0x20, 0x00, 0x00, 0x00, //03: sub ESP, 20H 0x53, //09: push EBX 0x56, //0A: push ESI 0x57, //0B: push EDI 0x81, 0x7D, 0x0C, 0x11, 0x01, 0x00, 0x00, //0C: cmp [EBP + nMsg], WM_COMMAND 0x75, 0x2B, //13: jne _CALL_OLD_WNDPROC 0x8B, 0x45, 0x10, //15: mov EAX, [EBP + wParam] 0x25, 0xFF, 0xFF, 0x00, 0x00, //18: and EAX, 0xFFFF 0x0F, 0xB7, 0xC8, //1D: movzx ECX, AX 0x83, 0xF9, 0x68, //20: cmp ECX, 0x68 (IDM_ABOUT = 104) 0x75, 0x1B, //23: jne _CALL_OLD_WNDPROC 0x6A, 0x40, //25: push MB_ICONINFORMATION 0x68, 0x90, 0x7C, 0x40, 0x00, //27: push pTitle (0x00407C90: "pediy") 0x68, 0xA0, 0x7C, 0x40, 0x00, //2C: push pText (0x00407CA0 : "...") 0xE8, 0x00, 0x00, 0x00, 0x00, //31: call ___crtMessageBoxA (rel32,需要调整) 0x83, 0xC4, 0x0C, //36: add ESP, 0Ch 调用方复原esp 0xB8, 0x01, 0x00, 0x00, 0x00, //39: mov EAX, 1 0xEB, 0x15, //3E: jmp _RETURN //_CALL_OLD_WNDPROC: 0x8B, 0x45, 0x14, //40: mov EAX, [EBP + lParam] 0x50, //43: push EAX 0x8B, 0x4D, 0x10, //44: mov ECX, [EBP + wParam] 0x51, //47: push ECX 0x8B, 0x55, 0x0C, //48: mov EDX, [EBP + nMsg] 0x52, //4B: push EDX 0x8B, 0x45, 0x08, //4C: mov EAX, [EBP + hWnd] 0x50, //4F: push EAX 0xE8, 0x00, 0x00, 0x00, 0x00, //50: call oldWndProc (rel32,需要调整) //_RETURN: 0x5F, //55: pop EDI 0x5E, //56: pop ESI 0x5B, //57: pos EBX 0x81, 0xC4, 0x20, 0x00, 0x00, 0x00, //58: add ESP, 20h 0x5D, //5E: pop EBP, 0xC2, 0x10, 0x00 //5F: retn 10h }; union { int offset; UINT dwVal; BYTE bytes[4]; } rel32; //计算 ___crtMessageBoxA 的偏移地址 int nextAddr = InsertPos + 0x36; //注意nextAddr是文件地址,也就是 rva (没有加ImageBase) //0x59C4 是 showMsgBox 函数的 rva rel32.offset = 0x59C4 - nextAddr; _code[0x32] = rel32.bytes[0]; _code[0x33] = rel32.bytes[1]; _code[0x34] = rel32.bytes[2]; _code[0x35] = rel32.bytes[3]; //计算 oldWndProc 的偏移地址 nextAddr = InsertPos + 0x55; //0x12D5 是 WndProc 函数的rva rel32.offset = 0x12D5 - nextAddr; _code[0x51] = rel32.bytes[0]; _code[0x52] = rel32.bytes[1]; _code[0x53] = rel32.bytes[2]; _code[0x54] = rel32.bytes[3]; int BytesToWrite = sizeof(_code); FILE *fp = NULL; _tfopen_s(&fp, pFileName, _T("r+b")); fseek(fp, InsertPos, SEEK_SET); fwrite(_code, 1, BytesToWrite, fp); fclose(fp); }
在上面的代码中,_code 数组的内容是根据 NewWndProc 的 debug 版本的汇编代码的基础上,经过删减得到的,已经增加了注释。在所有相关的调整步骤完成后,可以再次反汇编目标文件,查看新插入的窗口过程是否正常,由于上面对 _code 内容的注释将和反汇编工具中看到的一样,所以这里就不再重复给出在反汇编工具中看到的“新的窗口过程”的代码了。
【注意】:插入新的函数到 .text 尾部后,可能依然需要手工更新 section header 中的 VirtualSize 。
代码中由两处偏移地址需要进行调整,分别是 ___crtMessageBoxA 和 oldWndProc 的偏移地址。showMsgBox 的前两个参数为新插入到 .rdata 尾部的两个常量字符串,其地址(VA)已经直接编入 _code 数组中了。即,通过以下方式完成插入新的窗口过程:
Code 2.7 插入 “常量字符串” 和 “新的窗口过程” 到 PE 文件的执行动作:
//[2] 向修改后的PE文件中插入常量字符串 InsertString(szPath, 0x7C80, "---OurString---"); InsertString(szPath, 0x7C90, "pediy"); InsertString(szPath, 0x7CA0, "看雪论坛.珠海金山2007逆向分析挑战赛 http://www.pediy.com"); //[3] 插入新的窗口过程!相当于对其子类化 InsertNewWndProc(szPath, 0x6B80);
先在尾部插入一个没用的但容易识别的分隔字符串(其目的是帮助我们在 16 进制编辑器中快速定位到插入的内容):”---OurString---"。(恰好16Bytes,且 InsertString 函数对插入地址做了 16 Bytes 对齐,因此它在16进制编辑器中将占据一个整行),接下来插入两个常量字符串(作为题目要求弹出的 MessageBox 的标题和文本):
0x7C90: "pediy" // Title of MsgBox; ( 这里采用的是 “文件地址” 或者说 RVA。)
0x7CA0: "看雪论坛..珠海金山2007..." // Text of MsgBox;
注意:插入新的字符串常量后,不要忘记同步调整 .rdata 的 VirtualSize !
本文结尾补充了一个更通用的插入字符串的函数。请参考补充讨论。
新的窗口过程已经被插入到了 PE 文件中。接下来再修改 WinMain 中注册窗口类的代码,把新的窗口过程挂钩上去。窗口类的窗口过程是用 VA 提供的绝对地址,修改起来很简单,不需要计算偏移值,把对应的 VA 修改为我们插入的新的窗口过程的 VA (0x00406B80)即可。
同样的,找到 WinMain 函数中,设置窗口过程的指令:
.text:0040121A mov [ebp+WndClass.lpfnWndProc], offset OldWndProc
指令的机器码:
FA:0000121A: C7 45 B4 XX XX 40 00
来到文件地址 121A h 处,这条指令的后面 4 个字节就是窗口过程的 VA。把它修改为刚刚插入的新的窗口过程的 VA (0x 00406B80) 即可。即把 XX 位置调整为如下,即完成挂钩我们新插入的窗口过程:
FA:0000121A: C7 45 B4 80 6B 40 00
这样题目的三部分要求(文本将后两个要求合并)就全部完成了。修改后的 PE 文件运行效果如下:
【补充】对该条指令 ( .text:0040121A mov [ebp+WndClass.lpfnWndProc], offset OldWndProc ) 的机器码解读:
Prefixes | Opcode | ModR/M | SIB | Displacement | Immediate | ||||||||
Mod | Reg/Opcode | R/M | Scale | Index | Base | ||||||||
B | 11000111 | 01 | 000 | 101 | 10110100 | 略 | |||||||
H | <absent> | C7 | 45 | <absent> | B4 | 80 | 6B | 40 | 00 | ||||
+disp8 | <无意义> | [EBP] | |||||||||||
说 明 |
MOV | [EBP] + disp8 |
disp8 = -76 |
imm32 | |||||||||
[EBP - 4Ch], | 0x00406B80 | ||||||||||||
Dest Operand, | Src Operand | ||||||||||||
r/m32, | imm32 | ||||||||||||
&WndCls = EBP - 0x50; //描述窗口类的数据结构的地址 Offset of WndCls.lpfnWndProc = 4; //结构体成员偏移 因此: EBP - 0x4C => &WndCls + 4 => &WndCls.lpfnWndProc; 翻译到高级语言: WndCls.lpfnWndProc = 0x00406B80; |
imm32 立即数: 窗口过程的入口地址; VA (已包含 ImageBase); |
||||||||||||
对此 Opcode (C7)的特定说明(属于比较晦涩繁琐的细节,可忽略本单元格内容): Move imm32 to r/m32 (或 Move imm16 to r/m16). 寻址: Operand1 (destination operand): ModRM: r/m (w); Operand2 (source operand): imm8/16/32/64; |
在参考资料(5)中,C7 操作码的说明是“C7 /0”; 这里 “/0” 表示 ModR/M 字节仅仅使用 r/m (寄存机或主存)操作数。
ModR/M 字节的各个字段含义解释如下:
a). r/m = 101 (二进制), 表示 CH / BP / EBP / MM5 / XMM5 寄存器。
b). Mod = 01 (二进制),表示由 r/m 字段寻址的寄存器 + disp8 。也就是 ModR/M 字节后面将出现一个字节的 Displacement, 作为对此寄存器值的偏移量。(此字节被有符号扩展到寄存器数据尺寸后,作为对寄存器的值的修正。因此,这里 disp8 = B4h = -4C h;
c). Reg/Opcode = 000 (二进制),或者指定一个寄存器号,或者作为操作码的扩展信息,具体用途由主操作码指定。在该指令中此字段没有实际意义。在 OpCode = 7C 时,看起来我们只需要关心 R/M 的值(选择下表所在的某一行),在行内横向移动时改变的是 Reg/Opcode 字段的值,看起来似乎是无关紧要的。但实际证明,CPU 要求这个字节只能取第一列的值(也就是该字段必须为 0 )。下表为来自参考资料(5)(Intel 文档)中的 ModR/M 字节寻址表。在本指令(Opcode = C7)
中,ModR/M 只能在第一列(图中红色方框内的数据,即寻址寄存器为 AL/AX/EAX/MM0/XMM0 )中取值,如果在其他列取值将会引发运行时异常(参见如下实验)。
我做了一个实验,当改变 ModR/M 字节的值(另其在行内横向移动到第二列),例如将 0x0040121A 处的指令改为 C7 4D B4 XX XX 40 00 时,在 IDA 中可以正常解析出和修改前一样的指令, 但是运行时会提示异常,用 VS2005 调试,显示其反汇编代码,也会出现指令解释错误,如下图所示:
可以看到在 VS 反汇编器中 0040121A 处指令(原指令为 7 Bytes)无法识别,和后面三个字节(.text:00401221 mov [ebp+WndClass.cbClsExtra], esi)混淆在一起,无法正确识别原有指令(上图中红色方框中的部分),直到 00401224 处,才恢复成正常解释。
SIB 字节:
主要由 base + index 和 scale + index 寻址模式需要使用。scale 字段指定缩放因子,index 字段指定索引寄存器号。base 字段指定作为基址的寄存器号。
【一些有趣的补充】
(1)可以发现一个有趣的现象,在 EXE 类型的 Windows 程序中,传递给 WinMain 的第一个参数 hInstance 是一个 hardcode 的常数:0x0040 0000。也就是说,由于 EXE 是进程的第一个被加载的 Module,并且 linker 对 EXE 的默认 ImageBase 是 0x0040 0000,所以 EXE 自身的 Module 总是位于进程空间的 0x0040 0000 位置。
(2)在资源表中的字符串是以 Unicode 编码存储的,而导入表中的字符串,是以 ASCII 编码存储的。两者分别采用了两种编码,这意味着程序要读取 PE 文件的这两个表,肯定要做编码转换。为什么会这样的?大概原因可能是:
导入表的字符串都是 DLL 和 函数的名称,很明显它们都可以也应该以 ASCII 编码,也就是说,DLL 和 函数名称一律都是英文的(字母+数字),至今我没有听说过有谁用自己国家民族的特殊语言字符来为 DLL 和函数命名,所以导入表中的字符串都是 ASCII 编码,这对于存储和网络传输来说比较经济(我们知道,Windows 系统从 NT 开始内部已经统一采用 Unicode 字符串,在这种环境下,采用 Unicode 编码的程序比采用多字节编码的程序的运行效率更高,关于这一点 Matt Pietrek 在他的专栏曾经写过文章比较这两种编码之间的性能差异,所以在现在所处的时代应该优先采用 Unicode 编码,尽管 ASCII 编码的 C-Style / STL 字符串更为人们熟悉和惯用,但早就是时候改变习惯了)。
而资源就不一样了,资源可以由字符串来标识,完全可以用个性化的语言文字来定义,比如说用户把菜单名字取名为“我的上下文菜单”这样的名称,是完全可能也被允许的,所以资源表中的字符串一律采用 Unicode 编码。
(3)由于我的笔记本安装的是 Win7 / 64-bit 版本操作系统,所以在 IDA 中调试时居然是 64 位模式,有一些不适应。
【下载链接】本题目的附件,和文本中提到的代码的下载链接:
http://files.cnblogs.com/hoodlum1980/pediy02_Answer.zip
【参考资料】
[1]. hoodlum1980 (myself),读取文件的导入表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/08/1821778.html。
[2]. hoodlum1980 (myself),读取文件的资源表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/10/1822906.html。
[3]. hoodlum1980 (myself),[VC6] 图像文件格式数据查看器,http://www.cnblogs.com/hoodlum1980/archive/2010/09/05/1818308.html。
[4]. Billy Belceb,《病毒编写教程---Win32篇》,“PE文件头”章节,翻译:onlyu。
来自:看雪论坛精华6 病毒木马技术 病毒编写 Billy Belceb 病毒教程Win32篇。
[5]. Intel® 64 and IA-32 Architectures Software Developer’s Manual,Volume 2 (2A, 2B & 2C): "Instruction Set Reference, A-Z",
--> CHAPTER 2. INSTRUCTION FORMAT
2.1 INSTRUCTION FORMAT FOR PROTECTED MODE, REAL-ADDRESS MODE, AND VIRTUAL-8086 MODE
2.1.3 ModR/M and SIB Bytes;
--> CHAPTER 3. INSTRUCTION SET REFERENCE, A-L MOV-Move;
--> APPENDIX B. INSTRUCTION FORMATS AND ENCODINGS;
【本文维护历史】:
[1]. 重新制作本文中的插图:图 1 和图 2,使其更加美观,内容更加准确。2014-6。
[2]. 修订对机器码解读表格中的部分说明。2014-6-27。
另:本文中的插图(图1,图2),采用 Office 2007 - Excel 制作基础资料,在 Photoshop CS 中进一步加工得到。
【补充讨论】
讨论 1. 一个更通用一点的向 PE 文件插入常量字符串的函数。
文中使用的向 PE 插入字符串的函数过于简单,其目前主要局限在于:
(1)需要给出插入位置的文件地址。(人工计算得出)
(2)需要调整插入字符串后,受影响的 section header 中的 VirualSize 字段的值。
(3)仅仅考虑了 ASCII 字符串。
因此,我完全可以把这个函数做的更加简单易用一些,但依然建立在以下假设条件下:
(1)文件具有一个只读的 section; 且该 section 尾部有足够的空间容纳要插入的字符串。
增强易用性的函数的优点是,仅仅需要给出待修改的 PE 文件的路径,要插入的字符串,要写入的字节数就可以了,文件同时向调用方返回以下信息:只读 section 的名称,该字符串的文件地址,相对地址(RVA,不含 ImageBase)。
函数代码如下(目前并没有设置 ErrorMsg 的值,所以在目前版本中该参数目前仅占位):
//.text 的 section.Characters #define INCLUDE_TEXT (IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE) #define EXCLUDE_TEXT (IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_DISCARDABLE) //.rdata 的 section.Characters; #define INCLUDE_RDATA IMAGE_SCN_MEM_READ #define EXCLUDE_RDATA (IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_DISCARDABLE | IMAGE_SCN_CNT_CODE) BOOL InsertStringEx(LPCTSTR pFileName, //[in]要修改的 PE 文件路径 LPVOID pStr, //[in]要插入的字符串 int nBytesToWrite, //[in]要写入的字节数(包括 null terminator) DWORD dwInclude, //[in]节属性中应该包含的属性 DWORD dwExclude, //[in]节属性中不应该包含的属性 LPTSTR pSectionName, //[out] 输出插入到了哪个section中, 要求至少为 9 chars LPDWORD pRVA, //[out]返回插入后字符串的RVA LPDWORD pFA, //[out] 返回插入的文件地址 LPTSTR pErrorMsg, //[out] 错误信息 UINT nBufSize); //更加智能的插入常量字符串,自动调整 SectionHeader.VirtualSize //假设 .rdata 段尾部具有足够的空间 BOOL InsertStringEx(LPCTSTR pFileName, //[in]要修改的PE文件路径 LPVOID pStr, //[in]要插入的字符串 int nBytesToWrite, //[in]要写入的字节数(包括 null terminator) DWORD dwInclude, //[in]节属性中应该包含的属性 DWORD dwExclude, //[in]节属性中不应该包含的属性 LPTSTR pSectionName, //[out] 输出插入到了哪个section中, 要求至少为 9 chars LPDWORD pRVA, //[out]返回插入后字符串的RVA LPDWORD pFA, //[out] 返回插入的文件地址 LPTSTR pErrorMsg, //[out] 错误信息 UINT nBufSize) { IMAGE_DOS_HEADER DosHdr; IMAGE_NT_HEADERS NtHdrs; PIMAGE_SECTION_HEADER pSectionHdrs = NULL; BOOL bRet = FALSE; int InsertPos; //需要计算 FILE *fp = NULL; errno_t nErr = _tfopen_s(&fp, pFileName, _T("r+b")); if(nErr != 0 || fp == NULL) goto _CLEANUP; fread(&DosHdr, 1, sizeof(IMAGE_DOS_HEADER), fp); fseek(fp, DosHdr.e_lfanew, SEEK_SET); fread(&NtHdrs, 1, sizeof(IMAGE_NT_HEADERS), fp); pSectionHdrs = (PIMAGE_SECTION_HEADER)malloc( sizeof(IMAGE_SECTION_HEADER) * NtHdrs.FileHeader.NumberOfSections); if(pSectionHdrs == NULL) goto _CLEANUP; fread(pSectionHdrs, sizeof(IMAGE_SECTION_HEADER), NtHdrs.FileHeader.NumberOfSections, fp); //找到只读的section int i, iSection = -1; DWORD dwChar; //section 属性 for(i = 0; i < NtHdrs.FileHeader.NumberOfSections; i++) { dwChar = pSectionHdrs[i].Characteristics; if((dwChar & dwInclude) == dwInclude && (dwChar & dwExclude) == 0) { iSection = i; break; } } //没找到符合要求的section? if(iSection < 0) goto _CLEANUP; //计算section的插入地址 PIMAGE_SECTION_HEADER p1 = pSectionHdrs + iSection; if(pSectionName != NULL) { for(i = 0; i < 8; i++) pSectionName[i] = p1->Name[i]; pSectionName[i] = 0; } //计算当前的下一个section的地址 DWORD nNextAddr0 = GetAligned(p1->PointerToRawData + p1->Misc.VirtualSize, NtHdrs.OptionalHeader.SectionAlignment); //把它对齐到 16 bytes InsertPos = p1->PointerToRawData + p1->Misc.VirtualSize; InsertPos = GetAligned(InsertPos, 0x10); //判断是否有足够插入空间 DWORD nNewSectionSize = InsertPos + nBytesToWrite - p1->PointerToRawData; DWORD nNextAddr1 = GetAligned(p1->PointerToRawData + nNewSectionSize, NtHdrs.OptionalHeader.SectionAlignment); if(nNextAddr1 > nNextAddr0) goto _CLEANUP; //设置两种地址 if(pFA != NULL) *pFA = InsertPos; if(pRVA != NULL) *pRVA = p1->VirtualAddress + (InsertPos - p1->PointerToRawData); //修改section hdr里的值 fseek(fp, DosHdr.e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * iSection + 8, //sizeof(IMAGE_SECTION_HEADER.Name) SEEK_SET); fwrite(&nNewSectionSize, sizeof(DWORD), 1, fp); //插入字符串 fseek(fp, InsertPos, SEEK_SET); fwrite(pStr, 1, nBytesToWrite, fp); bRet = TRUE; _CLEANUP: if(fp != NULL) fclose(fp); if(pSectionHdrs != NULL) free(pSectionHdrs); return bRet; }
其中,代码中忘了附上 GetAligned 函数,其函数内容可能是:
UINT GetAligned(UINT nVal, UINT nAlignUnit)
{
return (nVal + nAlignUnit - 1) / nAlignUnit * nAlignUnit;
}
可以看到,增强版本函数去除了之前的三个局限。使用起来更加方便(只需要提供 PE 的路径和要插入的字符串内容就可以了),完全不再需要关心那些琐碎细节。例如:
//要追加字符串的 PE 文件路径 TCHAR szExePath[MAX_PATH]; _tcscpy_s(szExePath, MAX_PATH, _T("E:\pediy02_Test.exe")); DWORD dwRVA, dwFA; TCHAR szSectionName[16]; char ascii_str[256]; strcpy_s(ascii_str, _ARRAYSIZE(ascii_str), "this is a MultiByte ascii string."); InsertStringEx(szExePath, ascii_str, (strlen(ascii_str) + 1) * sizeof(char),
INCLUDE_RDATA, EXCLUDE_RDATA, szSectionName, &dwRVA, &dwFA, NULL, 0); wchar_t unicode_str[256]; wcscpy_s(unicode_str, _ARRAYSIZE(unicode_str), L"that is a WideChar unicode string."); InsertStringEx(szExePath, unicode_str, (wcslen(unicode_str) + 1) * sizeof(wchar_t),
INCLUDE_RDATA, EXCLUDE_RDATA, szSectionName, &dwRVA, &dwFA, NULL, 0);
调用了该函数成功后,PE文件就已经就绪了,不再需要做其他调整。只需要手工记录下来函数返回的 RVA 地址即可,它可以用于替换掉 .code 中的常量字符串的地址,例如替换 MessageBox 的参数,就可以使得弹出的消息框显示新的内容/标题。FA (文件地址)仅仅用于确定在 16 进制编辑器中观察插入的字符串是否正常和正确。
讨论2. 导入表和 IAT 在内存中的布局。
在本文的图 1 给出了导入表的指针结构,但我希望对这些元素在内存空间(文件)中的布局和位置有一个更直观的认识,因此我写了下面这个程序,来输出位于 .rdata section 起始位置的导入表的所有元素。程序读取所有的 Import Table Descriptors, Thunks, Ascii Strings, 根据这些元素的地址进行排序,以此复现他们在内存空间中的位置/出现次序。采用的 PE 文件即为我给出的题目答案为样例。完整的程序代码如下:
// ImportTable.cpp // 打印出一个 PE 文件的导入表的布局分布图(在 .rdata 的头部的位置) // #include "stdafx.h" #include <stdlib.h> #include <windows.h> #include <vector> #include <algorithm> #include <functional> //#include <stdarg.h> using namespace std; enum TypesDef { T_Descriptor = 0, //descriptor; T_Thunk = 1, //trunk T_String = 2, //常量字符串 }; typedef struct tagNODE { int RVA; //RVA int RVA_End; //在自己身后的RVA(不包含在本元素中) int FA; //文件地址 int type; char name[256]; } NODE, *LPNODE; int MyComparer(const void *pA, const void *pB); bool IsSuccessive(NODE a, NODE b); DWORD RVAToFA(DWORD rva, PIMAGE_SECTION_HEADER pSectionHdrs, int NumberOfSections); int ReadAsciiString(FILE* fp, char *pBuf); int ReadInt32(FILE* fp); int ReadInt16(FILE* fp); int _tmain(int argc, _TCHAR* argv[]) { //先遍历PE文件,找出需要多少个NODE节点 BOOL bPrintGap = TRUE; vector<NODE> nodes; vector<NODE>::const_iterator pos; NODE node; TCHAR szPath[MAX_PATH]; _tcscpy_s(szPath, MAX_PATH, _T("E:\pediy02_new.exe")); IMAGE_DOS_HEADER DosHdr; IMAGE_NT_HEADERS NtHdrs; PIMAGE_SECTION_HEADER pSectionHdrs = NULL; FILE *fp = NULL; errno_t nErr = _tfopen_s(&fp, szPath, _T("rb")); if(nErr != 0 || fp == NULL) goto _CLEANUP; fread(&DosHdr, 1, sizeof(IMAGE_DOS_HEADER), fp); fseek(fp, DosHdr.e_lfanew, SEEK_SET); fread(&NtHdrs, 1, sizeof(IMAGE_NT_HEADERS), fp); pSectionHdrs = (PIMAGE_SECTION_HEADER)malloc( sizeof(IMAGE_SECTION_HEADER) * NtHdrs.FileHeader.NumberOfSections); if(pSectionHdrs == NULL) goto _CLEANUP; fread(pSectionHdrs, sizeof(IMAGE_SECTION_HEADER), NtHdrs.FileHeader.NumberOfSections, fp); //已经读出文件头: int RVA_import_table, RVA_thunk, RVA_name, RVA_import_by_name; int FA_import_table, FA_thunk, FA_name, FA_import_by_name; int RvaArray[2]; RVA_import_table = NtHdrs.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; FA_import_table = RVAToFA(RVA_import_table, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); char buf[256]; int nDescriptorCount = 0; //非空元素数量 int nThunkCount = 0; //非空元素数量 int i; int Hint, BytesRead; IMAGE_IMPORT_DESCRIPTOR import_descriptor, null_descriptor; IMAGE_THUNK_DATA32 thunk_data; //IMAGE_IMPORT_BY_NAME import_by_name; memset(&null_descriptor, 0, sizeof(IMAGE_IMPORT_DESCRIPTOR)); while(TRUE) { fseek(fp, FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount, SEEK_SET); fread(&import_descriptor, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, fp); if(memcmp(&import_descriptor, &null_descriptor, sizeof(IMAGE_IMPORT_DESCRIPTOR)) == 0) { node.RVA = RVA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.RVA_End = node.RVA + sizeof(IMAGE_IMPORT_DESCRIPTOR); node.FA = FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.type = T_Descriptor; _tcscpy_s(node.name, _ARRAYSIZE(node.name), _T(" 00000000 <null> ") _T(" FirstTrunk: 00000000 <null> ") _T(" OriginalFirstTrunk: 00000000 <null> ") _T(" Name: 00000000 <null>") ); nodes.push_back(node); break; } RVA_name = import_descriptor.Name; FA_name = RVAToFA(RVA_name, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); //DLL Name 字符串节点 (没有Hint,所以Hint用 “----” 表示) fseek(fp, FA_name, SEEK_SET); BytesRead = ReadAsciiString(fp, buf); node.RVA = RVA_name; node.RVA_End = node.RVA + BytesRead; node.FA = FA_name; node.type = T_String; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("---- "%s""), buf); nodes.push_back(node); //descriptor节点 node.RVA = RVA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.RVA_End = node.RVA + sizeof(IMAGE_IMPORT_DESCRIPTOR); node.FA = FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.type = T_Descriptor; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%s ") _T(" FirstTrunk: %08X ") _T(" OriginalFirstTrunk: %08X ") _T(" Name: %08X"), buf, import_descriptor.FirstThunk, import_descriptor.OriginalFirstThunk, import_descriptor.Name); nodes.push_back(node); //读取FirstTrunk & OriginaTrunk; RvaArray[0] = import_descriptor.FirstThunk; RvaArray[1] = import_descriptor.OriginalFirstThunk; for(i = 0; i < 2; i++) { RVA_thunk = RvaArray[i]; FA_thunk = RVAToFA(RVA_thunk, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); nThunkCount = 0; while(TRUE) { fseek(fp, FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount, SEEK_SET); fread(&thunk_data, sizeof(IMAGE_THUNK_DATA32), 1, fp); if(thunk_data.u1.AddressOfData == 0) { node.type = T_Thunk; node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; _tcscpy_s(node.name, _ARRAYSIZE(node.name), _T("00000000 ========[null]========")); nodes.push_back(node); break; } //按照什么方式导入? if(thunk_data.u1.AddressOfData & IMAGE_ORDINAL_FLAG32) { node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.type = T_Thunk; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("Ordinal: %ld"), (thunk_data.u1.Ordinal & 0x7FFFFFFF)); nodes.push_back(node); } else { RVA_import_by_name = thunk_data.u1.AddressOfData; FA_import_by_name = RVAToFA(RVA_import_by_name, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); fseek(fp, FA_import_by_name, SEEK_SET); Hint = ReadInt16(fp); BytesRead = ReadAsciiString(fp, buf); //字符串节点 if(i == 1) { //因为两个数组的内容一模一样,所以字符串只加一次就够了 node.RVA = RVA_import_by_name; node.RVA_End = node.RVA + sizeof(WORD) + BytesRead; node.FA = FA_import_by_name; node.type = T_String; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%04X "%s""), Hint, buf); nodes.push_back(node); } //Trunk节点 node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.type = T_Thunk; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%08X %s"), RVA_import_by_name, buf); nodes.push_back(node); } nThunkCount++; } } nDescriptorCount++; } //打印结果 sort(nodes.begin(), nodes.end(), IsSuccessive); FILE *fpLog = NULL; _tfopen_s(&fpLog, _T("D:\ImportTable_log.txt"), _T("w")); //fpLog = stdout; int PrevRVA_End = -1; i = 0; for(pos = nodes.begin(); pos != nodes.end(); ++pos) { i++; if((i & 0xFF) == 0) fflush(fpLog); //相邻两个元素之间存在空隙? if(bPrintGap && pos->type != T_String && PrevRVA_End >= 0 && pos->RVA > PrevRVA_End) { fprintf(fpLog, "------------------------------------- "); fprintf(fpLog, " GAP: 0x%08X (%ld Bytes) ", pos->RVA - PrevRVA_End, pos->RVA - PrevRVA_End); fprintf(fpLog, "------------------------------------- "); } PrevRVA_End = pos->RVA_End; fprintf(fpLog, "%08X: ", pos->RVA); switch(pos->type) { case T_Descriptor: fprintf(fpLog, "Descriptor: "); break; case T_Thunk: fprintf(fpLog, " Trunk: "); break; case T_String: break; } fprintf(fpLog, "%s ", pos->name); } fclose(fpLog); _CLEANUP: nodes.clear(); if(fp == NULL) printf("Canot open PE file. "); else fclose(fp); if(pSectionHdrs != NULL) free(pSectionHdrs); //printf("press any key to continue... "); //getchar(); return 0; } // qsort 用到的比较函数,本例中没有用到 int MyComparer(const void *pA, const void *pB) { LPNODE pNode1 = (LPNODE)pA; LPNODE pNode2 = (LPNODE)pB; return (pNode1->RVA - pNode2->RVA); } //sort 用到的函数,两个元素是已经排好序的吗? bool IsSuccessive(NODE a, NODE b) { return (a.RVA < b.RVA); } DWORD RVAToFA(DWORD rva, PIMAGE_SECTION_HEADER pSectionHdrs, int NumberOfSections) { int i, iSection = -1; //查找该Rva位于那个段中 for(i = 0; i < NumberOfSections; i++) { if(rva >= pSectionHdrs[i].VirtualAddress && (rva <= pSectionHdrs[i].VirtualAddress + pSectionHdrs[i].Misc.VirtualSize)) { //该rva位于该段 iSection = i; break; } } //未找到? if(iSection < 0) return 0; //换算 return pSectionHdrs[iSection].PointerToRawData + (rva - pSectionHdrs[iSection].VirtualAddress); } //从 PE 文件中读取一个长度不固定的 Ascii 字符串到缓冲区 //返回读取的字节数(包括了 null_terminator, 即 retval = strlen(buf) + 1;) int ReadAsciiString(FILE* fp, char *pBuf) { int i = 0; while(TRUE) { fread(pBuf + i, 1, 1, fp); if(pBuf[i] == 0) break; ++i; } return i + 1; } int ReadInt32(FILE* fp) { int val; fread(&val, sizeof(DWORD), 1, fp); return val; } int ReadInt16(FILE* fp) { WORD val; fread(&val, sizeof(WORD), 1, fp); return val; }
程序产生的输出如下(其中,地址均为 RVA,所有被 Thunk 引用的字符串前面有两个字节表示的 Hint。同时,程序中给出了相邻元素之间的空隙字节数):
//FirstThunk 即为 IAT 地址,也是 .rdata 的起始地址 7000: Trunk:78A2 DeleteObject //GDI32.dll 的 FirstThunk 7004: Trunk:79A0 GetTextExtentPoint32A 7008: Trunk:7994 BeginPath ...(此处省略若干函数) 7044: Trunk:7896 RestoreDC 7048: Trunk:0000 ========[null]======== 704C: Trunk:7BA0 RtlUnwind //KERNEL32.dll 的 FirstThunk 7050: Trunk:7BAC WriteFile 7054: Trunk:7BB8 GetCPInfo ...(此处省略若干函数) 70EC: Trunk:7C5E LCMapStringW 70F0: Trunk:0000 ========[null]======== 70F4: Trunk:7878 DefWindowProcA //USER32.dll 的 FirstThunk 70F8: Trunk:786A BeginPaint 70FC: Trunk:785E EndPaint ...(此处省略若干函数) 7124: Trunk:77BA DispatchMessageA 7128: Trunk:0000 ========[null]======== -------------------------------------
GAP: 0x04EC (1260 bytes) ------------------------------------- 7618: Descriptor: KERNEL32.dll FirstTrunk: 704C OriginalFirstTrunk: 76B4 Name: 77AC 762C: Descriptor: USER32.dll FirstTrunk: 70F4 OriginalFirstTrunk: 775C Name: 788A 7640: Descriptor: GDI32.dll FirstTrunk: 7000 OriginalFirstTrunk: 7668 Name: 79B8 7654: Descriptor: <null> FirstTrunk: <null> OriginalFirstTrunk: <null> Name: <null> 7668: Trunk:78A2 DeleteObject //GDI32.dll 的 OriginalFirstThunk 766C: Trunk:79A0 GetTextExtentPoint32A 7670: Trunk:7994 BeginPath ...(此处省略若干函数) 76AC: Trunk:7896 RestoreDC 76B0: Trunk:0000 ========[null]======== 76B4: Trunk:7BA0 RtlUnwind //KERNEL32.dll 的 OriginalFirstThunk 76B8: Trunk:7BAC WriteFile 76BC: Trunk:7BB8 GetCPInfo ...(此处省略若干函数) 7754: Trunk:7C5E LCMapStringW 7758: Trunk:0000 ========[null]======== 775C: Trunk:7878 DefWindowProcA //USER32.dll 的 OriginalFirstThunk 7760: Trunk:786A BeginPaint 7764: Trunk:785E EndPaint ...(此处省略若干函数) 778C: Trunk:77BA DispatchMessageA 7790: Trunk:0000 ========[null]======== 7794: 0302 "lstrcpyA" //以下是 ascii 字符串 77A0: 0308 "lstrlenA"
77AC: ---- "KERNEL32.dll"
77BA: 0095 "DispatchMessageA"
77CE: 0282 "TranslateMessage"
77E2: 012A "GetMessageA"
77F0: 0291 "UpdateWindow"
7800: 026A "ShowWindow"
780E: 0059 "CreateWindowExA"
7820: 01F2 "RegisterClassA"
7832: 019A "LoadCursorA"
7840: 019E "LoadIconA"
784C: 01E0 "PostQuitMessage"
785E: 00BB "EndPaint"
786A: 000C "BeginPaint"
7878: 0084 "DefWindowProcA"
788A: ---- "USER32.dll"
7896: 01B9 "RestoreDC"
78A2: 0053 "DeleteObject"
... (此处省略略干字符串)
7C5E: 01C0 "LCMapStringW"
根据以上的输出,我可以大概画出导出表在加载后的镜像所在的虚拟空间(文件空间)中的元素大概分布,如下图所示。
【注意】下图对应于 VC 编译的 Release 版本的一种典型结果,如果是 Debug 版本,则元素分布可能和下图不同。
其中,Ascii Strings 部分是长度不固定的字符串,相邻的字符串之间有可能有 1 个字节的空隙(由于这个空隙太小,对于我们能够利用起来的意义不大,所以在图中没有画出字符串之间的空隙)。最主要的空隙位于 FirstThunk 和 Import descriptors 之间,可能有超过 1 KB 的空间看起来是好像空闲的(尚有待验证确认)。每个 Thunk 是 4 Bytes(大多数情况为指向 Ascii 字符串的指针,也可能为函数序号 Ordinal,例如 MFC 类库函数均以 Ordinal 导入,如果经过事先绑定,则 FirstThunk 内容为绑定后的函数地址),由 NULL 元素标识结束。每个 Import descriptor 固定 20 Bytes,由于这个尺寸有点不伦不类,所以在 16 进制编辑器中会显示的很不整齐(每个元素占据一行外加 4 Bytes,在下图中对它们采用了很理想的对齐,在 16 进制编辑器中不存在的这样的视图)。Ascii Strings 为字符串,如果是函数名称,则字符串前面有两个字节的 Hint。
PE 文件头中的 OptionalHeader.DataDirectory[1].Address 指向第一个 Import Descriptor 所在的位置,即 Import Table 的地址,Size 为所有 descriptor 的总大小(包含最后那个 NULL 元素)。DataDirectory[12].Address 指向第一个 FirstThunk 的起始位置,也就是 Import Address Table(IAT),Size 为所有 Thunk 元素的总大小(包括所有的 NULL 元素)。这里也就是系统加载时对所有导入函数的绑定后的实际地址(VA),在代码段中将通过直接跳转或者间接 call 的方式调用导入函数,IAT 元素的地址已经被 hardcode 到代码段中(散乱分布于代码段中),这意味着要增加导入函数,就需要调整代码段中的那些 hardcode 的 IAT 元素的地址,这将是一个稍显麻烦的工作。
在图中可以看到指针对于二进制文件设计的地位和意义,图中,Import Descriptors 数组和 Thunks 数组都是“元素尺寸固定”的“长度可变”数组,由 NULL 元素标识尾部。这些数组,要求元素尺寸固定,这对于规范 loader 的工作非常重要,所以凡是长度不固定的内容,就从元素中提取到后面的离散数据区(长度不固定元素集中存放的地方),在元素中保留为一个大小固定的指针。
此外,从程序的输出结果可以看到,Thunks 数组出现的顺序,和 Import Descriptor 的顺序未必一致。例如,一个 Import Descriptor 在数组中排在后面,它的 Thunks 数组可能排在前面。但 FirstThunk 和 OriginalFirstThunk 指针数组集合(将两者看作多个指针数组组成的集合)中,这些指针数组的排列顺序将是完全一致的。
从导入表元素的布局可以看到,如果通过调整 PE 文件的内容,删除元素可能比较容易,插入函数和新的 DLL 则是一件麻烦事,因为 linker 会把数组紧凑排列,不会留下插入空隙。这也就意味着如果要插入新的元素(例如增加一个已导入 DLL 的某个函数,或者增加一个新的导入 DLL 和若干函数),必然会导致现有的 IAT 发生一定变动。也就是说,比如假设之前已经有个导入函数为 MessageBoxA,该函数实际被映射到进程空间中的 VA 被存储于地址为 0x00407010 的 IAT 元素,当插入新的元素时,这个 IAT 的地址就会发生变动,从而会影响到 .text 代码段中所有对 MessageBoxA 的调用(这些调用相当于 “hardcoding" )。所以插入新的元素意味着:(1)必然需要调整现有的 IAT,并且增加新的函数名称字符串。(2)搜索所有 .text 对受影响的导入函数的调用,并通过适当偏移来修正这些 IAT 元素的地址。
BTW: 特定的,对于本题目,如果要从 User32.dll 导入 MessageBoxA 则相对的简单,可以从其导入表元素空间分布中看出这一点。对本题目,我已经手工完成了修改导入表,使其导入 MessageBoxA 函数,并在代码段中调用它。因为不需要移动现有的 IAT,所以也不需要修正代码段中的导入函数的 VA,相对的还是比较简单的(只是一些插入字符串,移动字符串,扩充 Thunks 数组,修正数组元素的值等操作为主,例如,扩充数组时,会把紧挨在其后的一个或多个 Ascii 字符串挤到 idata 数据段的尾部去,以为新的数组元素提供空间,在这里我就不详细展示这个过程了)。
-- [1]. 2014-06-15 首次补充;
-- [2]. 2014-06-19 增加 ImportTable 元素分布示意图和说明。