zoukankan      html  css  js  c++  java
  • “金山杯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 窗口:

      点击菜单弹出的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)。

      补充说明:在没有经过事先绑定时,OriginalFirstTrunkFirstTrunk 指向的数组内容在加载之前都指向 .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 进行精心调整,从而避免在加载时重定向) 和 “事先绑定” 提高程序在客户运行环境的加载速度,系统通过时间戳判定绑定信息是否有效,如果时间戳不一致,或者发生重定向,系统则必须再次进行加载时绑定。

      

      OriginalFirstTrunkFirstTrunk 指向的这两个指针数组位于 .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 )中取值,如果在其他列取值将会引发运行时异常(参见如下实验)。

      Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte

      我做了一个实验,当改变 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;
    }
    InsertStringEx_cpp

      其中,代码中忘了附上 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;
    }
    ImportTable_Layout_cpp

      程序产生的输出如下(其中,地址均为 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 元素分布示意图和说明。

  • 相关阅读:
    (Java实现) 洛谷 P1603 斯诺登的密码
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1553 数字反转(升级版)
    8.4 确定两个日期之间的月份数或年数
    (Java实现) 洛谷 P1553 数字反转(升级版)
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/3775705.html
Copyright © 2011-2022 走看看