首页 > 代码库 > “金山杯2007逆向分析挑战赛”第一阶段第二题
“金山杯2007逆向分析挑战赛”第一阶段第二题
注:题目来自于以下链接地址:
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++ 代码:
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 = http://www.mamicode.com/0x8000; //+1000h 的 .rsrc pNtHdrs->OptionalHeader.SizeOfCode = 0x6000; pNtHdrs->OptionalHeader.SizeOfImage = 0xD000; pNtHdrs->OptionalHeader.SizeOfInitializedData = http://www.mamicode.com/0x5000; pNtHdrs->OptionalHeader.SizeOfUninitializedData = http://www.mamicode.com/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 = http://www.mamicode.com/0x6000; pSectionHdr[0].PointerToRawData = http://www.mamicode.com/0x1000; pSectionHdr[0].Misc.VirtualSize = 0x5B73; //.rdata pSectionHdr[1].VirtualAddress = 0x7000; pSectionHdr[1].SizeOfRawData = http://www.mamicode.com/0x1000; pSectionHdr[1].PointerToRawData = http://www.mamicode.com/0x7000; pSectionHdr[1].Misc.VirtualSize = 0x1000; //.data //.data 的虚拟内存大小(VirtualSize)必须比文件中更大,否则无法启动,现在我也不知道为什么 pSectionHdr[2].VirtualAddress = 0x8000; pSectionHdr[2].SizeOfRawData = http://www.mamicode.com/0x3000; pSectionHdr[2].PointerToRawData = http://www.mamicode.com/0x8000; pSectionHdr[2].Misc.VirtualSize = 0x4000; //【重要!】必须比 SizeofRawData 大一些 //.rsrc (resource) 因为.data 比文件中大,所以.rsrc 相应的要像高地址移动 pSectionHdr[3].VirtualAddress = 0xC000; pSectionHdr[3].SizeOfRawData = http://www.mamicode.com/0x1000; pSectionHdr[3].PointerToRawData = http://www.mamicode.com/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; }
上面的函数已经是最终版本的函数,它已经完成了以下工作:
(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,很有趣,可以弹出 MessageBox,为其取名为 showMsgBox。
现在只要知道,在文件头中把入口地址设置到 __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 中)。
导入表的示意结构如下图所示:
图中导入表的第一行并不是表头,而是字段名称,即上图表示了 pediy2_new.exe 的实际导入表,共导入了 3 个 DLL,每个导入 DLL 是导入表中的一个元素,单个元素大小为:
sizeof ( IMAGE_IMPORT_DESCRIPTOR ) = sizeof ( DWORD ) * 5 = 20 Bytes;
每个元素的 OriginalFirstTrunk 和 FirstTrunk 是两个指针,指向了两个 并行的指针数组,这两个数组的内容是相同的,在静态 PE 文件中,这两个数组都指向相同的长度不固定的函数名称字符串。
OriginalFirstTrunk 和 FirstTrunk 指向的数组内容在加载之前都指向 .rdata 中的一些长度不固定的字符串,在加载时 FirstTrunk 指向的数组被系统绑定成映射到本进程的 DLL 的实际函数地址(因此该数组称为 IAT),所以这些元素称为 Trunk,因为指向的是数组头部,所以称之为 First(IMAGE_IMPORT_DESCRIPTOR . (Original) FirstTrunk 表示某个 DLL 被本模块导入的首个函数的 Trunk 的位置,后面还有更多的函数 Trunk,以 NULL 表征结束)。OriginalFirstTrunk 在加载后保持不变(所以称为 Original),所以相当于存储着导入函数名称的一份副本。在模块被加载后,可以通过 OriginalFirstTrunk 数组了解到该模块导入了哪些函数(名称),通过 FirstTrunk 数组的内容可了解到导入函数的运行时虚拟地址。由于导入函数的实际地址是在加载时绑定(无法在编译时确定),所以编译器为每个 dll 函数调用生成一个很小的函数体,称为 j_XXX, 该函数体负责 jmp 到 FirstTrunk 数组中的元素给出的运行时函数地址。
虽然应用程序可以通过序号导入函数,并具有极高效率,但是这样会导致看不到导入函数的名字,对程序和系统的维护造成障碍。所以按名称导入是普遍做法,显然按名称导入,需要线性搜索模块的导出函数表,这就会消耗一定的加载时间成本。为了提高程序加载时效率,应用程序可以通过 “事先 Rebase” (将程序需要导入的模块自身建议的 ImageBase 进行精心调整,从而避免在加载时重定向) 和 “事先绑定” 提高程序在客户运行环境的加载速度,系统通过时间戳判定绑定信息是否有效,如果时间戳不一致,或者发生重定向,系统则必须再次进行加载时绑定。
OriginalFirstTrunk 和 FirstTrunk 指向的这两个指针数组位于 .rdata 的不同位置,其中 FirstTrunk 指向的数组位于 .rdata 的起始位置(稍后可以看到这就是 IAT),OriginalFirstTrunk 指向的数组位于稍微靠后的位置。两个 Trunk 在 PE 文件中的值都指向相同的 IMAGE_IMPORT_BY_NAME (由 Hint 和 函数名称字符串 组成的数据结构)。IAT 所在的页面将在加载时被临时设定为可写,绑定之后再恢复为只读。有关这部分的细节请参考我的博客文章:《读取PE文件的导入表》。
了解了导入表结构,就可以很快找到导入表的位置了,首先在 .rdata 中查找 DLL 名称字符串,可以找到如下的字符串:
0x000077AC: "KERNEL32.dll"; (这里使用的是文件地址,或者说是 RVA)
找到附近指向该位置的指针,即在附近的文件内容中搜索 "AC 77 00 00" 片段,可以找到文件地址:
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
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 文件中,菜单就加好了!
有关资源表的结构的更多信息,请参考我的博客文章(这里不做更多说明):《读取PE文件的资源表》。
(2)添加菜单处理函数(子类化窗口):
菜单加好以后,现在点击菜单还没有任何反应。接下来为菜单添加命令处理函数,因此观察窗口过程 WndProc 的汇编代码,可以发现:WndProc 没有为 WM_COMMAND 留出任何空隙和空间供我们插入自己的代码,即没有办法 hack 已有代码来完成这个功能。因此只能在 .text 中追加新的代码。
方法是,在 .text 尾部追加一个函数作为新的窗口过程,这个过程和在 MFC 中子类化一个控件的本质相同,也类似于通常所说的 Hook,即挂钩一个新的函数,由新的 Hook 函数添加自己的处理逻辑,然后再把控制权交回到原来的函数。
这里还需要说明另一个问题,题目要求点击菜单时弹出 MessageBox。但是在现有的导入表中可以看到,程序并没有导入 MessageBoxA 这个函数,所以如果直接调用 MessageBoxA,则需要调整导入表。这样相对的比较麻烦。这时候前面我们找到的那个非常有趣的函数(sub_4059C4)就有用了,观察那个函数,其汇编代码如下:
Code 2.3 代码段中的函数: sub_4059C4 的汇编代码(USER32. 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 showMsgBox(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++ 代码(作为 sub_4059C4 的参数):
//在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); }
为了得到新的窗口过程,在样本程序中写出新的窗口过程函数,并以 debug 选项编译(之所以采用 debug,是因为 release 优化幅度过大,其结果不利于我们利用。例如我在 release 下编译出挂钩后的结果,编译结果显示原有的窗口过程的第一个参数 hWnd 被优化掉了,因为它已经不再作为窗口过程使用,而仅仅是被新的窗口过程调用的一个普通函数,所以编译器可以按照自己的喜好对它做任何等效变换!)。
Code 2.5 为了子类化窗口,新窗口过程的 C++ 代码(用于得到其汇编代码):
BOOL showMsgBox(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) { int wmId; if(message == WM_COMMAND && LOWORD(wParam) == IDM_ABOUT) { showMsgBox( "看雪论坛.珠海金山2007逆向分析挑战赛\r\nhttp://www.pediy.com", "pediy", MB_ICONINFORMATION); return TRUE; } return WndProc(hWnd, message, wParam, lParam); }
其中上面代码中的 showMsgBox 只是对实际函数的一个简单模拟,这样产生的窗口过程的代码,只需要计算出一些偏移值即可。接下来反汇编上面的样本代码的 debug 编译结果,把 debug 版本中做简要处理,去掉 debug 版本特有的那些填充 INT3 和 ESP校验 那些没什么用处的代码,就可以得到需要插入的汇编代码了,通过以下函数,把新的窗口过程代码插入到 PE 文件中(由于段在内存中对齐到 4KB,所以每个段的结尾基本上都有相当大的空间剩余,可以插入一些新的内容),如下所示:
Code 2.6 用于向 .text 尾部插入新的窗口过程的 C++ 代码(用于窗口子类化):
//返回插入的字节数 void InsertNewWndProc(LPCTSTR pFileName, int InsertPos) { 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 showMsgBox (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; //计算 showMsgBox 的偏移地址 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 版本的汇编代码的基础上,经过删减得到的,已经增加了注释。
代码中由两处偏移地址需要进行调整,分别是 showMsgBox 和 oldWndProc 的偏移地址。showMsgBox 的前两个参数为新插入到 .rdata 尾部的两个常量字符串,其地址(VA)已经直接编入 _code 数组中了。即,通过以下方式完成插入新的窗口过程:
Code 2.7 插入 “常量字符串” 和 “新的窗口过程” 到 PE 文件的执行动作:
//[2] 向修改后的PE文件中插入常量字符串 InsertString(szPath, 0x7C80, "---OurString---"); InsertString(szPath, 0x7C90, "pediy"); InsertString(szPath, 0x7CA0, "看雪论坛.珠海金山2007逆向分析挑战赛\r\nhttp://www.pediy.com"); //[3] 插入新的窗口过程!相当于对其子类化 InsertNewWndProc(szPath, 0x6B80);
现在尾部插入一个没用的分隔字符串:”---OurString---"。(恰好16Bytes,在16进制编辑器中占据一行),接下来插入两个常量字符串:
0x7C90: "pediy" , Title of MsgBox; // 这里采用的是 “文件地址” 或者说 RVA。
0x7CA0: "看雪论坛..珠海金山2007..." , Text of MsgBox;
注意:插入新的字符串常量后,不要忘记同步调整 .rdata 的 VirtualSize !
Code 2.8 再次反汇编得到“新窗口过程” 的汇编代码 (0x62 Bytes):
.text:00406B80 ; int __stdcall NewWndProc(int hWnd,UINT Msg,WPARAM wParam,LPARAM lParam) .text:00406B80 hWnd = dword ptr 8 .text:00406B80 Msg = dword ptr 0Ch .text:00406B80 wParam = dword ptr 10h .text:00406B80 lParam = dword ptr 14h .text:00406B80 .text:00406B80 push ebp .text:00406B81 mov ebp, esp .text:00406B83 sub esp, 20h .text:00406B89 push ebx .text:00406B8A push esi .text:00406B8B push edi .text:00406B8C cmp [ebp+Msg], 111h ; Msg == WM_COMMAND? .text:00406B93 jnz short __CALL_OLD_WNDPROC .text:00406B95 mov eax, [ebp+wParam] .text:00406B98 and eax, 0FFFFh .text:00406B9D movzx ecx, ax .text:00406BA0 cmp ecx, 68h ; LOWORD(wParam) == IDM_ABOUT? .text:00406BA3 jnz short __CALL_OLD_WNDPROC .text:00406BA5 push 40h .text:00406BA7 push offset aPediy_0 ; "pediy" .text:00406BAC push offset unk_407CA0 ; "看雪论坛.珠海金山2007..." .text:00406BB1 call sub_4059C4 ; MessageBoxA 的已有替代品 .text:00406BB6 add esp, 0Ch ; 复原 栈指针 .text:00406BB9 mov eax, 1 ; return TRUE; .text:00406BBE jmp short __END .text:00406BC0 .text:00406BC0 __CALL_OLD_WNDPROC: .text:00406BC0 mov eax, [ebp+lParam] .text:00406BC3 push eax ; lParam .text:00406BC4 mov ecx, [ebp+wParam] .text:00406BC7 push ecx ; wParam .text:00406BC8 mov edx, [ebp+Msg] .text:00406BCB push edx ; Msg .text:00406BCC mov eax, [ebp+hWnd] .text:00406BCF push eax ; hWnd .text:00406BD0 call sub_4012D5 ; return OldWndProc(hWnd, Msg, wParam, lParam); .text:00406BD5 .text:00406BD5 __END: .text:00406BD5 pop edi .text:00406BD6 pop esi .text:00406BD7 pop ebx .text:00406BD8 add esp, 20h .text:00406BDE pop ebp .text:00406BDF retn 10h .text:00406BDF sub_406B80 endp
新的窗口过程已经被插入到了 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 ) 的机器码解读:
字段 | Prefixes | Opcode | ModR/M | SIB | Displacement | Immediate | |||||||
Mod | Reg/Opcode | R/M | Scale | Index | Base | ||||||||
二进制 | 1100 0111 | 01 | 000 | 101 | 1011 0100 | 略 | 略 | 略 | 略 | ||||
16进制 | <absent> | C7 | 45 | <absent> | B4 | 80 | 6B | 40 | 00 | ||||
+disp8 | <meaningless> | [EBP] | |||||||||||
说明 | MOV | [EBP] + disp8 | disp8 = -76 | imm32 | |||||||||
[EBP] - 4Ch, | 0x0040 6B80 | ||||||||||||
Dest Operand, | Src Operand | ||||||||||||
r/m32, | imm32 | ||||||||||||
WndCls 的地址位于 EBP - 50h 处, WndCls.lpfnWndProc 的结构体内偏移值 = 4 Bytes; 因此 EBP - 4Ch 相当于 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 系统内部已经统一采用 Unicode 字符串,在这种环境下,采用 Unicode 编码的程序比采用多字节编码的程序的运行效率更高,所以 VC 项目的选项应该优先采用 Unicode 编码)。
而资源就不一样了,资源可以由字符串来标识,完全可以用个性化的语言文字来定义,比如说用户把菜单名字取名为“我的上下文菜单”这样的名称,是完全可能也被允许的,所以资源表中的字符串一律采用 Unicode 编码。
(3)由于我的笔记本安装的是 Win7 / 64-bit 版本操作系统,所以在 IDA 中调试时居然是 64 位模式,有一些不适应。
【下载链接】本题目的附件,和文本中提到的代码的下载链接:
http://files.cnblogs.com/hoodlum1980/pediy02_Answer.zip
【参考资料】
(1)读取文件的导入表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/08/1821778.html。
(2)读取文件的资源表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/10/1822906.html。
(3)[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;