UPX源码分析——加壳篇-飞外

0x00 前言

UPX作为一个跨平台的著名开源压缩壳,随着Android的兴起,许多开发者和公司将其和其变种应用在.so库的加密防护中。虽然针对UPX及其变种的使用和脱壳都有教程可查,但是至少在中文网络里并没有针对其源码的分析。作为一个好奇宝宝,我花了点时间读了一下UPX的源码并梳理了其对ELF文件的处理流程,希望起到抛砖引玉的作用,为感兴趣的研究者和使用者做出一点微不足道的贡献。

0x01 编译一个debug版本的UPX

UPX for Linux的源码位于其git仓库地址https://github.com/upx/upx.git中,使用git工具或者直接在浏览器中打开页面就可以获取其源码文件。为了方便学习,我编译了一个debug版本的UPX4debug

将UPX源码clone到本地Linux机器上后,我们需要修改/src/Makefile中的BUILD_TYPE_DEBUG := 0 为BUILD_TYPE_DEBUG = 1 ,编译出一个带有符号表的debug版本UPX方便后续的调试。此外,UPX依赖UCL算法库,ZLIB算法库和LZMA算法库。在修改完Makefile返回其根目录下输入make all进行编译时,编译器会报出如下错误提示:



按照提示输入命令 git submodule update --init --recursive后成功安装lzma,再次运行make all报错提示依赖项UCL未找到:



UCL库最后一次版本更新为1.03,运行命令
wgethttp://www.oberhumer.com/opensource/ucl/download/ucl-1.03.tar.gzUCL源码,编译安装成功后再次运行make all,报错提示找不到zlib



wgethttp://pkgs.fedoraproject.org/re ... /zlib-1.2.11.tar.xz获取最新版本的zlib库并编译安装成功后再次运行make all编译,编译器未报错,在/src/下发现编译成功的结果upx.out


这个upx.out保留了符号,可以被IDA识别,方便后续进行调试。

0x02 UPX源码结构

UPX根目录包含以下文件及文件夹



其中,README,LICENSE,THANKS等文件的含义显而易见。在/doc中目前包含了elf-to-mem.txt,filter.txt,loader.txt,Makefile,selinux.txt,upx.pod几项。elf-to-mem.txt说明了解压到内存的原理和条件,filter.txt解释了UPX所采用的压缩算法和filter机制,loader.txt告诉开发者如何自定义loader,selinux.txt介绍了SE Linux中对内存匿名映像的权限控制给UPX造成的影响。这部分文件适用于想更加深入了解UPX的研究者和开发者们,在此我就不多做介绍了。

我们在这个项目中感兴趣的UPX源码都在文件夹/src中,进入该文件夹后我们可以发现其源码由文件夹/src/stub,/src/filter,/lzma-sdk和一系列*.h, *.cpp文件构成。其中/src/stub包含了针对不同平台,架构和格式的文件头定义和loader源码,/src/filter是一系列被filter机制和UPX使用的头文件。其余的代码文件主要可以分为负责UPX程序总体的main.cpp,work.cp和packmast.cpp,负责加脱壳类的定义与实现的p_*.h和p_*.cpp,以及其他起到显示,运算等辅助作用的源码文件。我们的分析将会从main.cpp入手,经过work.cpp,最终跳转到对应架构和平台的packer()类中。

0x03 加壳前的准备工作

在上文中我们提到分析将会从main.cpp入手。main.cpp可以视为整个工程的 入口 ,当我们在shell中调用UPX时,main.cpp中的代码将对程序进行初始化工作,包括运行环境检测,参数解析和实现对应的跳转。

我们从位于main.cpp末尾的main函数开始入手。可以看到main函数开头的代码进行了位数检查,参数检查,压缩算法库可用性检查和针对windows平台进行文件名转换。从1516行开始的switch结构针对不同的命令cmd跳转至不同的case其中compress和decompress操作直接break,在1549行注释标注的check options语句块后,1565行出现了一个名为do_files的函数。

[AppleScript]纯文本查看复制代码

从for循环和iname的赋值我们可以看出UPX具有操作多个文件的功能,每个文件都会调用do_one_file()进行操作。

继续深入do_one_file(),前面的代码对文件名进行处理,并打开了两个自定义的文件流fi和fo,fi读取待操作的文件,fo根据参数创建一个临时文件或创建一个文件,这个参数就是-o. 随后函数获取了PackMaster类的实例pm并调用其成员函数进行操作,在这里我们关心的是pm.pack( fo)。这个函数的实现位于packmast.cpp中。

packMaster::pack()非常简单,调用了getPacker()获取一个Packer实例,随后调用Packer的成员函数doPack()进行加壳。跳转到getPacker()发现其调用visitAllPakcers()获取Packer

[AppleScript]纯文本查看复制代码
Packer* PackMaster::visitAllPackers(visit_func_t func, InputFile *f, const options_t *o, void *user)Packer *p = NULL;......// .exe......// atari......// linux kernel...... // linuxif (!o- o_unix.force_execve)...... if ((p = func(new PackLinuxElf64amd(f), user)) != NULL)return p;delete p; p = NULL;if ((p = func(new PackLinuxElf32armLe(f), user)) != NULL)return p;delete p; p = NULL;if ((p = func(new PackLinuxElf32armBe(f), user)) != NULL)return p;delete p; p = NULL;......}// psone......// .sys and .com......// Mach (MacOS X PowerPC)......return NULL;

try_pack()调用了Packer类的成员函数assertPacker(), initPackHeader(), canPack(), updatePackHeader(),在其中起到关键作用的是canPack().通过查看头文件p_lx_elf.h,p_unix.h和packer.h我们发现PackLinuxElf64amd()位于一条以在packer.h中定义的类Packer为基类的继承链尾端,assertPacker(), initPackHeader()和updatePackHeader()的实现均位于文件packer.cpp中,其功能依次为断言一些UPX信息,初始化和更新一个用于加壳的类PackHeader实例ph.

0x04 Packer的适配和初始化

通过对上一节的分析我们得知Packer能否适配成功最终取决于每一个具体Packer类的成员函数canPack().我们以常用的Linux for AMD 64为例,其实现位于p_lx_elf.cpp的PackLinuxElf64amd::canPack()中,而Linux for x86和Linux for ARM的实现均位于PackLinuxElf32::canPack()中,从visitAllPackers()的代码中我们也可以看到UPX当前并不支持64位ARM平台。

我们接下来将以Linux for AMD 64为例进行代码分析,并在每一个小节的末尾补充Linux for x86和Linux for ARM的不同之处。我们从PackLinuxElf64amd::canPack()开始:
PackLinuxElf64amd::canPack()
{
同时具有可写和可执行的限制,作者注释掉了一行修改段属性的代码。

[AppleScript]纯文本查看复制代码
if (Elf64_Ehdr::ET_DYN==get_te16( ehdri.e_type)) {upx_uint64_t const base= get_te64( elfout.phdr[0].p_vaddr);set_te16( elfout.ehdr.e_type, Elf64_Ehdr::ET_DYN);set_te16( elfout.ehdr.e_phnum, 1);set_te64( elfout.ehdr.e_entry,get_te64( elfout.ehdr.e_entry) - base);set_te64( elfout.phdr[0].p_vaddr, get_te64( elfout.phdr[0].p_vaddr) - base);set_te64( elfout.phdr[0].p_paddr, get_te64( elfout.phdr[0].p_paddr) - base);// Strict SELinux (or PaX, grSecurity) disallows PF_W with PF_X//elfout.phdr[0].p_flags |= Elf64_Phdr:F_W;}fo- seek(0, SEEK_SET);if (0!=xct_off) { // shared libraryfo- rewrite( ehdri, sizeof(ehdri));fo- rewrite(phdri, e_phnum * sizeof(*phdri));}else {if (Elf64_Phdr:T_NOTE==get_te64( elfout.phdr[2].p_type)) {upx_uint64_t const reloc = get_te64( elfout.phdr[0].p_vaddr);set_te64( elfout.phdr[2].p_vaddr,reloc + get_te64( elfout.phdr[2].p_vaddr));set_te64( elfout.phdr[2].p_paddr,reloc + get_te64( elfout.phdr[2].p_paddr));fo- rewrite( elfout, sz_elf_hdrs);// FIXME fo- rewrite( elfnote, sizeof(elfnote));}else {fo- rewrite( elfout, sz_elf_hdrs);}fo- rewrite( linfo, sizeof(linfo));}

对父类的pack4(),即位于p_unix.cpp的PackUnix::pack4()进行分析,发现其调用了writePackHeader(),然后写入了一个overlay_offset。从子类的pack4()第一行代码我们得知overlay_offset为新的Ehdr+Phdrs+l_info的长度。我们把目光转向位于p_unix.cpp的

[AppleScript]纯文本查看复制代码
const int hsize = ph.getPackHeaderSize();assert((unsigned)hsize = sizeof(buf));// note: magic constants are always le32set_le32(buf+0, UPX_MAGIC_LE32);set_le32(buf+4, UPX_MAGIC2_LE32);checkPatch(NULL, 0, 0, 0); // resetpatchPackHeader(buf, hsize);checkPatch(NULL, 0, 0, 0); // resetfo- write(buf, hsize);

函数初始化了一个长度为buf[32]的数组,调用在try_pack()中初始化的PackHeader的成员函数getPackHeaderSize(),该函数位于packhead.cpp,通过对版本和架构的判断给出这个PackHeader的长度,对于Linux来说该长度为32
随后函数调用了位于packer.cpp的Packer::patchPackHeader(),而该函数进行检查后最终调用了位于packhead.cpp的PackHeader::putPackHeader()对该数组进行数据填充。

[AppleScript]纯文本查看复制代码
void PackHeader::putPackHeader(upx_bytep p) {assert(get_le32(p) == UPX_MAGIC_LE32);if (get_le32(p + 4) != UPX_MAGIC2_LE32) {// fprintf(stderr, "MAGIC2_LE32: %x %x", get_le32(p+4), UPX_MAGIC2_LE32);throwBadLoader();}int size = 0;int old_chksum = 0;// the new variable length headerif (format 128) {......} else {size = 32;old_chksum = get_packheader_checksum(p, size - 1);set_le32(p + 16, u_len);set_le32(p + 20, c_len);set_le32(p + 24, u_file_size);p[28] = (unsigned char) filter;p[29] = (unsigned char) filter_cto;assert(n_mru == 0 || (n_mru = 2 n_mru = 256));p[30] = (unsigned char) (n_mru ? n_mru - 1 : 0);}set_le32(p + 8, u_adler);set_le32(p + 12, c_adler);} else {......}p[4] = (unsigned char) version;p[5] = (unsigned char) format;p[6] = (unsigned char) method;p[7] = (unsigned char) level;// header_checksumassert(size == getPackHeaderSize());// check old header_checksumif (p[size - 1] != 0) {if (p[size - 1] != old_chksum) {// printf("old_checksum: %d %d", p[size - 1], old_chksum);throwBadLoader();}}// store new header_checksump[size - 1] = get_packheader_checksum(p, size - 1);

对于ARM,PackLinuxElf32::pack4()在重写结构时若文件中有jni_onload,同样需要进行重写。

0x06 Loader的获取

至此,我们已经将UPX对输入文件的加壳流程梳理了一遍,但除了我不打算在这篇文章中讲解的其对代码段实现的Filter压缩机制外,我们还有一个问题没解决 loader从哪来?loader作为加壳后文件运行时的自解密代码,其重要性不言而喻。因此在本节中我们将追查loader的来源和构造流程。


在分析PackUnix::pack3()时,函数通过getLoader()获取了loader的首地址写入文件,这个函数位于packer.cpp,调用了linker- getLoader()获取loader,最后我们在linker.cpp中找到了 真正的 getLoader()

[AppleScript]纯文本查看复制代码

这里的output和outputlen都是linker类的成员变量。显然,我们需要找到对output进行赋值的函数,满足这个条件的函数只有位于linker.cpp中的ElfLinker::addLoader()

[AppleScript]纯文本查看复制代码
memcpy(output + outputlen, section- input, section- size);section- output = output + outputlen;// printf("section added: 0x%04x %3d %s", outputlen, section- size, section- name);outputlen += section- size;......}sect += strlen(sect) + 1;}free(begin);return outputlen;



所以我们的关注点应该在于对addLoader()的调用和对Section这个结构体的赋值。


在pack2()的packExtent()中,函数针对可执行的PT_LOAD段调用了compressWithFilters()进行压缩,在compressWithFilters()的末尾有这么一行函数调用。
buildLoader( best_ft);
函数名已经告诉了我们这个函数想干嘛,并且pack()函数只有在此处compressWithFilters()会被激活,显然这里就是唯一一个生成loader的地方。
让我们更深入地挖掘。对于AMD 64,其实现为p_lx_elf.cpp中的PackLinuxElf64amd::buildLoader()

[AppleScript]纯文本查看复制代码
buildLinuxLoader(stub_amd64_linux_shlib_init, sizeof(stub_amd64_linux_shlib_init),NULL, 0, ft );return;}buildLinuxLoader(stub_amd64_linux_elf_entry, sizeof(stub_amd64_linux_elf_entry),stub_amd64_linux_elf_fold, sizeof(stub_amd64_linux_elf_fold), ft);

发现其对于动态库和可执行文件输入buildLinuxLoader()的参数不同,stub_amd64_linux_shlib_init,stub_amd64_linux_elf_entry,stub_amd64_linux_elf_fold分别位于/src/stub/amd64-linux.shlib-init.h,/src/stub/amd64-linux.elf-entry和/src/stub/amd64-linux.elf.fold中。PackLinuxElf64::buildLinuxLoader()位于文件p_lx_elf.cpp中。
函数开头调用了initLoader(),对于共享库,传入的参数为shlib_init,对于可执行文件,参数为elf_entry,我们先跳过这个函数继续分析。

[AppleScript]纯文本查看复制代码

由于共享库不会传入fold和szfold参数,这个if语句块只对可执行文件有效。这个语句块从elf_fold中提取了ehdr+phdrs+l_info之后的所有内容进行压缩,并在压缩数据块头部补上一个b_info结构体,最后放在名为FOLDEXEC的Section内。

[AppleScript]纯文本查看复制代码
unsigned fold_hdrlen = 0;cprElfHdr1 const *const hf = (cprElfHdr1 const *)fold;fold_hdrlen = umax(0x80, sizeof(hf- ehdr) +get_te16( hf- ehdr.e_phentsize) * get_te16( hf- ehdr.e_phnum) +sizeof(l_info) );h.sz_unc = ((szfold fold_hdrlen) ? 0 : (szfold - fold_hdrlen));h.b_method = (unsigned char) ph.method;h.b_ftid = (unsigned char) ph.filter;h.b_cto8 = (unsigned char) ph.filter_cto;unsigned char const *const uncLoader = fold_hdrlen + fold;h.sz_cpr = MemBuffer::getSizeForCompression(h.sz_unc + (0==h.sz_unc));unsigned char *const cprLoader = New(unsigned char, sizeof(h) + h.sz_cpr);int r = upx_compress(uncLoader, h.sz_unc, sizeof(h) + cprLoader, h.sz_cpr,NULL, ph.method, 10, NULL, NULL );if (r != UPX_E_OK || h.sz_cpr = h.sz_unc)throwInternalError("loader compression failed");unsigned const sz_cpr = h.sz_cpr;set_te32( h.sz_cpr, h.sz_cpr);set_te32( h.sz_unc, h.sz_unc);memcpy(cprLoader, h, sizeof(h));// This adds the definition to the "library", to be used later.linker- addSection("FOLDEXEC", cprLoader, sizeof(h) + sz_cpr, 0);delete [] cprLoader;}else {linker- addSection("FOLDEXEC", "", 0, 0);}......

调用了addStubEntrySections()把sections添加到linker- output中,等待pack3()写入文件。调用relocateLoader()重定位loader。当文件为可执行文件时调用defineSymbols()修改symbols,最后重定位loader
......
addStubEntrySections(ft);


if (0==xct_off)
defineSymbols(ft);// main program only, not for shared lib
relocateLoader();
}
这里提到的sections,symbols和relocateLoader()中提到的relocation records都是什么呢?我们以可执行文件为例,将stub_amd64_linux_elf_entry的内容转换为字符输出,在文件末尾发现了三个表:



sections表中的File off应该指的是对应的段在stub_amd64_linux_elf_entry中的偏移。为了验证我们的想法,我们用IDA打开一个加壳后的AMD64 ELF文件对比ELFMAINX段



除了call指令的地址需要重定位外,其余的opcode都是一致的。对之后的内容进行验证,发现在这个可执行文件的loader中存在ELFMAINX, NRV_HEAD, NRV2E, NRV_TAIL/ELFMAINY ELFMAINZ, LUNMP000, ELFMAINZu几个段,其中在ELFMAINY和ELFMAINZ中插入了其他指令。
回到PackLinuxElf64::buildLinuxLoader()中,我们还剩下initLoader(),addStubEntrySections()两个较为重要的函数没看. Packer::initLoader()位于packer.cpp初始化了一个linker,调用了位于linker.cpp中的ELFLinker::init()

[AppleScript]纯文本查看复制代码
void ElfLinker::init(const void *pdata_v, int plen) {const upx_byte *pdata = (const upx_byte *) pdata_v;if (plen = 16 memcmp(pdata, "UPX#", 4) == 0) {// decompress pre-compressed stub-loader......} else {inputlen = plen;input = new upx_byte[inputlen + 1];if (inputlen)memcpy(input, pdata, inputlen);}input[inputlen] = 0; // NUL terminateoutput = new upx_byte[inputlen ? inputlen : 0x4000];outputlen = 0;if ((int) strlen("Sections:"SYMBOL TABLE:"RELOCATION RECORDS FOR ") inputlen) {int pos = find(input, inputlen, "Sections:", 10);assert(pos != -1);char *psections = (char *) input + pos;char *psymbols = strstr(psections, "SYMBOL TABLE:assert(psymbols != NULL);char *prelocs = strstr(psymbols, "RELOCATION RECORDS FOR ");assert(prelocs != NULL);preprocessSections(psections, psymbols);preprocessSymbols(psymbols, prelocs);preprocessRelocations(prelocs, (char *) input + inputlen);addLoader("*UND*");}

函数通过检测读入内容的标志位判断加壳还是脱壳,如果是加壳则初始化output,最后将全部sections,symbols和relocations读入内存。
再看位于p_lx_elf.cpp的PackLinuxElf::addStubEntrySections()

[AppleScript]纯文本查看复制代码
int all_pages = opt- o_unix.unmap_all_pages | is_big;addLoader("ELFMAINX", NULL);if (hasLoaderSection("ELFMAINXu")) {// brk() trouble if staticall_pages |= (Elf32_Ehdr::EM_ARM==e_machine 0x8000==load_va);addLoader((all_pages ? "LUNMP000" : "LUNMP001"), "ELFMAINXu", NULL);}//addLoader(getDecompressorSections(), NULL);addLoader(( M_IS_NRV2E(ph.method) ? "NRV_HEAD,NRV2E,NRV_TAIL": M_IS_NRV2D(ph.method) ? "NRV_HEAD,NRV2D,NRV_TAIL": M_IS_NRV2B(ph.method) ? "NRV_HEAD,NRV2B,NRV_TAIL": M_IS_LZMA(ph.method) ? "LZMA_ELF00,+80C,LZMA_DEC20,LZMA_DEC30": NULL), NULL);if (hasLoaderSection("CFLUSH"))addLoader("CFLUSH");addLoader("ELFMAINY,IDENTSTR,+40,ELFMAINZ", NULL);if (hasLoaderSection("ELFMAINZu")) {addLoader((all_pages ? "LUNMP000" : "LUNMP001"), "ELFMAINZu", NULL);}addLoader("FOLDEXEC", NULL);

该函数通过标志位和某些sections是否存在将sections拼接到output中,分析函数流程我们发现其添加顺序为ELFMAINX, NRV_HEAD, NRV2E, NRV_TAIL, ELFMAINY, IDENTSTR, ELFMAINZ, LUNMP000, ELFMAINZu, FOLDEXEC。除了IDENTSTR和位于stub_amd64_linux_elf_fold的内容,其余sections的顺序和我们在加壳后样本头部找到的sections块和顺序完全一致。至此我们完成了加壳文件的最后一块拼图 loader的拼接。

0x07 总结


通过对加壳部分源码的分析,我们可以整理出一份Linux ELF通用的UPX加壳文件格式,分为可执行文件和共享库总结如下:




ELF共享库(shared library)被UPX加壳后的文件格式