0%

《逆向工程核心原理》学习笔记10

逆向工程核心原理第十八章

UPack PE文件头分析

使用UPack压缩abexcm1.exe
  • 使用WinUpack 0.39 Final版本压缩abexcm1.exe。直接拖拽abexcm1.exe到WinUpack,按以下勾选参数进行压缩,其中默认是没有清除重定位表的,这里勾选上以便复现原文。将压缩后abexcm1.exe重命名为abexcm1_upack.exe

    image-20211204162922497

  • 下图为使用PE View查看的结果,这里用的是PE View 0.9.9版,这个版本仍无法正常读取PE文件

    image-20211204163155007

使用Stud_PE工具
  • 下面使用Stud_PE查看abexcm1_upack.exe

    image-20211204163506287

  • 可以看到Stud_PE界面要比PE View界面复杂一些,但它拥有其他工具无法比拟的众多独特优点(也能更好地显示Upack)

比较PE文件头
  • 使用HEdit打开abexcm1.exe和abexcm1_upack.exe,再比较它们PE头部分

  • 下图为原abexcm1.exe的PE文件头,其中数据按照IMAGE_DOS_HEADER、DOS Stub、IMAGE_NT_HEADERS、IMAGE_SECTIONS_HEADER顺序排列

    image-20211204164022657

  • 下图为abexcm1_upack.exe的PE文件头,可以看到abexcm1_upack.exe的PE头看上去有些奇怪。MZ与PE签名贴得太近了,并且没有DOS存根,出现了大量字符串,中间好像还夹杂着代码

    image-20211204164721595

分析UPack的PE文件头

重叠文件头
  • 重叠文件头也是其他压缩器经常使用的技法,借助该方法可以把MZ文件头(IMAGE_DOS_HEADER)与PE文件头(IMAGE_NT_HEADERS)巧妙重叠在一起,并可有效节约文件头空间。

  • 下面使用Stud_PE看一下MZ文件头部分,在文件头选项卡中,选择"在十六进制编辑器中以树形结构查看文件头"

    4.55.56

  • MZ文件头(IMAGE_DOS_HEADER)中有以下2个重要成员,其余成员都不怎么重要(对程序运行没有任何意义)

    • e_magic:Magic number = 4D5A("MZ")
    • e_lfanew:File address of new exe header
  • 问题在于,根据PE文件格式规范,IMAGE_NT_HEADERS的起始位置是"可变的"。换言之,IMAGE_NT_HEADERS的起始位置是由e_lfanew的值决定的。一般在一个正常程序中,e_lfanew拥有如下所示的值(不同的构建环境会有不同)

    e_lfanew = MZ文件头大小 (40) + DOS存根大小 (可变: VC++下为A0) = E0

  • UPack中的e_lfanew的值为10,这并不违反PE规范,只是钻了规范的空子。像这样就可以把MZ文件头和PE文件头重叠在一起

IMAGE_FILE_HEADER.SizeOfOptionalHeader
  • 修改IMAFE_FILE_HEADER.SizeOfOptionalHeader的值,可以向文件头插入解码代码。

  • SizeOfOptionalHeader表示PE文件头中紧接在IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_HEADER结构体的长度(E0)。如下图所示,Upack将该值更改为148

    image-20211204171427997

  • 这里会产生一个疑问,IMAGE_OPTIONAL_HEADER是结构体,PE32文件格式中其大小已经被确定为E0。既然如此,PE文件格式的设计者们为何还要另外输入IMAGE_OPTIONAL_HEADER结构体的大小呢?原本的设计意图是,根据PE文件形态分别更换并插入其他IMAGE_OPTIONAL_HEADER形态的结构体。简单来说,就是由于IMAGE_OPTIONAL_HEADER的种类很多,所以需要另外输入结构体大小(例如:64位PE32+的IMAGE_OPTIONAL_HEADER结构体的大小为F0)

  • SizeOfOptionalHeader的另一层含义是确定节区头(IMAGE_SECTION_HEADER)的起始偏移

  • 仅从PE文件头来看,紧接着IMAGE_OPTIONAL_HEADER的好像就是IMAGE_SECTION_HEADER。但实际上(更准确地说),从IMAGE_OPTIONAL_HEADER的起始偏移加上SizeOfOptionalHeader值后的位置开始才是IMAGE_SECTION_HEADER

  • Upack把SizeOfOptionalHeader的值设置为148,比正常值(E0或F0)要更大一些。所以IMAGE_SECTION_HEADER是从偏移170开始的(IMAGE_OPTIONAL_HEADER起始偏移(28)+SizeOfOptionalHeader(148)=170)

  • Upack这么修改的原因是:把PE文件头变形,向文件头适当插入解码需要的代码,增大逆向分析的难度

  • 增大SizeOfOptionalHeader的值后,就在IMAGE_OPTIONAL_HEADER与IMAGE_SECTION_HEADER之间添加了额外空间,Upack就向这个区域添加解码代码。

  • 下面查看该区域,IMAGE_OPTIONAL_HEADER结束位置为D7,IMAGE_SECTION_HEADER的起始位置为170。使用HEdit查看中间区域,如下图所示

    image-20211204202928417

  • 使用Ollydbg查看反汇编代码,如下图所示,这部分信息不是PE头文件中的信息,而是UPack中使用的代码。若PE相关实用工具将其识别为PE文件头信息,就会引发错误,导致程序无法正常运行

    image-20211204203319376

IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes
  • 从IMAGE_OPTIONAL_HEADER结构体中可以看到,其NumberOfRvaAndSizes的值也发生了改变,这样做的目的也是为了向文件头插入自身代码

  • NumberOfRvaAndSizes值用来指出紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数。正常文件中IMAGE_DATA_DIRECTORY数组元素的个数为10h,但在UPack中将其更改为了Ah个。

    image-20211204204439471

  • IMAGE_DATA_DIRECTORY结构体数组元素的个数已经被确定为10,但PE规范将NumberOfRvaSizes值作为数组元素的个数。所以UPack中IMAGE_DATA_DIRECTORY结构体数组的后6个元素被忽略。

  • 下表对IMAGE_DATA_DIRECTORY的各项进行了说明,其中粗体的项如果更改不正确就会引发运行错误

    索 引 内 容 索 引 内 容
    0 EXPORT Directory 8 GLOBALPTR Directory
    1 IMPORT Directory 9 TLS Directory
    2 RESOURCE Directory A LOAD_CONFIG Directory
    3 EXCEPTION Directory B BOUND_IMPORT Directory
    4 SECURITY Directory C IAT Directory
    5 BASERELOC Directory D DELAY_IMPORT Directory
    6 DEBUG Directory E COM_DESCRIPTOR Directory
    7 COPYRIGHT Directory F Reserved Directory
  • Upack将IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes的值更改为A,从LOAD_CONFIG Directory项(文件偏移D8以后)开始不再使用。UPack就在这块被忽视的IMAGE_DATA_DIRECTORY区域中覆写自己的代码。

  • 接下来使用HEdit查看IMAGE_OPTIONAL_HEADER结构体数组区域

    image-20211204214502804

IMAGE_SECTION_HEADER
  • IMAGE_SECTION_HEADER结构体中,Upack会把自身数据记录到程序运行不需要的项目。这与UPack向PE文件头中不使用的区域覆写自身代码与数据的方法是一样的

  • 下面使用HEdit查看IMAGE_SECTION_HEADER结构体,UPack把代码数据存放在IMAGE_SECTION_HEADER结构体的offset to relocations、offset to line numbers、number of relocations、number of line numbers这四个成员中,因为这四个成员本身对程序运行没有任何意义

    image-20211204215046678

重叠节区
  • UPack的主要特征之一就是可以随意重叠PE节区与文件头,下面通过Stud_PE查看UPack的IMAGE_SECTION_HEADER

    image-20211204215948031

  • 从图可以看到,某些部分看上去比较奇怪。首先是第一个节区与第三个节区的文件起始偏移(RawOffset)值都为10。偏移10是文件头区域,UPack中该位置起即为节区部分。

  • 另一奇怪的地方是,第一个节区和第三个节区的文件起始偏移与在文件中的大小(RawSize)是完全一致的。但是,节区内存的起始RVA(VirtualOffset)项与内存大小(VirtualSize)值是彼此不同的。根据PE规范,这样做不会有什么问题。

  • 综合以上两点可知,UPack会对PE文件头、第一个节区、第三个节区进行重叠,三者重叠示意图如下。根据节区头(IMAGE_SECTION_HEADER)中的定义,PE装载器会将文件偏移0~1FF的区域分别映射到3个不同的内存位置(文件头、第一个节区、第三个节区)。也就是说,用相同的文件映像可以分别创建出处于不同位置的、不同大小的内存映像

    image-20211204222232165

  • 文件的头(第一/第三节区)区域的大小为200,第二个节区大小为5A0(占据了文件的大部分区域),原文件(abexcm1.exe)就压缩于此

  • 另一点需要注意的是内存中的第一个节区区域,它的内存尺寸为6000,与原文件(abexcm1.exe)的Size of Image具有相同的值。也就是说,压缩在第二个节区中的文件映像会被原样解压缩到第一个节区(abexcm1的内存映像)。另外,原abexcm1.exe拥有5个节区,它们被解压到一个节区。

RVA to RAW
  • 各种PE实用程序对Upack束手无策的原因就是无法正确进行RVA=>RAW的变换。

  • 首先复习下常规的RVA=>RAW方法

    1
    2
    3
    RAW - PointerToRawData = RVA - VirtualAddress
    RAW = RVA - VirtualAddress + PointerToRawData
    VirtualAddress、PointerToRawData是从RVA所在节区头中获取的值,它们都是已知值
  • 根据上述公式,算一下EP的文件偏移量(RAW)。

    image-20211204223804768

  • UPack的EP是RVA 1018,RVA 1018位于第一个节区,将其代入公式,换算如下

    1
    RAW = 1018 - 1000 + 10 = 28
  • 用HEdit查看文件偏移28处,如下图所示,可以看到RAW 28不是代码区域,而是(ordinal:010B)"LoadLibraryA"字符串区域。

    image-20211204224202028

  • 造成上面这个问题原因在于第一个节区的PointerToRawData值10。一般而言,指向节区开始的文件偏移的PointerToRawData值应该是FileAlignment的整数倍。UPack的FileAlignment为200,故PointerToRawData值应为0、200、400、600等值。PE装载器发现第一个节区的PointerToRawData(10)不是FileAlignment(200)的整数倍时,它会强制将其识别为整数倍(该情况下为0)。这使UPack文件能够正常运行,但是许多PE相关实用程序都会发生错误。正确的RVA=>RAW变换如下

    1
    2
    RAW = 1018 - 1000 + 0 = 18
    PointerToRawData倍识别为0
  • 使用Ollydbg查看相应区域的代码,如下图所示

    image-20211204225058145

导入表(IMAGE_IMPORT_DESCRIPTOR array)
  • UPack的导入表(Import Table)组织结构相当独特(暗藏玄机)

  • 下面使用HEdit查看IMAGE_IMPORT_DESCRIPTOR结构体。首先要从Directory Table中获取IDT(IMAGE_IMPORT_DESCRIPTOR结构体数组)的地址,如下图所示,图中框选的8个字节大小的数据就是指向导入表的IMAGE_IMPORT_DESCRIPTOR结构体。前四个字节为导入表地址(RVA),后面四个字节为导入表的大小(Size)。从图中可知导入表的RVA为F1EE

    image-20211204225454371

  • 使用HEdit查看之前,需要先进行RVA=>RAW变换。首先确定该RVA值属于哪个节区,内存地址F1EE在内存中是第三个节区,参考下图

    image-20211204225842802

  • 进行RVA=>RAW变换,如下所示

    1
    2
    RAW = RVA(F1EE) - VirtualOffset(F000) + RawOffset(0) = 1EE
    这里同样地第三个节区的RawOffset不是10,而是被强制变换为0
  • 使用HEdit查看偏移1EE中的数据,如下图所示,该处就是UPack暗藏玄机的地方

    image-20211204230434611

  • 首先看一下IMAGE_IMPORT_DESCRIPTOR结构体的定义,再继续分析(结构体的大小为20字节)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    union {
    DWORD   Characteristics;

    DWORD OriginalFirstThunk; // INT(Import Name Table) address (RVA)

    };

    DWORD   TimeDateStamp;  

    DWORD   ForwarderChain;

    DWORD   Name; // library name string address (RVA)

    DWORD   FirstThunk; // IAT(Import Address Table) address (RVA)

    } IMAGE_IMPORT_DESCRIPTOR;
  • 根据PE规范,导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成的数组,最后以一个内容为NULL的结构体结束。

  • 文件偏移1EE~201为第一个结构体,其后既不是第二个结构体,也不是(表示导入表结束的)NULL结构体。乍一看这种做法分明是违反PE规范的。但是偏移1FF处为第三个节区的结束,因此文件偏移在200以后的部分不会映射到第三个节区内存。

    image-20211204232357659

  • 第三个节区加载到内存时,文件偏移0~1FF到区域映射到内存的F000~F1FF区域,而(第三个节区其余内存区域)F200~10000区域全部填充为NULL。使用调试器查看相同区域,如下图所示

    image-20211204233029629

  • 准确地说,只映射到0040F1FF,从0040F200开始全部填充为NULL值。

  • 再次返回PE规范的导入表条件,0040F202地址以后出现NULL结构体,这并不算违反PE规范。这就是UPack使用节区的玄机。从文件看导入表好像是损坏了,但是其实它已在内存中准确表现出来。

导入地址表
  • UPack都输入了哪些DLL中的哪些API呢?下面通过分析IAT查看

    偏 移 成 员 RVA
    1EE OriginalFirstThunk(INT) 0
    1FA Name 2
    1FE FirstThunk(IAT) 11E8
  • 首先Name的RVA值为2,它属于Header区域(因为第一个节区是从RVA 1000开始的)

  • Header区域中RVA值与RAW值是一样的,故使用Hex Editor查看文件中偏移(RAW)为2的区域,如下图所示

    image-20211204234101448

  • 在偏移为2的区域中可以看到字符串KERNEL32.DLL。该位置原本是DOS头部分(IAMGE_DOS_HEADER),属于不使用区域,UPack将ImportDLL名称写入该处。得到DLL名称后,再看一下从中导入了哪些API函数

  • 一般而言,跟踪OriginalFirstThunk(INT)能够发现API名称字符串,但是像UPack这样,OriginalFirstThunk(INT)为0时,跟踪FirstThunk(IAT)也是可以的(只要INT、IAT其中一个有API名称字符串即可)。IAT值为11E8,属于第一个节区,故RVA=>RAW换算如下

    1
    RAW = RVA(11E8) - VirtualOffset(1000) + RawOffset(0) = 1E8
  • 用HEdit查看文件偏移1E8,如下图所示,图中高亮部分就是IAT区域,同时也作为INT来使用。也就是说,该处是Name Pointer(RVA)数组,其结束是NULL。

    image-20211204235022350

  • 此外可以看到导入了2个API,分别为RVA 28与BE。RVA位置上存在着导入函数的[ordinal+名称字符串],由于都是header区域,所以RVA与RAW值是一样的

    image-20211204235326466

    image-20211204235354500

  • 从图中可以看到导入的2个API函数分别为LoadLibrary和GetProcAddress,它们在形成原文件的IAT时非常方便,所以普通压缩器也常常导入使用。


逆向工程核心原理第十九章

UPack调试 - 查找OEP

使用x64dbg调试
  • x64dbg是类似于Ollydbg的开源调试器,而且由于Ollydbg早已停止更新,所以x64dbg也是Ollydbg的替代品

  • 下载最新版x64dbg,因为调试的是32位程序,所以运行时应该选择x86dbg.exe来调试

  • 下图为x86dbg运行界面,可以看到软件界面跟Ollydbg非常相似,下面就用x86dbg来调试UPack压缩的abexcm1_upack.exe

    image-20211205112358070

EntryPoint
  • x86dbg默认停住运行的地方不是EntryPoint,但是x86dbg能识别出程序的EntryPoint(UPack的也能正确识别)并设置断点,点击断点选项卡,查看设置的断点

    image-20211205112848841

  • 从图可以看到x86dbg默认在EP上设置了一个一次性的断点,并且准确识别了UPack的EP "00401018"

  • 接着,我们F9让程序运行到EP处,可以看到程序在"00401018"处停下

    image-20211205113109965

解码循环
  • 所有压缩器中都存在解码循环(Decoding Loop)。如果明白压缩/解压算法本身就是由许多条分支语句和循环构成的,那么就能理解为何解码循环看上去如此复杂。

  • 调试这样的循环时,应该适当跳过条件分支语句以跳出某个循环。

  • UPack把压缩后的数据放到第二个节区,再运行解码循环将这些数据解压缩后放到第一个节区。下面从EP代码开始调试,如下图所示

    image-20211205114733058

  • 前两条指令用于从004011B0处读取4个字节,然后保存到EAX寄存器。EAX拥有的值为"00401000",它就是原本abexcm1.exe的OEP(lodsd指令,相当于这2句汇编代码mov eax,[esi],esi=esi+4;该指令从esi所指的地址处读取4字节储存到EAX寄存器)。这里可以直接设置硬件断点,然后F9运行,程序就会在OEP处暂停。

  • 我们的目标时提高调试水平,所以这里继续调试,经过一阵调试后,会出现如下图所示的函数调用代码

    image-20211205170248915

  • 此时ESI的值为00407443,该地址就是decode()函数的地址,后面会反复调用执行该函数。F7跟进查看该函数代码

    image-20211205170523733

  • 仅从这部分来看,还搞不清楚这段代码的用途。继续调试遇到如下图所示的代码

    image-20211205172256256

  • 004075CF与004075D5地址处有"向EDI所指位置写入内容"的指令。此时EDI值指向第一个节区中的地址。也就是说,这些命令会先执行解压缩操作,然后写入实际内存。在004075D6处与004075D9地址处通过CMP/JB指令继续执行循环,直到EDI值为0040604C([ESI+34]=0040604C)

设置IAT
  • 一般而言,压缩器执行完解码循环后会根据原文件重新组织IAT。UPack也有类似的过程,如下图所示,UPack会使用导入的2个函数(LoadLibrary和GetProcAddress),边执行循环边构建原abexcm1.exe的IAT(先获取abexcm1.exe导入函数的实际内存地址,再写入原IAT区域)。

    image-20211205173322197

  • 该过程结束后,由00407627地址处的RETN命令将运行转到OEP处,如下图所示

    image-20211205173733473