• “金山杯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 元素分布示意图和说明。

  • 相关阅读:
    HDU 1086 You can Solve a Geometry Problem too(水题,判断线段相交)
    2011ACM福州网络赛第一题 A Card Game(水题)
    Lottery hdu1099
    卡特兰数 ACM 数论
    (转)CXF的Exception问题
    简单PS合成图像(抹去某个人等)
    摄影技巧
    PS中文字变形
    螺旋阵(递归和非递归)
    路径与滤镜和自定义工具
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/3775705.html
Copyright © 2020-2023  润新知