hoodlum1980 [ 发发 ] 的技术博客

博客园 首页 新随笔 联系 订阅 管理

  注:题目来自于以下链接地址: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文件格式的辅助工具而强烈推荐):

  https://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逆向分析挑战赛\r\nhttp://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逆向分析挑战赛\r\nhttp://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 位模式,有一些不适应。

 

  【下载链接】本题目的附件,和文本中提到的代码的下载链接:

   https://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>\n")
                _T("          FirstTrunk:         00000000 <null>\n")
                _T("          OriginalFirstTrunk: 00000000 <null>\n")
                _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\n")
            _T("          FirstTrunk:         %08X\n")
            _T("          OriginalFirstTrunk: %08X\n")
            _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, "-------------------------------------\n");
            fprintf(fpLog, "      GAP: 0x%08X (%ld Bytes)\n", 
                pos->RVA - PrevRVA_End,
                pos->RVA - PrevRVA_End);
            fprintf(fpLog, "-------------------------------------\n");
        }
        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\n", pos->name);
    }
    fclose(fpLog);

_CLEANUP:
    nodes.clear();

    if(fp == NULL)
        printf("Canot open PE file.\n");
    else
        fclose(fp);

    if(pSectionHdrs != NULL)
        free(pSectionHdrs);

    //printf("press any key to continue...\n");
    //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 元素分布示意图和说明。

 

posted on 2014-06-08 06:37  hoodlum1980  阅读(1440)  评论(1编辑  收藏  举报