论坛首页 逆向工程技术区 阅读主题

[原创] UPX源码学习和简单修改

233 浏览 24 回复
#1 楼主 2026-06-01 21:09:01
之前一直学习如何脱壳,接触到的第一种壳就是UPX。经过一段脱壳训练后,逐渐对UPX的压缩流程有了兴趣。本文是笔者对UPX源码的学习记录,包括代码学习过程和源码简单修改,希望对大家有所帮助。分析环境:
UPX版本: 3.96 (0ccK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6#2M7s2S2Q4x3V1k6#2M7s2S2Q4x3V1k6J5k6h3I4W2j5i4y4W2M7#2)9J5c8X3c8G2N6$3&6D9L8$3q4V1i4K6u0r3N6U0y4Q4x3X3f1&6y4W2)9J5c8Y4g2H3P5q4)9J5k6o6y4Q4x3X3f1&6y4W2)9J5k6s2y4J5j5#2)9J5k6i4c8S2M7W2)9J5k6i4S2*7i4K6t1&6
linux系统: centOS 7 - Linux localhost.localdomain 3.10.0-862.el7.x86_64 #1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
kali: Linux kali 5.9.0-kali1-amd64 #1 SMP Debian 5.9.1-1kali2 (2020-10-29) x86_64 GNU/Linux
010 Editor版本: v10.0 (64 bit)为了方便调试,笔者只分析了x86-64位ELF文件的加壳流程代码,本文所用程序见结尾文件 demo。代码跟踪过程主要参考文章UPX源码分析——加壳篇,大家有兴趣可详细阅读。这里只介绍大体流程。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处理后,压缩后的文件格式如下。new eheader(64 bytes) (文件头)
+ new pheader(56 bytes) * 3 (程序头表)
+ l_info(12 bytes)
+ p_info(12 bytes)
+ b_info(12 bytes) + compressed block (原程序文件头和程序头表)
+ b_info(12 bytes) + compressed block (第一个类型为PT_LOAD的段中除原程序文件头和程序头表的部分)
+ b_info(12 bytes) + compressed block (第二个类型为PT_LOAD的段)
+ ......
+ fpad8 (8字节对齐)
+ int(4 bytes) (第一个b_info的文件偏移)
+ int(4 bytes) (当前位置的文件偏移,也就是之前所有数据总长度)
+ loader (加载器,也就是脱壳代码)
+ b_info(12 bytes) + compressed block (第一个PT_LOAD和第二个PT_LOAD中间的数据)
+ b_info(12 bytes) + compressed block (第二个PT_LOAD和第三个PT_LOAD中间的数据)
+ ......
+ b_info(12 bytes) + compressed block (最后一个PT_LOAD到文件末尾之间的数据)
+ 00 00 00 00 55 50 58 21 00 00 00 00 (b_info)
+ fpad4 (4字节对齐)
+ PackHeader(32 bytes)
+ int(4 bytes) (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结构体中对section的解析代码去掉,因为UPX压缩后的文件没有相关字段。最后,在file结构体中program_header_table结构体生成之后,添加l_info结构之后的数据解析代码。最终效果如下,红框中是我们添加的新结构。 修改完成的模板见结尾文件 upx.bt。因为文件格式问题,该模板只适用于 demo 文件压缩后的文件。大家有其他需求可以在此基础上自己修改。之前做病毒分析时,碰到了一个无法使用标准

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-275753.htm
#2 2026-06-01 21:09:01
WIN下的,也是一样的么?
#3 2026-06-01 21:09:01
为何没有修改任何文件,直接使用make(安装了camke)编译src得到的upx加壳的文件运行也会出现segmentation fault错误?
#4 2026-06-01 21:09:01
Vantler


你这个问题我也遇到了,答案是那个p_lx_elf里有两个同名函数,分别隶属于不同的类,用于处理不同平台下的loader(一个32位的一个64位的),你改了其中一个但实际用的是另一个就会出现这个问题,得 ...

好的,我一会试试,谢谢~
#5 2026-06-01 21:09:01
weekend~


压缩数据修改,修改这4处,运行不了,会报错。

你这个问题我也遇到了,答案是那个p_lx_elf里有两个同名函数,分别隶属于不同的类,用于处理不同平台下的loader(一个32位的一个64位的),你改了其中一个但实际用的是另一个就会出现这个问题,得俩都改才能保证linux下的使用没有问题
#6 2026-06-01 21:09:01
感谢分享
#7 2026-06-01 21:09:01
压缩数据修改,修改这4处,运行不了,会报错。
#8 2026-06-01 21:09:01
楼主好!我在根据您的文章进行代码修改和复现过程中,发现一个问题,分享出来供参考。问题出现在修改入口点代码的loader部分。文章中loader异或key的位置在汇编码中,根据注释是将upx_main函数的返回值进行异或恢复,但我这里测试这样的操作会导致segmentation fault。我根据注释找到upx_main函数查看,可以获知入口点由前面的do_xmap函数返回,跟入该函数发现它的返回值实际上是ehdr->e_entry + reloc,而我们在加壳部分处理的代码实际上是直接对ehdr->e_entry进行处理的,由于reloc的存在导致返回值异或后会产生异常。
我重新分析了upx_main的流程,ehdr是该函数的第三个参数,推测前面的loader部分只是提取ehdr不会对其进行处理,进入upx_main后可看到ehdr被赋值给xo.buf,随后被传入unpackExtent函数处理。处理的代码我没仔细跟,因为根据函数名和其注释来看,该函数确实会对ehdr进行处理,应该是包含一些入口点相关的内容的,不能硬碰,另外我们在加壳部分的代码是位于packExtent的头部(申请内存并填充后立刻进行了入口点的异或),后续的处理逻辑应该和这个函数对应上了。所以根据前后对应的逻辑,我将异或恢复的代码放在了该函数后:
// ehdr = Uncompress Ehdr and Phdrs
    unpackExtent(&xi2, &xo, f_exp, 0); // never filtered?
    ehdr->e_entry = (ehdr->e_entry ^ 0xdeafdeaf);
然后程序就正常运行不会报错了。
因此提出一个问题,原始版本的代码实际上仅能应用于reloc为0的情况下(do_xmap函数结尾的部分),当其不为0就会导致问题,而我这里就刚刚好踩了这个坑。我这里在简单的逻辑上修复了该问题(pack前加密,unpack后解密,没有经过do_xmap的处理和影响;但这是简单的逻辑层面的,深层上仍应该严格分析packExtent和unpackExtent函数的逻辑来确定相关修改有没有影响),供各位参考
#9 2026-06-01 21:09:01
修改入口点代码的部分,修改.h文件是不行的,或者说只修改那三个部分不行。那个长度常量后面还跟着两个校验值也会变,并且代码长度也不是固定增加5字节,具体取决于编译器生成的bin文件。生成h文件的逻辑在stub/scripts/下,主要是那给bin2h的py脚本。自行编译修改的话可以用一下
#10 2026-06-01 21:09:01
用vmtest师傅提到的工具就可以,根据说明配置好之后,编译的时候类似amd64-linux.elf-fold.h的.h文件都会重新生成的。

最后于 2023-2-9 17:01
被luoye_ATL编辑

,原因:
#11 2026-06-01 21:09:01
vmtest


33eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6#2M7s2S2Q4x3V1k6#2M7s2S2Q4x3X3c8K6N6s2g2T1N6r3!0G2L8s2x3`.
upx的loader是通过这个工具生成的 (也就是你上面修改的 amd64-linux.elf-fold.h )

请教下windows环境下怎么使用这个工具呢
#12 2026-06-01 21:09:01
请教下修改.s的汇编文件后, 怎么编译呢
#13 2026-06-01 21:09:01
dayang

WIN下的,也是一样的么? WIN的加壳流程我不太清楚,得你自己阅读源码了。loader部分的话使用的是另外的文件,在目录upx-3.96/src/stub/src/可以看到

最后于 2023-2-6 14:29
被luoye_ATL编辑

,原因:
#14 2026-06-01 21:09:01
不错的帖子,二次开发,加点VM进去?
#15 2026-06-01 21:09:01
感謝分享!
#16 2026-06-01 21:09:01
vmtest


9b2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6#2M7s2S2Q4x3V1k6#2M7s2S2Q4x3X3c8K6N6s2g2T1N6r3!0G2L8s2x3`.
upx的loader是通过这个工具生成的 (也就是你上面修改的 amd64-linux.elf-fold.h )

是的,我后来就是用这个工具重新编译修改后的代码生成 amd64-linux.elf-fold.h 文件
‹ 上一页 1 2 下一页 ›

请登录后参与讨论

立即登录 注册账号