目录

upx压缩壳源码分析的一些关键点


目录

最近工作需要研究了一下upx针对可执行文件的压缩算法,就顺便理了一下代码的框架,下面大概的给一个分析过程。

  • 编译调试

upx还是需要在32位linux下编译调试比较方便,windows下还是免得折腾了,MinGW不好用,一堆依赖的环境也很难搞定。

首先在github上下载upx源码,并且在源码的src目录下建一个lzma-sdk目录,再git clone下载lzmasdk下来

1
git clone https://github.com/upx/upx-lzma-sdk.git

也可以在在源码目录下用如下命令

1
git submodule update --init --recursive

根据作者的文档介绍,还需要一个UCL库,在如下地址 http://www.oberhumer.com/opensource/ucl/

下载并解压,(比如解压在当前用户的local/src/ucl-1.03目录下)在编译upx前需要把库编译好并设置一下环境变量或预编译变量

但是ucl直接用configure生成makefile编译会有问题,这个库需要用到clang来编译,命令

1
2
3
./configure --prefix=/home/[user]/ucl CC=clang

make

下面就可以编译upx了,可能依赖一个zlib-dev可能机器上不存在,用以下包替代

1
sudo apt install zlib1g-dev

编译调试版本

1
make all UPX_UCLDIR=/home/[user]/src/ucl-1.03 UPX_LZMADIR=./src/lzma-sdk BUILD_TYPE_DEBUG=1

到这里就可以成功编译upx.out文件并可以用gdb方便调试,作者喜欢用visualcode来调试代码,具体怎么配置一堆json之类的请查文档,题外话就不详细说了.

  • 源码分析

这个是一个顺序的流程,所以从main函数开始分析,代码开始主要做一些库的初始化工作及根据参数等设置好一个全局的opt变量.

opt->cmd就是一个主要的命令参数,如果参数中未设置,默认为设置为CMD_COMPRESS压缩文件的方式.

处理文件在关键点在 do_files —> dofile InputFile是输入文件对象 OutPutFile是输出文件对象

以下代码片断是输入文件构造一个PackMaster对象,pack方法再传入输出文件对象

1
2
3
4
5
6
7
    InputFile fi;
    fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR);
    OutputFile fo;

    PackMaster pm(&fi, opt);
    if (opt->cmd == CMD_COMPRESS)
    pm.pack(&fo);

pack这个方法中

getPacker —> visitAllPackers 会根据文件来找对应的Packer厂类下的子类.最终调用子类的doPack,比如我拿windows32位可执行文件来压缩,就会对应到 PackW32Pe::doPack —> PeFile::pack0

下面主要分析windows可执行文件压缩的一些关键点,这里面有些算法非常有意思. pe文件涉及一些预处理,比如处理导入导出符号表,TLS,重定位表,代码节优化等,涉及很多内容不一一详细分析,PE结构资料太多我也懒得再介绍了,需要了解请自行搜索资料,下面挑两个内容来讲

  1. 重定位表的处理

在processRelocs中,有如下处理流程

  • class Reloc为重定位表处理类,在构造函数中会遍历重定位结构中的修正RVA中的type类型并存在counts数组中(实际上应该所有的类型都为3)
  • 如果当前文件需要删除重定位表并且为非dll文件或者根本没有重定位表,则干掉重定位表
  • 去掉重复的重定位修正项
  • 将所有的重定位偏移处(这里是长跳转或者长CALL后的偏移,例如代码 0xFF 0x15 Dest)处修正为 Dest - ih.imagebase - rvamin rvamin是节表最小地址,imagebase是加载地址,这里只处理type3类型的重定位项.
  • 重定位的优化,这个主要是翻转Dest提高冗余度提升压缩率的一个算法,具体看代码.

(EOF)

  1. 短跳短call代码优化算法(E8/E9优化大法)

代码流程是以下的这样的 callCompressWithFilters —> compressWithFilters —> ft.filter —> (*fe->do_filter)(this) —>f_cto32_e8e9_bswap_le 关键算法在f_cto32_e8e9_bswap_le中,这个是个宏编译自动生成的调用,代码在cto.h中 这里也是提高压缩率的一个算法,算法不太好讲代码细节,这里讲一下思想,(直接拿作者写的例子来解说了)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
00025970: E877410600                     calln     FatalError
00025975: 8B414C                         mov       eax,[ecx+4C]
00025978: 85C0                           test      eax,eax
0002597A: 7419                           je        file:00025995
0002597C: 85F6                           test      esi,esi
0002597E: 7504                           jne       file:00025984
00025980: 89C6                           mov       esi,eax
00025982: EB11                           jmps      file:00025995
00025984: 39C6                           cmp       esi,eax
00025986: 740D                           je        file:00025995
00025988: 83C4F4                         add (d)   esp,F4
0002598B: 68A0A91608                     push      0816A9A0
00025990: E857410600                     calln     FatalError
00025995: FF45F4                         inc       [ebp-0C]

我们都知道,如果要想提高压缩率,就得提高压缩数据的冗余度,对于如上的二进制,我们可以用特定的规则置换提高冗余度, 如上两个call二进制码可以做如下的置换 相对的偏移量换算成实际的地址 如E8(POS) 实际地址 = POS + 当前地址 + 5(实际运算中只要一个固定值就可以)

0x64177 + 0x25970 = 0x89AE7

0x64157 + 0x25990 = 0x89AE7

对应来说我们就得到了下面的重复数据

E8 E79A0800 8B

E8 E79A0800 FF

实际算法上还需要再把E8后四位倒转一下,这样对应不同的短call地址至少有E8 00这样的数据是大量重复的

对于这样的算法如何还原也是一个麻烦的问题

有大量0xE8二进制后并不是一个真实的短call的代码,但是如果按此算法不做区分的压缩会有不可预知的压缩率的问题,所以判断是否是真实短跳需要判断一下E8后的地址范围,一般在代码节内是正常短跳的概率比较大,那么我们就还需要一个标志来标识记录下我们转换过的call

upx的处理算法是这么做的

  • 搜索代码节的0xE8,按后的偏移算出范围来判断是否是真实短call
  • 假设代码节不超过0xffffff(16MB)的范围,这样我们可以保持一个最高字节做flag
  • 搜索代码中非真实短call的后一个字节记录做标识,找到一个全局代码中都没有出现过的字节做为字节标识
  • 除了短call(0xE8),短jmp(0xE9)也是一样可以用此方式处理
  • 如果你看的有点糊涂,结合代码再多看几遍就理解了^_^

其他upx各种流程都请结合代码再分析调试,关键点就分析到这里.