UPX的核心加壳代码是upx-3.96/src/p_unix.cpp文件的pack函数。
该函数调用了4个关键函数,分别为pack1、pack2、pack3和pack4,代表了加壳的四个步骤。
pack1函数功能是,写入新文件的elf头,写入程序头表,写入1个初始化的l_info结构。
pack2函数功能是,对所有类型为PT_LOAD的段进行压缩存储。
其中,在对第一个类型为PT_LOAD的段(该块一般包含原文件的文件头和程序头表)进行压缩时,会将该段分为两个部分分别压缩写入。这两部分为:一、原文件的elf头和程序头表;二、该段数据的其他部分。例如,demo文件中第一个PT_LOAD段如下
第一段文件偏移为0,大小为0x5F0。从图中可以看到文件头+程序头表的大小为0x270,这就是需要压缩的第一块数据。第二块数据就是文件偏移为0x270,大小为0x380。
pack3函数的功能是,写入loader,压缩原文件中除PT_LOAD段之外的其余数据并写入,修正程序头表内容
pack4函数的功能是,写入PackHeader和overlay_offset。这里overlay_offset值为p_info字段的文件偏移。本demo中为0xf4。
经过UPX处理后,压缩后的文件格式如下。
neweheader(64bytes)(文件头)+newpheader(56bytes)*3(程序头表)+l_info(12bytes)+p_info(12bytes)+b_info(12bytes)+compressedblock(原程序文件头和程序头表)+b_info(12bytes)+compressedblock(第一个类型为PT_LOAD的段中除原程序文件头和程序头表的部分)+b_info(12bytes)+compressedblock(第二个类型为PT_LOAD的段)+......+fpad8(8字节对齐)+int(4bytes)(第一个b_info的文件偏移)+int(4bytes)(当前位置的文件偏移,也就是之前所有数据总长度)+loader(加载器,也就是脱壳代码)+b_info(12bytes)+compressedblock(第一个PT_LOAD和第二个PT_LOAD中间的数据)+b_info(12bytes)+compressedblock(第二个PT_LOAD和第三个PT_LOAD中间的数据)+......+b_info(12bytes)+compressedblock(最后一个PT_LOAD到文件末尾之间的数据)+000000005550582100000000(b_info)+fpad4(4字节对齐)+PackHeader(32bytes)+int(4bytes)(p_info的文件偏移)
其中,b_info、l_info和p_info是三个结构体。
针对demo文件的loader生成代码在upx-3.96/src/p_lx_elf.cpp文件的PackLinuxElf64::buildLinuxLoader()函数中,loader中各section的相应顺序由函数PackLinuxElf::addStubEntrySections()确定。
除了"FOLDEXEC",其余section的汇编代码在upx-3.96/src/stub/src/amd64-linux.elf-entry.S文件中,编译后的二进制数据在文件upx-3.96/src/stub/amd64-linux.elf-entry.h中。loader直接使用*.h文件中的二进制数据。
最终,demo文件压缩后loader的结构如下
FOLDEXEC节存放的是压缩后的数据,源数据是编译后的二进制数据,存放在upx-3.96/src/stub/amd64-linux.elf-fold.h文件中。编译前的代码分为两部分,一部分是upx-3.96/src/stub/src/amd64-linux.elf-fold.S文件中的汇编代码,一部分是upx-3.96/src/stub/src/amd64-linux.elf-main.c文件中的C代码。该文件核心是upx_main()函数,此函数返回值为解压后程序(原程序)的入口点.
在阅读UPX源码过程中,经常需要对压缩后的文件格式进行分析,以验证自己的猜想。因为笔者经常使用010作为二进制分析工具,遂决定自己写一个010模板。这里是对标准的ELF.bt(V2.5.5)进行了修改。
首先,将上节所述的三个关键结构体加入到模板中,为了增加可读性,在个别字段添加属性值。添加的结构体代码如下:
最后,在file结构体中program_header_table结构体生成之后,添加l_info结构之后的数据解析代码。
最终效果如下,红框中是我们添加的新结构。
修改完成的模板见结尾文件upx.bt。因为文件格式问题,该模板只适用于demo文件压缩后的文件。大家有其他需求可以在此基础上自己修改。
之前做病毒分析时,碰到了一个无法使用标准UPX程序解压的程序。后来通过搜寻资料和学习,了解到有多种方式可实现该效果。基本思路是修改替换程序中的特征字符,常见的修改方法如下:
据此,笔者萌生直接对源码进行修改的想法,所以有以下两个简单尝试。
本次尝试主要是修改源程序的入口点位置,使常规的UPX解压程序后无法快速找到正确的入口点位置。
存储入口点之前,处理该值,这里异或一个常量0xdeafdeaf。文件upx-3.96/src/p_unix.cpp添加如下代码
解压得到入口点之后,处理该值,也是异或常量0xdeafdeaf。这里有两种方式,实现的功能一样。
xoreax,3736067759->35AFDEAFDE->53,175,222,175,222
上面对于入口点的修改虽然成功了,但是压缩后的程序仍然可被解压。而且从原理来看,解压后是可以看到原程序代码执行逻辑的。所以笔者有了第二次代码修改。主要修改思路是,对压缩之前的数据进行处理,然后再进行压缩。这样标准UPX就无法解压我们自己压缩的程序,比只对入口点的修改效果更好。
压缩前修改数据,我们做一个简单的异或处理,这里对所有的数据异或0xe9(十进制为233)文件:upx-3.96/src/compress.cppupx_compress函数修改,核心部分
upx_decompress函数修改,此处修改主要是因为在文件upx-3.96/src/packer.cpp的Packer::compress函数中,会对压缩后的数据进行解压验证,需要保持压缩和解压函数逻辑对称。
upx在加载loader是会对其进行压缩处理,而解压算法已写成汇编代码,修改起来比较麻烦。这里我就对loader数据多进行一次处理,以抵消upx-3.96/src/compress.cpp文件upx_compress函数中添加的异或处理。文件:upx-3.96/src/p_lx_elf.cpp
因为原数据压缩前进行了处理,为保证执行时程序逻辑不变,需要对解压后的数据进行逆处理。下面就是对loader的源码进行修改。文件:upx-3.96/src/stub/src/amd64-linux.elf-main.c
本文主要介绍了三个部分的内容:
WIN的加壳流程我不太清楚,得你自己阅读源码了。loader部分的话使用的是另外的文件,在目录upx-3.96/src/stub/src/可以看到
用vmtest师傅提到的工具就可以,根据说明配置好之后,编译的时候类似amd64-linux.elf-fold.h的.h文件都会重新生成的。