首页 > 代码库 > “金山杯2007逆向分析挑战赛”第一阶段第二题

“金山杯2007逆向分析挑战赛”第一阶段第二题

  注:题目来自于以下链接地址:

  http://www.pediy.com/kssd/

  目录:第13篇 论坛活动 \ 金山杯2007逆向分析挑战赛 \ 第一阶段 \ 第二题 \ 题目 \ [第一阶段 第二题]

 

  题目描述:

 

  己知是一个 PE 格式 EXE 文件,其三个(section)区块的数据文件依次如下:(详见附件)
 
  _text,_rdata,_data

  1. 将 _text, _rdata, _data合并成一个 EXE 文件,重建一个 PE 头,一些关键参数,如 EntryPoint,ImportTable 的 RVA,请自己分析文件获得。合并成功后,程序即可运行。
  2. 请在第1步获得的EXE文件基础上,增加菜单。具体见图:

  要插入的菜单

 

  3. 执行菜单 Help / About 弹出如下图所示的 MessageBox 窗口:

  点击菜单弹出的MessageBox

 

  题目分析和解答:

 

  (一)拼接可执行文件:

 

  首先下载题目的附件,附件中已经有三个文件,分别是 PE 文件的三个 section,可以看到三个 section 文件已经按照 0x1000 大小对齐。这样我们只需要把这三个文件依次连接在一起,接在一个正确的 PE 文件头后面就可以了。

 

  可以先用 VC (我采用 VS2005)创建一个 Windows 窗口程序(它将提供一些主要样本,所以称这个程序为样本程序),把程序写的尽可能和题目中的程序类似,然后编译,即首先得到了一个 PE 文件头的原型,再次基础上进行修改,也就是根据题目给出的 section,适当调整 PE 文件头中的需要修改的字段。

 

  在本题求解过程中,我严重依赖于我从前写的一个展示 PE 文件格式的应用程序,此程序最近经过我的调整和改进,它的优点是由于此程序基于扩展 TreeView 控件,因此帮助快速理解 PE 文件头的结构,其效果见以下截图:

 

  

  

  关于此程序的更多信息,请参见我的博客文章:《[VC6] 图像文件格式数据查看》。

 

  BmpFileView 的可执行文件的下载链接(不敢说它是最好的,但作为帮助学习PE文件格式的辅助工具而强烈推荐):

  http://files.cnblogs.com/hoodlum1980/BmpFileView_V2_Bin.zip

 

  观察题目给出的三个 section 文件,可以给出这三个 section 的基本信息如下:

SectionName VirtualAddress RawDataSize VirtualSize
.text 1000h 6000h 5B73h
.rdata 7000h 1000h 0C6Eh
.data 8000h 3000h 4000h
.rsrc B000h    

  

  其中,.rsrc 是需要在稍后插入的资源 section,将在稍后讲解。

  这里需要特别注意的是,.data 的虚拟内存尺寸,必须要比文件尺寸(RawDataSize)更大一些,关于这一点我还暂时不能给出详细的解释,有待于在将来做进一步研究。如果把 .data 的 VirtualSize 设置为和 RawDataSize 一样大(3000h),则程序无法运行,会弹出一个消息框提示这不是一个有效的 Win32 程序。所以这一步我也是反复尝试是否是其他字段的问题,纠结了半天才发现原来问题卡在这个地方。

 

  对于 PE 文件头的 IMAGE_OPTINAL_HEADER.CheckSum,Windows 看起来完全忽略这个字段的值,所以这个字段可以不用管。

 

  明确了以上问题,现在可以把这三个 section 和文件头链接成一个新的 PE 文件了,把样本程序 pediy02.exe 和三个 section 文件放在同一个目录下,通过一个辅助的 Console 项目(pediy02_helper 项目)来完成这些工作,生成的新的 PE 文件名为 pediy02_new.exe,使用的辅助函数如下(为了简单明了起见,代码中并没有插入繁琐的检测性代码):

 

  Code 1.1 将三个 Section 拼接成 PE 文件的 C++ 代码:

 

int CreateNewPe()
{
    //PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL;
    PIMAGE_DOS_HEADER pDosHdr = NULL;
    PIMAGE_NT_HEADERS pNtHdrs = NULL;
    PIMAGE_SECTION_HEADER pSectionHdr = NULL;

    FILE *fp1, *fp2, *fp3;
    TCHAR szPath[MAX_PATH];
    LPCTSTR szNames[3] = 
    {
        _T("_text"), _T("_rdata"), _T("_data")
    };

    _stprintf_s(szPath, _T("%s\\pediy02.exe"), THE_DIR);
    _tfopen_s(&fp1, szPath, _T("rb"));
    _stprintf_s(szPath, _T("%s\\pediy02_new.exe"), THE_DIR);
    _tfopen_s(&fp2, szPath, _T("wb"));

    //读取文件头部
    void* buf = malloc(0xD000);
    fread(buf, 1, 0x1000, fp1);

    pDosHdr = (PIMAGE_DOS_HEADER)buf;
    pNtHdrs = (PIMAGE_NT_HEADERS)((DWORD)buf + pDosHdr->e_lfanew);
    pSectionHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHdrs + sizeof(IMAGE_NT_HEADERS));

    /*
        | section | addr  | RawDataSize  | VirtualSize
        |---------+-------+--------------+-------------
        | .text   | 1000h | 6000h        | 5B73h 
        | .rdata  | 7000h | 1000h        | 0C6Eh
        | .data   | 8000h | 3000h        | 4000h
        | .rsrc   | B000h | 1000h        | 1000h
    */

    pNtHdrs->FileHeader.NumberOfSections = 4;
    pNtHdrs->OptionalHeader.BaseOfCode = 0x1000;
    pNtHdrs->OptionalHeader.BaseOfData = http://www.mamicode.com/0x8000; //+1000h 的 .rsrc
    pNtHdrs->OptionalHeader.SizeOfCode = 0x6000;
    pNtHdrs->OptionalHeader.SizeOfImage = 0xD000;
    pNtHdrs->OptionalHeader.SizeOfInitializedData = http://www.mamicode.com/0x5000;
    pNtHdrs->OptionalHeader.SizeOfUninitializedData = http://www.mamicode.com/0;
    pNtHdrs->OptionalHeader.AddressOfEntryPoint = 0x1527; //入口点

    //IMAGE_DIRECTORY_ENTRY_IMPORT 需要进一步调整, kernel32.dll, gdi32.dll, user32.dll 加上一个结尾
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = 0x7618;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = 
        sizeof(IMAGE_IMPORT_DESCRIPTOR) * (3 + 1);

    //资源表
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = 0xC000;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = 0x011C;
    
    // IMAGE_DIRECTORY_ENTRY_DEBUG 6
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress = 0;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size = 0;

    // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress = 0;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].Size = 0;

    //IMAGE_DIRECTORY_ENTRY_IAT 12; (import address table), IMAGE_IMPORT_DESCRIPTOR.FirstTrunk 中的最小值
    //IAT 地址需要在修改后找,需要进一步调整
    //IAT 的地址通常就是 .rdata 的起始地址
    //Size 是 FirstTrunk 中的最大地址 - IAT 起始地址) + 8;
    //(其中 +4 是最后一个元素占用的空间,再 +4 是一个NULL元素,表示结尾)
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress = 0x7000;
    pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size = 0x012C;


    //section_headers
    //.text
    pSectionHdr[0].VirtualAddress = 0x1000;
    pSectionHdr[0].SizeOfRawData = http://www.mamicode.com/0x6000;
    pSectionHdr[0].PointerToRawData = http://www.mamicode.com/0x1000;
    pSectionHdr[0].Misc.VirtualSize = 0x5B73;
    //.rdata
    pSectionHdr[1].VirtualAddress = 0x7000;
    pSectionHdr[1].SizeOfRawData = http://www.mamicode.com/0x1000;
    pSectionHdr[1].PointerToRawData = http://www.mamicode.com/0x7000;
    pSectionHdr[1].Misc.VirtualSize = 0x1000;
    //.data
    //.data 的虚拟内存大小(VirtualSize)必须比文件中更大,否则无法启动,现在我也不知道为什么
    pSectionHdr[2].VirtualAddress = 0x8000;
    pSectionHdr[2].SizeOfRawData = http://www.mamicode.com/0x3000;
    pSectionHdr[2].PointerToRawData = http://www.mamicode.com/0x8000;
    pSectionHdr[2].Misc.VirtualSize = 0x4000; //【重要!】必须比 SizeofRawData 大一些

    //.rsrc (resource) 因为.data 比文件中大,所以.rsrc 相应的要像高地址移动
    pSectionHdr[3].VirtualAddress = 0xC000;
    pSectionHdr[3].SizeOfRawData = http://www.mamicode.com/0x1000;
    pSectionHdr[3].PointerToRawData = http://www.mamicode.com/0xB000; //文件中的地址还是紧靠.data
    pSectionHdr[3].Misc.VirtualSize = 0x011C; //从范本文件中得到该值

    fwrite(buf, 1, 0x1000, fp2);
    fflush(fp2);

    int i;
    DWORD dwFileSize;
    for(i = 0; i < 3; i++)
    {
        _stprintf_s(szPath, _T("%s\\%s"), THE_DIR, szNames[i]);
        _tfopen_s(&fp3, szPath, _T("rb"));
        fseek(fp3, 0, SEEK_END);
        dwFileSize = ftell(fp3);
        fseek(fp3, 0, SEEK_SET);
        fread(buf, 1, dwFileSize, fp3);
        fclose(fp3);
        
        WriteToFile(fp2, buf, dwFileSize);
    }

    //从已有的范本复制 .rsrc 节
    fseek(fp1, 0xB000, SEEK_SET);
    fread(buf, 1, 0x1000, fp1);
    WriteToFile(fp2, buf, 0x1000);

    fclose(fp1);
    fclose(fp2);

    free(buf);
    return 0;
}

 

  上面的函数已经是最终版本的函数,它已经完成了以下工作:

 

  (1)确定 AddressOfEntryPoint 的地址。

  (2)确定 DataDirectory[1]: ImportTable (导入表)的地址和尺寸。

  (3)确定 DataDirectory[12]: Import Address Table (绑定导入函数地址表)的地址和尺寸。

  (4)从样本程序 pediy02.exe 中插入资源 (.rsrc) section,并确定 DataDirectory[2]: resource Table (资源表)的地址和尺寸。

 

  当然很显然上面的工作并不是一步到位完成的,下面简要介绍上面的工作是如何完成的:

 

  (1)确定入口点地址:

 

  该工作相对简单容易,先把 EntryPoint 设置为 .text (代码段)的起始地址:0x1000,然后生成文件后,加载到 IDA 中分析代码段的内容,就可以很容易的找到以下函数的地址(以下地址为 VA,即加上了 ImageBase 后的地址):

 

  0x00401527: __tmainCRTStartup,是 PE 文件的实际入口点。

  0x004011EC: WinMain,高级语言编程时的程序入口点。

  0x004012D5: WndProc, 当前的窗口过程(稍后将会被子类化)

  0x004059C4: sub_4059C4,很有趣,可以弹出 MessageBox,为其取名为 showMsgBox

 

  现在只要知道,在文件头中把入口地址设置到 __tmainCRTStartup 函数即可,文件头要求的是 RVA,因此在代码中设置入口点:

 

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint = 0x1527;

 

  这样入口点地址就确定好了。

 

  (2)确定 DataDirectory [1] 导入表的地址和大小:

 

  这一步也相对比较简单,导入表位于 .rdata 中(位于中部)。在此之前,必须了解导入表的结构,导入表是一个由多个 IMAGE_IMPORT_DESCRIPTOR 元素组成的数组,以 NULL 元素(内容全部是 0 )标识结尾(IMAGE_IMPORT_DESCRIPTOR 的数据结构定义参见 winnt.h)。每个元素由 5 个 DWORD 组成,其中倒数第二个 DWORD 是 Name 字段(字符串指针),它的值是一个 RVA(即相对于 ImageBase 的偏移),指向了 Dll 名字(ASCII)字符串(该字符串同样位于 .rdata 中)。

  导入表的示意结构如下图所示:

  

 

  图中导入表的第一行并不是表头,而是字段名称,即上图表示了 pediy2_new.exe 的实际导入表,共导入了 3 个 DLL,每个导入 DLL 是导入表中的一个元素,单个元素大小为:

 

  sizeof ( IMAGE_IMPORT_DESCRIPTOR ) = sizeof ( DWORD ) * 5 = 20 Bytes;

 

  每个元素的 OriginalFirstTrunk 和 FirstTrunk 是两个指针,指向了两个 并行的指针数组,这两个数组的内容是相同的,在静态 PE 文件中,这两个数组都指向相同的长度不固定的函数名称字符串。

 

  OriginalFirstTrunkFirstTrunk 指向的数组内容在加载之前都指向 .rdata 中的一些长度不固定的字符串,在加载时 FirstTrunk 指向的数组被系统绑定成映射到本进程的 DLL 的实际函数地址(因此该数组称为 IAT),所以这些元素称为 Trunk,因为指向的是数组头部,所以称之为 First(IMAGE_IMPORT_DESCRIPTOR . (Original) FirstTrunk 表示某个 DLL 被本模块导入的首个函数的 Trunk 的位置,后面还有更多的函数 Trunk,以 NULL 表征结束)。OriginalFirstTrunk 在加载后保持不变(所以称为 Original),所以相当于存储着导入函数名称的一份副本。在模块被加载后,可以通过 OriginalFirstTrunk 数组了解到该模块导入了哪些函数(名称),通过 FirstTrunk 数组的内容可了解到导入函数的运行时虚拟地址。由于导入函数的实际地址是在加载时绑定(无法在编译时确定),所以编译器为每个 dll 函数调用生成一个很小的函数体,称为 j_XXX, 该函数体负责 jmp 到 FirstTrunk 数组中的元素给出的运行时函数地址。

 

  虽然应用程序可以通过序号导入函数,并具有极高效率,但是这样会导致看不到导入函数的名字,对程序和系统的维护造成障碍。所以按名称导入是普遍做法,显然按名称导入,需要线性搜索模块的导出函数表,这就会消耗一定的加载时间成本。为了提高程序加载时效率,应用程序可以通过 “事先 Rebase” (将程序需要导入的模块自身建议的 ImageBase 进行精心调整,从而避免在加载时重定向) 和 “事先绑定” 提高程序在客户运行环境的加载速度,系统通过时间戳判定绑定信息是否有效,如果时间戳不一致,或者发生重定向,系统则必须再次进行加载时绑定。

  

  OriginalFirstTrunkFirstTrunk 指向的这两个指针数组位于 .rdata 的不同位置,其中 FirstTrunk 指向的数组位于 .rdata 的起始位置(稍后可以看到这就是 IAT),OriginalFirstTrunk 指向的数组位于稍微靠后的位置。两个 Trunk 在 PE 文件中的值都指向相同的 IMAGE_IMPORT_BY_NAME (由 Hint 和 函数名称字符串 组成的数据结构)。IAT 所在的页面将在加载时被临时设定为可写,绑定之后再恢复为只读。有关这部分的细节请参考我的博客文章:《读取PE文件的导入表》。

 

  了解了导入表结构,就可以很快找到导入表的位置了,首先在 .rdata 中查找 DLL 名称字符串,可以找到如下的字符串:

 

  0x000077AC: "KERNEL32.dll";  (这里使用的是文件地址,或者说是 RVA)

  

  找到附近指向该位置的指针,即在附近的文件内容中搜索 "AC 77 00 00" 片段,可以找到文件地址:

 

  0x00007624: AC 77 00 00

 

  这里就是一个 IMAGE_IMPORT_DESCRIPTOR 元素,把该地址减去 3 个 DWORD 值,即得到该元素的起始地址为 0x00007618。由于导入表元素内容非常有特点,很容易就可以判断导入表的两端边界,因此可以很快确定导入表的起始地址(RVA)和 Size 如下:

 

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].VirtualAddress = 0x7618;

  IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].Size = sizeof ( IMAGE_IMPORT_DESCRIPTOR ) * 4;

 

  (3)确定 DataDirectory [12],IAT的地址和大小:

 

  IAT 的地址比较简单,它就是所有 DLL 的 FirstTrunk 字段的最小值,通常就是 .rdata 的起始位置(那些常量字符串位于 IAT 和 ImportTable 的后面),也就是 0x7000 (可以看到这里是从 Gdi32.dll 的导入的第一个函数 DeleteObject)。

 

  要计算 IAT 的大小,需要遍历导入表,找到导入的所有 Dll 的 FirstTrunk 的最后一个元素的位置,同时还要考虑到结尾还需要一个 NULL 指针作为结束标志,所以:

  

  IAT.Size = max ( 所有 DLL 的 FirstTrunk 数组元素所在的地址(RVA) ) - IAT.VirtualAddress (RVA) + 8 。

  

  有关如何遍历导入表的更多内容,请参考我的博客文章(在此就不再详细叙述了):《读取PE文件的导入表》。

 

  本题目中所有的 Trunk 的最大地址(RVA)是 0x7124(从 USER32.dll 导入的 DispatchMessageA),可得:

 

  DataDirectory[12].VirualAddress = 0x7000; //RVA

  DataDirectory[12].Size = 0x012C;

 

  经过以上修改,可以通过 CreateNewPe 函数,生成一个可以执行的 PE 文件了。题目的前半部分要求此时完成。接下来考虑后半部分要求,为程序添加菜单和相关的命令处理函数。

 

  (二)添加菜单 和 处理函数。

 

  (1)添加 .rsrc section (菜单资源)

 

  添加资源,同样通过在样本程序中实现。在样本程序中,添加题目要求一样的资源(只保留菜单,删除所有其他种类资源,这样可以使 .rsrc 最小,仅占用 1000h 大小),然后可以从样本程序中拷贝 .rsrc 段,追加到我们已经得到的 PE 文件的尾部。同时调整 PE 文件头中的相关字段。

 

  注意:由于 .data 节在加载到虚拟内存中时被扩大了 1000h,所以位于最后的 .rsrc 的文件地址(FA)和虚拟地址(VA)将会偏差 1000h。即:

 

  VA = FA + 1000h;

 

  众所周知,窗口的菜单通常是在注册窗口类时指定的。因此为了添加菜单,在 IDA 中观察 WinMain 函数的代码:

 

  Code 2.1 由 .text 提供的 WinMain 函数的汇编代码:

 

.text:004011EC ; int __stdcall WinMain(int,int,int,int nCmdShow)
.text:004011EC WinMain         proc near               ; CODE XREF: start+C9p
.text:004011EC
.text:004011EC WndClass        = WNDCLASSA ptr -50h
.text:004011EC Msg             = MSG ptr -28h
.text:004011EC var_C           = dword ptr -0Ch
.text:004011EC arg_0           = dword ptr  8
.text:004011EC nCmdShow        = dword ptr  14h
.text:004011EC
.text:004011EC                 push    ebp
.text:004011ED                 mov     ebp, esp
.text:004011EF                 sub     esp, 50h
.text:004011F2                 push    ebx
.text:004011F3                 push    esi
.text:004011F4                 push    edi
.text:004011F5                 mov     esi, offset aPediy_com ; "pediy.com"
.text:004011FA                 lea     edi, [ebp+var_C]
.text:004011FD                 mov     ebx, [ebp+arg_0]
.text:00401200                 movsd   ; char var_C[] = "pediy.com"; 【重要暗示!!!】
.text:00401201                 movsd
.text:00401202                 movsw
.text:00401204                 mov     edi, 7F00h
.text:00401209                 xor     esi, esi
.text:0040120B                 push    edi             ; lpIconName
.text:0040120C                 push    esi             ; hInstance
.text:0040120D                 mov     dword_40ABAC, ebx
.text:00401213                 mov     [ebp+WndClass.style], 3
.text:0040121A                 mov     [ebp+WndClass.lpfnWndProc], offset sub_406B80
.text:00401221                 mov     [ebp+WndClass.cbClsExtra], esi
.text:00401224                 mov     [ebp+WndClass.cbWndExtra], esi
.text:00401227                 mov     [ebp+WndClass.hInstance], ebx
.text:0040122A                 call    ds:LoadIconA
.text:00401230                 push    edi             ; lpCursorName
.text:00401231                 push    esi             ; hInstance
.text:00401232                 mov     [ebp+WndClass.hIcon], eax
.text:00401235                 call    ds:LoadCursorA
.text:0040123B                 push    esi             ; int
.text:0040123C                 mov     [ebp+WndClass.hCursor], eax
.text:0040123F                 call    ds:GetStockObject
.text:00401245                 mov     [ebp+WndClass.hbrBackground], eax
.text:00401248                 lea     eax, [ebp+var_C]
.text:0040124B                 mov     [ebp+WndClass.lpszMenuName], eax ; lpszMenuName = var_C;
.text:0040124E                 lea     eax, [ebp+WndClass]
.text:00401251                 mov     edi, offset aPediy_com_0 ; "pediy.com"
.text:00401256                 push    eax             ; lpWndClass
.text:00401257                 mov     [ebp+WndClass.lpszClassName], edi
.text:0040125A                 call    ds:RegisterClassA
.text:00401260                 test    ax, ax
.text:00401263                 jnz     short loc_401269
.text:00401265                 xor     eax, eax
 。。。

 

  菜单资源可以采用数字来标识,也可以采用字符串标识。如果在 VC 中添加菜单,默认为以数字标识。如果要以数字标识菜单,第一个想法是需要 hack 上面的代码。

  但上面的代码实际上不需要做任何改动,因为它给了我们一个强烈暗示,上面的汇编代码翻译到 C 语言如下:

 

  Code 2.2 将 WinMain 从汇编代码翻译到 C++ 的代码(得到 Menu Name):

 

int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    char var_C[] = "pediy.com"; // ---- 重要暗示!!!----
    MSG msg;
    WNDCLASSA wndCls;

    //保存到全局变量
    hInst = hInstance;

    wndCls.style = CS_HREDRAW | CS_VREDRAW;
    wndCls.lpfnWndProc = WndProc;
    wndCls.cbClsExtra = 0;
    wndCls.cbWndExtra = 0;
    wndCls.hInstance = hInst;
    wndCls.hIcon = LoadIconA(NULL, IDI_APPLICATION);
    wndCls.hCursor = LoadCursorA(NULL, IDC_ARROW);
    wndCls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndCls.lpszMenuName = var_C; // ---- 重要暗示!!!----
    wndCls.lpszClassName = "pediy.com";

    if (!RegisterClassA(&wndCls))
        return FALSE;

    HWND hWnd = CreateWindowExA(
        0,             // EXStyle
        "pediy.com",   // wndClass
        WS_BORDER | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP | WS_TABSTOP,
//style CW_USEDEFAULT, // X CW_USEDEFAULT, // Y CW_USEDEFAULT, // nWidth CW_USEDEFAULT, // nHeight NULL, // hWndParent NULL, // hMenu hInst, // hInstance 0); // lParam ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessageA(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; }

 

 

  就是窗口类的菜单是由 var_C 指定的,var_C 是栈上的临时变量,内容被加载为”pediy.com“。即菜单的字符串标识是 ”pediy.com“。所以任务就简单了,在样本程序中,把菜单的 ID 改为字符串”pediy.com“,然后把编译好的样本程序的 .rsrc 追加到 PE 文件中,菜单就加好了!

 

  有关资源表的结构的更多信息,请参考我的博客文章(这里不做更多说明):《读取PE文件的资源表》。

 

  (2)添加菜单处理函数(子类化窗口):

 

  菜单加好以后,现在点击菜单还没有任何反应。接下来为菜单添加命令处理函数,因此观察窗口过程 WndProc 的汇编代码,可以发现:WndProc 没有为 WM_COMMAND 留出任何空隙和空间供我们插入自己的代码,即没有办法 hack 已有代码来完成这个功能。因此只能在 .text 中追加新的代码。

 

  方法是,在 .text 尾部追加一个函数作为新的窗口过程,这个过程和在 MFC 中子类化一个控件的本质相同,也类似于通常所说的 Hook,即挂钩一个新的函数,由新的 Hook 函数添加自己的处理逻辑,然后再把控制权交回到原来的函数。

 

  这里还需要说明另一个问题,题目要求点击菜单时弹出 MessageBox。但是在现有的导入表中可以看到,程序并没有导入 MessageBoxA 这个函数,所以如果直接调用 MessageBoxA,则需要调整导入表。这样相对的比较麻烦。这时候前面我们找到的那个非常有趣的函数(sub_4059C4)就有用了,观察那个函数,其汇编代码如下:

 

  Code 2.3 代码段中的函数: sub_4059C4 的汇编代码(USER32. MessageBoxA 的替代品):

 

.text:004059C4 sub_4059C4      proc near               ;
.text:004059C4 arg_0           = dword ptr  8
.text:004059C4 arg_4           = dword ptr  0Ch
.text:004059C4
.text:004059C4                 push    ebx
.text:004059C5                 xor     ebx, ebx
.text:004059C7                 cmp     dword_40AB70, ebx
.text:004059CD                 push    esi
.text:004059CE                 push    edi
.text:004059CF                 jnz     short loc_405A13
.text:004059D1                 push    offset LibFileName ; "user32.dll"
.text:004059D6                 call    ds:LoadLibraryA
.text:004059DC                 mov     edi, eax
.text:004059DE                 cmp     edi, ebx
.text:004059E0                 jz      short loc_405A49
.text:004059E2                 mov     esi, ds:GetProcAddress
.text:004059E8                 push    offset aMessageboxa ; "MessageBoxA"
.text:004059ED                 push    edi             ; hModule
.text:004059EE                 call    esi ; GetProcAddress
.text:004059F0                 test    eax, eax
.text:004059F2                 mov     dword_40AB70, eax
.text:004059F7                 jz      short loc_405A49
.text:004059F9                 push    offset aGetactivewindo ; "GetActiveWindow"
.text:004059FE                 push    edi             ; hModule
.text:004059FF                 call    esi ; GetProcAddress
.text:00405A01                 push    offset aGetlastactivep ; "GetLastActivePopup"
.text:00405A06                 push    edi             ; hModule
.text:00405A07                 mov     dword_40AB74, eax
.text:00405A0C                 call    esi ; GetProcAddress
.text:00405A0E                 mov     dword_40AB78, eax
.text:00405A13
.text:00405A13 loc_405A13:                             ; CODE XREF: sub_4059C4+Bj
.text:00405A13                 mov     eax, dword_40AB74
.text:00405A18                 test    eax, eax
.text:00405A1A                 jz      short loc_405A32
.text:00405A1C                 call    eax
.text:00405A1E                 mov     ebx, eax
.text:00405A20                 test    ebx, ebx
.text:00405A22                 jz      short loc_405A32
.text:00405A24                 mov     eax, dword_40AB78
.text:00405A29                 test    eax, eax
.text:00405A2B                 jz      short loc_405A32
.text:00405A2D                 push    ebx
.text:00405A2E                 call    eax
.text:00405A30                 mov     ebx, eax
.text:00405A32
.text:00405A32 loc_405A32:                             ; CODE XREF: sub_4059C4+56j
.text:00405A32                                         ; sub_4059C4+5Ej ...
.text:00405A32                 push    [esp+0Ch+arg_4]
.text:00405A36                 push    [esp+10h+arg_0]
.text:00405A3A                 push    dword ptr [esp+18h]
.text:00405A3E                 push    ebx
.text:00405A3F                 call    dword_40AB70
.text:00405A45
.text:00405A45 loc_405A45:                             ; CODE XREF: sub_4059C4+87j
.text:00405A45                 pop     edi
.text:00405A46                 pop     esi
.text:00405A47                 pop     ebx
.text:00405A48                 retn
.text:00405A49 ;
.text:00405A49
.text:00405A49 loc_405A49:                             ; CODE XREF: sub_4059C4+1Cj
.text:00405A49                                         ; sub_4059C4+33j
.text:00405A49                 xor     eax, eax
.text:00405A4B                 jmp     short loc_405A45
.text:00405A4B sub_4059C4      endp

 

  这个函数内容非常简单,内容注释就不写了,总之,这个函数的功能是动态获取 MessageBoxA 的地址并调用。原型相当于:

 

  int showMsgBox(const char* pText, const char* pTitle, UINT nType);

 

  由于函数没有复原 ESP,所以是默认的 C 调用约定。这个函数和 MessageBoxA 的区别是:

 

  (a)调用约定不同,MessageBoxA 为 __stdcall 。

  (b)只比 MessageBoxA 少了第一个参数: HWND hWnd。该函数在内部获取了一个 HWND 作为 Owner 窗口弹出 MessageBox。

 

  因此,不需要调整导入表,只需要在新的窗口过程中去调用这个函数即可完成弹出 MessageBox 的功能。

 

  同时可以看到,MessageBox 的文本内容,在 .rdata 中并没有可供使用的现成字符串,所以需要插入常量字符串,只需要在 .rdata 的尾部插入即可,通过以下函数即可完成(注意插入新的常量字符串后,需要相应的调整 IMAGE_SECTION_HEADER.VirtualSize,以容纳新的字符串内容):

 

  Code 2.4 向 .rdata 段尾部插入常量字符串的 C++ 代码(作为 sub_4059C4 的参数):

 

//在PE文件中插入常量字符串
void InsertString(LPCTSTR pFileName, int InsertPos, const char *pStr)
{
    int BytesToWrite = strlen(pStr) + 1;
    FILE *fp = NULL;
    _tfopen_s(&fp, pFileName, _T("r+b"));

    fseek(fp, InsertPos, SEEK_SET);

    fwrite(pStr, 1, BytesToWrite, fp);
    fclose(fp);
}

 

  为了得到新的窗口过程,在样本程序中写出新的窗口过程函数,并以 debug 选项编译(之所以采用 debug,是因为 release 优化幅度过大,其结果不利于我们利用。例如我在 release 下编译出挂钩后的结果,编译结果显示原有的窗口过程的第一个参数 hWnd 被优化掉了,因为它已经不再作为窗口过程使用,而仅仅是被新的窗口过程调用的一个普通函数,所以编译器可以按照自己的喜好对它做任何等效变换!)。

 

  Code 2.5 为了子类化窗口,新窗口过程的 C++ 代码(用于得到其汇编代码):

 

BOOL showMsgBox(const char* szText, const char* szTitle, UINT nType)
{
    HMODULE hModule = LoadLibraryA("user32.dll");
    int (__stdcall *pFunc)(HWND, LPCSTR, LPCSTR, UINT uType);
    pFunc = (int (__stdcall *)(HWND, LPCSTR, LPCSTR,UINT uType))GetProcAddress(hModule, "MessageBoxA");

    pFunc(NULL, szText, szTitle, nType);

    return TRUE;
}

LRESULT CALLBACK NewWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId;
    if(message == WM_COMMAND && LOWORD(wParam) == IDM_ABOUT)
    {
        showMsgBox(
            "看雪论坛.珠海金山2007逆向分析挑战赛\r\nhttp://www.pediy.com", 
            "pediy", 
            MB_ICONINFORMATION);
        return TRUE;
    }
    return WndProc(hWnd, message, wParam, lParam);
}

 

  其中上面代码中的 showMsgBox 只是对实际函数的一个简单模拟,这样产生的窗口过程的代码,只需要计算出一些偏移值即可。接下来反汇编上面的样本代码的 debug 编译结果,把 debug 版本中做简要处理,去掉 debug 版本特有的那些填充 INT3 和 ESP校验 那些没什么用处的代码,就可以得到需要插入的汇编代码了,通过以下函数,把新的窗口过程代码插入到 PE 文件中(由于段在内存中对齐到 4KB,所以每个段的结尾基本上都有相当大的空间剩余,可以插入一些新的内容),如下所示:

 

  Code 2.6 用于向 .text 尾部插入新的窗口过程的 C++ 代码(用于窗口子类化):

 

//返回插入的字节数
void InsertNewWndProc(LPCTSTR pFileName, int InsertPos)
{
    BYTE _code[] =
    {
        0x55,            //00: push EBP
        0x8B, 0xEC,        //01: mov EBP, ESP
        0x81, 0xEC,    0x20, 0x00, 0x00, 0x00,
                        //03: sub ESP, 20H
        0x53,            //09: push EBX
        0x56,            //0A: push ESI
        0x57,            //0B: push EDI
        
        0x81, 0x7D, 0x0C, 0x11, 0x01, 0x00, 0x00,
                        //0C: cmp [EBP + nMsg], WM_COMMAND
        0x75, 0x2B,        //13: jne _CALL_OLD_WNDPROC
        0x8B, 0x45,    0x10,
                        //15: mov EAX, [EBP + wParam]
        0x25, 0xFF, 0xFF, 0x00, 0x00,
                        //18: and EAX, 0xFFFF
        0x0F, 0xB7, 0xC8,
                        //1D: movzx ECX, AX
        0x83, 0xF9, 0x68,
                        //20: cmp ECX, 0x68 (IDM_ABOUT = 104)
        0x75, 0x1B,        //23: jne _CALL_OLD_WNDPROC
        0x6A, 0x40,        //25: push MB_ICONINFORMATION
        0x68, 0x90, 0x7C, 0x40, 0x00,
                        //27: push pTitle (0x00407C90: "pediy") 
        0x68, 0xA0, 0x7C, 0x40, 0x00,
                        //2C: push pText  (0x00407CA0 : "...")
        0xE8, 0x00, 0x00, 0x00, 0x00,
                        //31: call showMsgBox (rel32,需要调整)
        0x83, 0xC4, 0x0C,
                        //36: add ESP, 0Ch 调用方复原esp
        0xB8, 0x01, 0x00, 0x00, 0x00,
                        //39: mov EAX, 1
        0xEB, 0x15,        //3E: jmp _RETURN

//_CALL_OLD_WNDPROC: 
        0x8B, 0x45, 0x14,
                        //40: mov EAX, [EBP + lParam]
        0x50,            //43: push EAX
        0x8B, 0x4D, 0x10,
                        //44: mov ECX, [EBP + wParam]
        0x51,            //47: push ECX
        0x8B, 0x55, 0x0C,
                        //48: mov EDX, [EBP + nMsg]
        0x52,            //4B: push EDX
        0x8B, 0x45, 0x08,
                        //4C: mov EAX, [EBP + hWnd]
        0x50,            //4F: push EAX
        0xE8, 0x00, 0x00, 0x00, 0x00,
                        //50: call oldWndProc (rel32,需要调整)

//_RETURN:
        0x5F,            //55: pop EDI
        0x5E,            //56: pop ESI
        0x5B,            //57: pos EBX
        0x81, 0xC4, 0x20, 0x00, 0x00, 0x00,
                        //58: add ESP, 20h
        0x5D,            //5E: pop EBP,
        0xC2, 0x10, 0x00
                        //5F: retn 10h
    };

    union
    {
        int offset;
        UINT dwVal;
        BYTE bytes[4];
    } rel32;

    //计算 showMsgBox 的偏移地址
    int nextAddr = InsertPos + 0x36;

    //注意nextAddr是文件地址,也就是rva(没有加ImageBase)
    //0x59C4 是 showMsgBox 函数的rva
    rel32.offset = 0x59C4 - nextAddr;
    _code[0x32] = rel32.bytes[0];
    _code[0x33] = rel32.bytes[1];
    _code[0x34] = rel32.bytes[2];
    _code[0x35] = rel32.bytes[3];

    //计算 oldWndProc 的偏移地址
    nextAddr = InsertPos + 0x55;

    //0x12D5 是 WndProc 函数的rva
    rel32.offset = 0x12D5 - nextAddr;
    _code[0x51] = rel32.bytes[0];
    _code[0x52] = rel32.bytes[1];
    _code[0x53] = rel32.bytes[2];
    _code[0x54] = rel32.bytes[3];

    int BytesToWrite = sizeof(_code);
    FILE *fp = NULL;
    _tfopen_s(&fp, pFileName, _T("r+b"));

    fseek(fp, InsertPos, SEEK_SET);

    fwrite(_code, 1, BytesToWrite, fp);
    fclose(fp);
}

 

  在上面的代码中,_code 数组的内容是根据 NewWndProc 的 debug 版本的汇编代码的基础上,经过删减得到的,已经增加了注释。

  代码中由两处偏移地址需要进行调整,分别是 showMsgBox 和 oldWndProc 的偏移地址。showMsgBox 的前两个参数为新插入到 .rdata 尾部的两个常量字符串,其地址(VA)已经直接编入 _code 数组中了。即,通过以下方式完成插入新的窗口过程:

 

  Code 2.7 插入 “常量字符串” 和 “新的窗口过程” 到 PE 文件的执行动作:

 

//[2] 向修改后的PE文件中插入常量字符串
InsertString(szPath, 0x7C80, "---OurString---");
InsertString(szPath, 0x7C90, "pediy");
InsertString(szPath, 0x7CA0, 
    "看雪论坛.珠海金山2007逆向分析挑战赛\r\nhttp://www.pediy.com");

//[3] 插入新的窗口过程!相当于对其子类化
InsertNewWndProc(szPath, 0x6B80);

 

  现在尾部插入一个没用的分隔字符串:”---OurString---"。(恰好16Bytes,在16进制编辑器中占据一行),接下来插入两个常量字符串:

 

  0x7C90: "pediy" , Title of MsgBox; // 这里采用的是 “文件地址” 或者说 RVA。

  0x7CA0: "看雪论坛..珠海金山2007..." , Text of MsgBox;

 

  注意:插入新的字符串常量后,不要忘记同步调整 .rdata 的 VirtualSize !

 

 

  Code 2.8 再次反汇编得到“新窗口过程” 的汇编代码 (0x62 Bytes):

 

.text:00406B80 ; int __stdcall NewWndProc(int hWnd,UINT Msg,WPARAM wParam,LPARAM lParam)
.text:00406B80 hWnd            = dword ptr  8
.text:00406B80 Msg             = dword ptr  0Ch
.text:00406B80 wParam          = dword ptr  10h
.text:00406B80 lParam          = dword ptr  14h
.text:00406B80
.text:00406B80                 push    ebp
.text:00406B81                 mov     ebp, esp
.text:00406B83                 sub     esp, 20h
.text:00406B89                 push    ebx
.text:00406B8A                 push    esi
.text:00406B8B                 push    edi
.text:00406B8C                 cmp     [ebp+Msg], 111h ; Msg == WM_COMMAND?
.text:00406B93                 jnz     short __CALL_OLD_WNDPROC
.text:00406B95                 mov     eax, [ebp+wParam]
.text:00406B98                 and     eax, 0FFFFh
.text:00406B9D                 movzx   ecx, ax
.text:00406BA0                 cmp     ecx, 68h ; LOWORD(wParam) == IDM_ABOUT?
.text:00406BA3                 jnz     short __CALL_OLD_WNDPROC
.text:00406BA5                 push    40h
.text:00406BA7                 push    offset aPediy_0 ; "pediy"
.text:00406BAC                 push    offset unk_407CA0 ; "看雪论坛.珠海金山2007..."
.text:00406BB1                 call    sub_4059C4 ; MessageBoxA 的已有替代品
.text:00406BB6                 add     esp, 0Ch ; 复原 栈指针
.text:00406BB9                 mov     eax, 1   ; return TRUE;
.text:00406BBE                 jmp     short __END
.text:00406BC0
.text:00406BC0 __CALL_OLD_WNDPROC:
.text:00406BC0                 mov     eax, [ebp+lParam]
.text:00406BC3                 push    eax             ; lParam
.text:00406BC4                 mov     ecx, [ebp+wParam]
.text:00406BC7                 push    ecx             ; wParam
.text:00406BC8                 mov     edx, [ebp+Msg]
.text:00406BCB                 push    edx             ; Msg
.text:00406BCC                 mov     eax, [ebp+hWnd]
.text:00406BCF                 push    eax             ; hWnd
.text:00406BD0                 call    sub_4012D5      ; return OldWndProc(hWnd, Msg, wParam, lParam);
.text:00406BD5
.text:00406BD5 __END:
.text:00406BD5                 pop     edi
.text:00406BD6                 pop     esi

.text:00406BD7                 pop     ebx
.text:00406BD8                 add     esp, 20h
.text:00406BDE                 pop     ebp
.text:00406BDF                 retn    10h
.text:00406BDF sub_406B80      endp

 

  新的窗口过程已经被插入到了 PE 文件中。接下来再修改 WinMain 中注册窗口类的代码,把新的窗口过程挂钩上去。窗口类的窗口过程是用 VA 提供的绝对地址,修改起来很简单,不需要计算偏移值,把对应的 VA 修改为我们插入的新的窗口过程的 VA (0x00406B80)即可。

 

  同样的,找到 WinMain 函数中,设置窗口过程的那条指定:

  .text:0040121A  mov [ebp+WndClass.lpfnWndProc], offset OldWndProc

 

  机器指令为:

  FA:0000121A: C7 45 B4   XX XX 40 00

 

  来到文件地址 121A h 处,这条指令的后面 4 个字节就是窗口过程的 VA。把它修改为刚刚插入的新的窗口过程的 VA (0x 00406B80) 即可。即把 XX 位置调整为如下,即完成挂钩我们新插入的窗口过程:

 

  FA:0000121A: C7 45 B4   80 6B 40 00

 

  这样整个题目的三部分要求(文本将后两个要求合并)就全部完成了。修改后的 PE 文件运行效果如下:

 

  

 

  【补充】对该条指令 ( .text:0040121A ) 的机器码解读:

   

字段 Prefixes Opcode ModR/M SIB Displacement Immediate
Mod Reg/Opcode R/M Scale Index Base
二进制   1100 0111 01 000 101       1011 0100
16进制 <absent> C7 45 <absent> B4 80 6B 40 00
      +disp8 <meaningless> [EBP]            
说明      MOV [EBP] + disp8  

disp8 = -76

imm32
[EBP] - 4Ch, 0x0040 6B80
Dest Operand, Src Operand
r/m32, imm32
   

WndCls 的地址位于 EBP - 50h 处,

WndCls.lpfnWndProc 的结构体内偏移值 = 4 Bytes;

因此 EBP - 4Ch 相当于 WndCls 的地址 + 4,

即 WndCls.lpfnWndProc 的地址。

翻译到高级语言为: WndCls.lpfnWndProc = 0x00406B80;

imm32 立即数:

窗口过程的入口地址;

VA (已包含 ImageBase);

对此 Opcode (C7)的特定说明: Move imm32 to r/m32 (或 Move imm16 to r/m16).

寻址模式:

Operand1 (destination operand): ModRM: r/m (w);

Operand2 (source operand): imm8/16/32/64;

 

  在参考资料(5)中,C7 操作码的说明是“C7 /0”; 这里 “/0” 表示 ModR/M 字节仅仅使用 r/m (寄存机或主存)操作数。

 

  ModR/M 字节的各个字段含义解释如下:

 

  a). r/m = 101 (二进制), 表示 CH / BP / EBP / MM5 / XMM5 寄存器。

 

  b). Mod = 01 (二进制),表示由 r/m 字段寻址的寄存器 + disp8 。也就是 ModR/M 字节后面将出现一个字节的 Displacement, 作为对此寄存器值的偏移量。(此字节被有符号扩展到寄存器数据尺寸后,作为对寄存器的值的修正。因此,这里 disp8 = B4h = -4C h;

 

  c). Reg/Opcode = 000 (二进制),或者指定一个寄存器号,或者作为操作码的扩展信息,具体用途由主操作码指定。在该指令中此字段没有实际意义。在 OpCode = 7C 时,看起来我们只需要关心 R/M 的值(选择下表所在的某一行),在行内横向移动时改变的是 Reg/Opcode 字段的值,看起来似乎是无关紧要的。但实际证明,CPU 要求这个字节只能取第一列的值(也就是该字段必须为 0 )。下表为来自参考资料(5)(Intel 文档)中的 ModR/M 字节寻址表。在本指令(Opcode = C7)
中,ModR/M 只能在第一列(图中红色方框内的数据,即寻址寄存器为 AL/AX/EAX/MM0/XMM0 )中取值,如果在其他列取值将会引发运行时异常(参见如下实验)。

 

  

 

  我做了一个实验,当改变 ModR/M 字节的值(另其在行内横向移动到第二列),例如将 0x0040121A 处的指令改为 C7 4D B4 XX XX 40 00 时,在 IDA 中可以正常解析出和修改前一样的指令, 但是运行时会提示异常,用 VS2005 调试,显示其反汇编代码,也会出现指令解释错误,如下图所示:

 

  

 

  可以看到在 VS 反汇编器中 0040121A 处指令(原指令为 7 Bytes)无法识别,和后面三个字节(.text:00401221 mov [ebp+WndClass.cbClsExtra], esi)混淆在一起,无法正确识别原有指令(上图中红色方框中的部分),直到 00401224 处,才恢复成正常解释。

 

  SIB 字节:

  主要由 base + index 和 scale + index 寻址模式需要使用。scale 字段指定缩放因子,index 字段指定索引寄存器号。base 字段指定作为基址的寄存器号。

 

  【一些额外补充】

 

  (1)我们可以发现一个有趣现象,在 EXE 类型的 Windows 程序中,传递给 WinMain 的第一个参数 hInstance 是一个 hardcode 的常数:0x0040 0000。也就是说,由于 EXE 是进程的第一个被加载的 Module,并且 linker 对 EXE 的默认 ImageBase 是 0x0040 0000,所以 EXE 自身的 Module 总是位于进程空间的 0x0040 0000 位置。

 

  (2)在资源表中的字符串是以 Unicode 编码存储的,而导入表中的字符串,是以 ASCII 编码存储的。两者分别采用了两种编码,这意味着程序要读取 PE 文件的这两个表,肯定要做编码转换。为什么会这样的?大概原因可能是:

 

  导入表的字符串都是 DLL 和 函数的名称,很明显它们都可以也应该以 ASCII 编码,也就是说,DLL 和 函数名称一律都是英文的(字母+数字),至今我们没有听说过有谁用自己国家民族的语言来为 DLL 和其中的函数取名,所以导入表中的字符串都是 ASCII 编码,这种编码对于存储和网络传输来说比较经济(我们知道 Windows 系统内部已经统一采用 Unicode 字符串,在这种环境下,采用 Unicode 编码的程序比采用多字节编码的程序的运行效率更高,所以 VC 项目的选项应该优先采用 Unicode 编码)。

 

  而资源就不一样了,资源可以由字符串来标识,完全可以用个性化的语言文字来定义,比如说用户把菜单名字取名为“我的上下文菜单”这样的名称,是完全可能也被允许的,所以资源表中的字符串一律采用 Unicode 编码。

 

  (3)由于我的笔记本安装的是 Win7 / 64-bit 版本操作系统,所以在 IDA 中调试时居然是 64 位模式,有一些不适应。

 

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

   http://files.cnblogs.com/hoodlum1980/pediy02_Answer.zip

 

  【参考资料】

  (1)读取文件的导入表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/08/1821778.html。

  (2)读取文件的资源表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/10/1822906.html。

  (3)[VC6] 图像文件格式数据查看器,http://www.cnblogs.com/hoodlum1980/archive/2010/09/05/1818308.html。

  (4)Billy Belceb,《病毒编写教程---Win32篇》( “PE文件头” 章节),翻译:onlyu。来自:看雪论坛精华6 \ 病毒木马技术 \ 病毒编写 \ Billy Belceb 病毒教程Win32篇。

  (5)Intel? 64 and IA-32 Architectures Software Developer’s Manual,Volume 2 (2A, 2B & 2C): "Instruction Set Reference, A-Z", 

    --> CHAPTER 2. INSTRUCTION FORMAT

        \ 2.1  INSTRUCTION FORMAT FOR PROTECTED MODE, REAL-ADDRESS MODE, AND VIRTUAL-8086 MODE

          \ 2.1.3  ModR/M and SIB Bytes;

    --> CHAPTER 3. INSTRUCTION SET REFERENCE, A-L \ MOV-Move;

    --> APPENDIX B. INSTRUCTION FORMATS AND ENCODINGS;