UPack(Ultimate PE压缩器),是一种PE文件的运行时压缩器,特点是使用独特的方法对PE头进行变形。UPack会使许多的PE分析程序无法正常运行,因此很多恶意代码都是通过UPack进行压缩。
UPack会直接压缩源文件而不会进行备份。压缩后的notepad文件更名为notepad_upack。
使用PEView尝试能否正常查看:
可以看到,新版的PEView也无法正常读取PE头信息,没有读取到NT头和节区头等信息。
Stud_PE可以读取UPack压缩后的PE文件信息。
用Win Hex打开正常的notepad.exe文件:
可以看到,这是典型的PE头格式,依次的顺序为IMAGE_DOS_HEADER、DOS Stub、IMAGE_NT_HEADERS、IMAGE_SECTION_HEADER。
用Win Hex打开使用UPack压缩后的notepad文件:
可以看到,MZ与PE签名离得很近,并且没有了DOS存根,出现了大量字符串,中间夹杂着一些代码。
重叠文件头是压缩器常用的技法,可以把MZ文件头(IMAGE_DOS_HEADER)与PE文件头(IMAGE_NT_HEADERS)重叠在一起,可有效节约文件头的空间。
使用Stud_PE查看MZ文件头(Headers选项卡的Basic HEADERS tree view in hexeditor):
可以看到,该DOS头确实含有e_magic和e_lfanew两个重要成员,其余的成员对程序运行并不会产生影响。
根据PE文件格式规范,IMAGE_NT_HEADERS的起始位置是可变的,即由e_lfanew的值确定,在一般的PE文件中,e_lfanew = DOS头大小(40) + DOS存根大小(可变,VC++的为A0) = E0。这里可看到UPack中e_lfanew的值为10,其并不违反PE规范,使得DOS头与NT头得以整合到一起。
修改IMAGE_FILE_HEADER.SizeOfOptionalHeader的值,可以向文件头插入解码代码。
SizeOfOptionalHeader表示PE文件头中IMAGE_OPTIONAL_HEADER结构体的长度(E0)。UPack将该值修改为148:
增大SizeOfOptionalHeader的值后,就在IMAGE_OPTIONAL_HEADER和IMAGE_SECTION_HEADER之间添加了额外的空间,从而UPack可在此区域添加解码代码。
观察IMAGE_OPTIONAL_HEADER的起始地址:
发现IMAGE_OPTIONAL_HEADER也是紧跟在IMAGE_FILE_HEADER之后,其起始地址为28。
由此可以推算出IMAGE_SECTION_HEADER的起始地址为:28 + 148 = 170。
因此,偏移地址D7~170之间的区域即为额外添加的空间(IMAGE_OPTIONAL_HEADER结尾的位置为D7,具体的分析由下一部分得出):
整个IMAGE_DATA_DIRECTORY结构体数组如图:
NumberOfRvaAndSizes用来指出紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数,正常文件中其值为10,这里UPack修改为了A个:
改变了NumberOfRvaAndSizes的值后,使用Ollydbg打开该文件会弹出如下框,主要是Ollydbg会检查PE文件的NumberOfRvaAndSizes值是否为10:
IMAGE_DATA_DIRECTORY结构体数组的元素个数已经被确定为10,但PE规范将NumberOfRvaAndSizes值作为数组元素的个数,也就是说,IMAGE_DATA_DIRECTORY结构体数组的后6个元素被忽略,即从LOAD_CONFIG项开始(文件偏移D8以后)不再使用,UPack就将这块被忽视的IMAGE_DATA_DIRECTORY区域覆写自己的代码:
由此可以确定IMAGE_OPTIONAL_HEADER结尾的位置为D7。
IMAGE_SECTION_HEADER结构体中,UPack会将自身数据记录到程序运行不需要的项目。
由上述知,IMAGE_SECTION_HEADER的起始地址为170,节区数为3个即有3个IMAGE_SECTION_HEADER结构体,则整个节区头大小为3 * 28 = 78,则IMAGE_SECTION_HEADER结束的偏移为170 + 78 - 1 = 1E7,因此IMAGE_SECTION_HEADER结构体区域为偏移170~1E7:
其中一些结构体成员对程序运行无意义。
UPack的主要特征之一是可以任意重叠PE节区与文件头。
通过Stud_PE查看UPack的IMAGE_SECTION_HEADER:
可以看到,第一节区和第三节区的文件偏移都是10,且大小都是一样的。UPack会对PE文件头、第一节区、第三节区进行重叠。
文件头(包括第一节区和第三节区)区域的大小为200(由第二节区的起始地址得出),第二节区的大小为AE28,占据了文件的大部分区域,原文件压缩于此。
内存中的第一节区的大小为1000,与原文件的Size of Image具有相同的值,即压缩在第二节区中的文件映像会被原样解压缩到第一节区。原notepad.exe拥有3个节区,都被解压到第一节区。
先查看Data Directory Table中的IDT(IMAGE_IMPORT_DESCRIPTOR结构体数组)的地址:
如上,前4个字节为导入表的地址(RVA),后4个字节为导入表的大小(Size)。
在使用Win Hex查看之前,需要进行RVA到RAW的变换。首先需要确定该RVA是属于哪个节区,内存地址271EE在内存中是第三个节区:
则RAW = RVA - VirtualAddress + PointerToRawData = 271EE - 27000 + 0 = 1EE
注意:第三节区的PointerToRawData 的值不是10而是会被强制变换为0。
这里说一下RVA转换为RAW的问题。
各种PE实用程序对UPack无法正常处理的原因在于无法正确地进行RVA到RAW的转换。
以计算EP的文件偏移量RAW为例,UPack的EP是RVA 1018:
由上述知,RVA 1018位于第一节区,第一节区的VirtualAddress 为1000、PointerToRawData为10,则有公式计算RAW如下:
RAW = RVA - VirtualAddress + PointerToRawData = 1018 - 1000 + 10 = 28
用Win Hex到RAW 28处查看:
可以看到,该区域保存的是API名称而并非是代码区域。
出现上述情况的根源在于PointerToRawData。一般而言,指向节区起始地址的文件偏移PointerToRawData值应该是FileAlignment的整数倍。UPack的FileAlignment值为200,其PointerToRawData值应该为0、200、400、600等200的整数倍的值。当PE装载器发现第一节区的PointerToRawData值(10)并非FileAlignment(200)的整数倍时,会强制将其识别为整数倍,即设置PointerToRawData值为0,从而使得UPack得以正常运行。
因而,正确的计算公式应该如下:
RAW = RVA - VirtualAddress + PointerToRawData = 1018 - 1000 + 0 =18
查看该RAW地址的内容:
再到Ollydbg中的EP代码中确认是否一致:
可以看到,是一致的。
接着使用Win Hex查看文件偏移1EE处的内容:
根据PE规范,导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成的数组,最后由一个内容为NULL的结构体结束。上述区域为IMAGE_IMPORT_DESCRIPTOR结构体数组,1EE~201位第一个结构体,后面既无第二个结构体也无NULL结构体,这明显是违反PE规范的。但可以看到在偏移200上方的划线,其表示文件中第三节区的结束,因此运行时偏移在200以下的部分不会映射到第三节区内存中。
当第三节区加载到内存时,文件偏移0~1FF的区域映射到内存的27000~271FF区域,而27200~28000区域(第三节区其余的内存区域)全部填充为NULL。
分析IMAGE_IMPORT_DESCRIPTOR结构体的内容,可知OriginalFirstThunk(INT)为0(RVA),Name为2(RVA),FirstThunk(IAT)为11E8(RVA):
查看Name,可知其属于Header区域,在该区域RAW即RVA,在偏移为2的区域看到KERNEL32.DLL,原本该区域为DOS头不使用的区域,UPack将Import DLL名称写入了该处:
接着查看INT。一般而言,跟踪OriginalFirstThunk(INT)能够查看得到API名称字符串,但此时UPack的OriginalFirstThunk(INT)为0,转而跟踪查看FirstThunk(IAT)也可以查看得到API名称字符串。
IAT的RVA值为11E8,则其RAW = RVA - VirtualAddress + PointerToRawData = 11E8 - 1000 + 0 = 1E8
注意:第一节区的PointerToRawData被强制转换为0而不是10。
用Win Hex打开到1E8处查看:
其中导入了两个API(RVA 28和BE),结束是NULL。
查看这两个API,分别为LoadLibraryA()和getProcAddress():
明确目标为调试查找UPack压缩的notepad_upx.exe文件的OEP。
由于UPack将IMAGE_OPTIONAL_HEADER中的NumberOfRvaAndSizes值设置为了A(默认为10),导致Ollydbg打开notepad_upx.exe文件时会弹出错误消息框:
上述这个错误导致Ollydbg无法转到EP处,而是停留在其他区域:
这是由于Ollydbg的Bug造成的,此时需要强制设置EP。
用Stud_PE查看EP的VirtualAddress:
可以看到,ImageBase为01000000,EP的RVA为1018,经过计算可知EP的RVA为01001018。
Ollydbg中转到该地址,右键>New origin here,再点击确定即可设置新的EP代码:
所有压缩器中都存在解码循环。在调试到这样的解码循环时,在恰当的时候应当跳出条件分支语句以跳出循环。
UPack将压缩后的数据放在第二节区,然后运行解码循环将这些数据解压缩后放到第一节区。
从EP处开始调试:
前两条指令将010011B0地址处的4个字节(0100739D)保存到EAX中,该值是原notepad.exe的OEP。若已经知道该值为OEP,则可直接设置硬件断点再F9运行至OEP处停止即可。
LODS指令是从DS:[ESI]读取DWORD大小的数据放入EAX寄存器中。
接着调试至遇到如下CALL指令(该处函数为decode函数,可从后续调试中不断调用该函数来推断):
此时ESI的值为0101FCCB,进入该地址略微查看一下:
从这部分代码还没看出该函数是干嘛的,接着F7运行进入该函数进行调试,直至遇到如下部分代码:
框中的两处指令为向EDI寄存器所指位置写入内容,其中REP指令将字节内容从ESI移到EDI中,STOS指令将EAX内容保存到EDI中。此时EDI指向第一节区中的地址。这些命令会执行解压缩操作,然后写入内存中。直至EDI的值为01014B5A时,解码循环结束,地址0101FE61处即使解码循环的结束部分。
一般而言,压缩器执行完解码循环后,会根据原文件重新组织IAT。
继续向下调试一段时间后,遇到如下两条CALL指令,分别为调用LoadLibraryA() API和GetProcAddress() API,据此可推断这段代码为重新设置IAT:
最后是RETN指令,返回到0100739D地址,即OEP:
至此,已成功调试查找到了UPack压缩的notepad程序的OEP。