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,运行命令
wget http://www.oberhumer.com/opensource/ucl/download/ucl-1.03.tar.gz 下载UCL源码,编译安装成功后再次运行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的函数。
01 02 03 04 05 06 07 08 09 10 11 | int __acc_cdecl_main main ( int argc , char * argv[] ) { ...... / * check options * / ...... / * start work * / set_term ( stdout ) ; do_files ( i , argc , argv ) ; ...... return exit_code; } |
do_files()的实现位于文件work.cpp中。work.cpp非常简练,只有do_one_file(), unlink_ofile()和do_files()三个函数,而do_files()几乎由for循环和try…catch块构成
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | void do_files ( int i , int argc , char * argv[] ) { ...... for ( ; i < argc; i + + ) { infoHeader ( ) ; const char * iname = argv; char oname[ACC_FN_PATH_MAX + 1 ]; oname[ 0 ] = 0 ; try { do_one_file ( iname , oname ) ; } ...... } ...... } |
从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
1 2 3 4 5 6 7 8 | Packer * PackMaster : : getPacker ( InputFile * f ) { Packer * pp = visitAllPackers ( try_pack , f , opt , f ) ; if ( !pp ) throwUnknownExecutableFormat ( ) ; pp - > assertPacker ( ) ; return pp; } |
跳转到函数visitAllPackers()中,我们发现获取对应平台和架构的Packer的方法其实是一个遍历操作,以输入文件流fi和不同的Packer类作为参数传递给函数指针类型参数try_pack,通过函数try_pack()进行判断。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | Packer * PackMaster : : visitAllPackers ( visit_func_t func , InputFile * f , const options_t * o , void * user ) { Packer * p = NULL; ...... / / .exe ...... / / atari ...... / / linux kernel ...... / / linux if ( !o - > o_unix.force_execve ) { ...... if ( ( p = func ( new PackLinuxElf 64 amd ( f ) , user ) ) ! = NULL ) return p; delete p; p = NULL; if ( ( p = func ( new PackLinuxElf 32 armLe ( f ) , user ) ) ! = NULL ) return p; delete p; p = NULL; if ( ( p = func ( new PackLinuxElf 32 armBe ( f ) , user ) ) ! = NULL ) return p; delete p; p = NULL; ...... } / / psone ...... / / .sys and .com ...... / / Mach ( MacOS X PowerPC ) ...... return NULL; } |
当且仅当其返回true,函数不返回空,此时visitAllPackers()的对应if分支被执行,packer被传递回PackMaster::pack()执行Packer::doPack()开始加壳。
跳转到位于同一个源码文件下的函数try_pack()
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static Packer * try_pack ( Packer * p , void * user ) { if ( p = = NULL ) return NULL; InputFile * f = ( InputFile * ) user; p - > assertPacker ( ) ; try { p - > initPackHeader ( ) ; f - > seek ( 0 , SEEK_SET ) ; if ( p - > canPack ( ) ) { if ( opt - > cmd = = CMD_COMPRESS ) p - > updatePackHeader ( ) ; f - > seek ( 0 , SEEK_SET ) ; return p; } } catch ( const IOException & ) { } catch ( ... ) { delete p; throw; } delete p; 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()
{
第一部分代码,该部分代码主要是对ELF文件头Ehdr和程序运行所需的基本单位Segment的信息Phdr进行校验。代码读取了文件中长度为Ehdr+14*Phdr大小的内容,首先通过checkEhdr()将Ehdr中的字段与预设值进行比较,确定Phdr数量大于1且偏移值正确,随后对Ehdr的大小和偏移进行判定,判定Phdr数量是否大于14,最后确定第一个具有PT_LOAD属性的segment是否覆盖了整个文件的头部。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | union { unsigned char buf[sizeof ( Elf 64 _Ehdr ) + 14 * sizeof ( Elf 64 _Phdr ) ]; / / struct { Elf 64 _Ehdr ehdr; Elf 64 _Phdr phdr; } e; } u; COMPILE_TIME_ASSERT ( sizeof ( u ) < = 1024 ) fi - > readx ( u.buf , sizeof ( u.buf ) ) ; fi - > seek ( 0 , SEEK_SET ) ; Elf 64 _Ehdr const * const ehdr = ( Elf 64 _Ehdr * ) u.buf; / / now check the ELF header if ( checkEhdr ( ehdr ) ! = 0 ) return false ; / / additional requirements for linux / elf 386 if ( get_te 16 ( & ehdr - > e_ehsize ) ! = sizeof ( * ehdr ) ) { throwCantPack ( "invalid Ehdr e_ehsize; try '--force-execve'" ) ; return false ; } if ( e_phoff ! = sizeof ( * ehdr ) ) { / / Phdrs not contiguous with Ehdr throwCantPack ( "non-contiguous Ehdr/Phdr; try '--force-execve'" ) ; return false ; } / / The first PT_LOAD 64 must cover the beginning of the file ( 0 = = p_offset ) . Elf 64 _Phdr const * phdr = phdri; for ( unsigned j = 0 ; j < e_phnum; + + phdr , + + j ) { if ( j > = 14 ) return false ; if ( phdr - > T_LOAD 64 = = get_te 32 ( & phdr - > p_type ) ) { load_va = get_te 64 ( & phdr - > p_vaddr ) ; upx_uint 64 _t file_offset = get_te 64 ( & phdr - > p_offset ) ; if ( ~page_mask & file_offset ) { if ( ( ~page_mask & load_va ) = = file_offset ) { throwCantPack ( "Go-language PT_LOAD: try hemfix.c, or try '--force-execve'" ) ; / / Fixing it inside upx fails because packExtent ( ) reads original file . } else { throwCantPack ( "invalid Phdr p_offset; try '--force-execve'" ) ; } return false ; } exetype = 1 ; break; } } |
第二部分代码,从两段长注释中我们可以看出UPX仅支持对位置无关(PIE)的可执行文件和代码位置无关(PIC)的共享库文件进行加壳处理,然而可执行文件和共享库都(可能)具有ET_DYN属性,理论上没有办法将他们区分开。作者采用了一个巧妙的办法:当文件入口点为
__libc_start_main,__uClibc_main或__uClibc_start_main之一时,说明文件依赖于libc.so.6,该文件为满足PIE的可执行文件。因此该部分通过判定文件是否具有ET_DYN属性,若是则在其重定位表中搜寻以上三个符号,满足则跳转至proceed标号处
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | / / We want to compress position - independent executable ( gcc - pie ) / / main programs , but compressing a shared library must be avoided / / because the result is no longer usable. In theory , there is no way / / to tell them apart : both are just ET_DYN. Also in theory , / / neither the presence nor the absence of any particular symbol name / / can be used to tell them apart; there are counterexamples. / / However , we will use the following heuristic suggested by / / If a ET_DYN has __libc_start_main as a global undefined symbol , / / then the file is a position - independent executable main program / / ( that depends on libc.so. 6 ) and is eligible to be compressed. / / Otherwise ( no __libc_start_main as global undefined ) : skip it . / / Also allow __uClibc_main and __uClibc_start_main . if ( Elf 32 _Ehdr : : ET_DYN = = get_te 16 ( & ehdr - > e_type ) ) { / / The DT_STRTAB has no designated length. Read the whole file . alloc_file_image ( file_image , file_size ) ; fi - > seek ( 0 , SEEK_SET ) ; fi - > readx ( file_image , file_size ) ; memcpy ( & ehdri , ehdr , sizeof ( Elf 64 _Ehdr ) ) ; phdri = ( Elf 64 _Phdr * ) ( ( size_t ) e_phoff + file_image ) ; / / do not free ( ) !! shdri = ( Elf 64 _Shdr const * ) ( ( size_t ) e_shoff + file_image ) ; / / do not free ( ) !! / / sec_strndx = & shdri[ehdr - > e_shstrndx]; / / shstrtab = ( char const * ) ( sec_strndx - > sh_offset + file_image ) ; sec_dynsym = elf_find_section_type ( Elf 64 _Shdr : : SHT_DYNSYM ) ; if ( sec_dynsym ) sec_dynstr = get_te 64 ( & sec_dynsym - > sh_link ) + shdri; int j = e_phnum; phdr = phdri; for ( ; --j>=0; ++phdr) if ( Elf 64 _Phdr : T_DYNAMIC = = get_te 32 ( & phdr - > p_type ) ) { dynseg = ( Elf 64 _Dyn const * ) ( get_te 64 ( & phdr - > p_offset ) + file_image ) ; break; } / / elf_find_dynamic ( ) returns 0 if 0 = = dynseg. dynstr = ( char const * ) elf_find_dynamic ( Elf 64 _Dyn : T_STRTAB ) ; dynsym = ( Elf 64 _Sym const * ) elf_find_dynamic ( Elf 64 _Dyn : T_SYMTAB ) ; / / Modified 2009 -10 -10 to detect a ProgramLinkageTable relocation / / which references the symbol , because DT_GNU_HASH contains only / / defined symbols , and there might be no DT_HASH. Elf 64 _Rela const * rela = ( Elf 64 _Rela const * ) elf_find_dynamic ( Elf 64 _Dyn : T_RELA ) ; Elf 64 _Rela const * jmprela = ( Elf 64 _Rela const * ) elf_find_dynamic ( Elf 64 _Dyn : T_JMPREL ) ; for ( int sz = elf_unsigned_dynamic ( Elf 64 _Dyn : T_PLTRELSZ ) ; 0 < sz; ( sz - = sizeof ( Elf 64 _Rela ) ) , + + jmprela ) { unsigned const symnum = get_te 64 ( & jmprela - > r_info ) > > 32 ; char const * const symnam = get_te 32 ( & dynsym[symnum].st_name ) + dynstr; if ( 0 = = strcmp ( symnam , "__libc_start_main" ) || 0 = = strcmp ( symnam , "__uClibc_main" ) || 0 = = strcmp ( symnam , "__uClibc_start_main" ) ) goto proceed; } / / 2016 -10 -09 DT_JMPREL is no more ( binutils -2.2 6.1 ) ? / / Check the general case , too. for ( int sz = elf_unsigned_dynamic ( Elf 64 _Dyn : T_RELASZ ) ; 0 < sz; ( sz - = sizeof ( Elf 64 _Rela ) ) , + + rela ) { unsigned const symnum = get_te 64 ( & rela - > r_info ) > > 32 ; char const * const symnam = get_te 32 ( & dynsym[symnum].st_name ) + dynstr; if ( 0 = = strcmp ( symnam , "__libc_start_main" ) || 0 = = strcmp ( symnam , "__uClibc_main" ) || 0 = = strcmp ( symnam , "__uClibc_start_main" ) ) goto proceed; } |
第三部分代码,该部分针对第二部分的“漏网之鱼”,此时将待处理的文件视为共享库文件。共享库文件需要满足PIC——文件中不包含代码重定位信息节DT_TEXTREL。此外,文件最靠前的可执行节地址(通常为.init节代码)必须在重定位信息之后,因为此时链接器ld-linux必须在.init节之前进行重定位,UPX加壳后会将入口点设置在.init节上,必须避免破坏ld-linux所需的信息。若判定通过,变量xct_off将记录下.init段地址(必然不等于0)并作为后续pack函数中对待操作文件是否为共享库的判定条件。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | / / Heuristic HACK for shared libraries ( compare Darwin ( MacOS ) Dylib. ) / / If there is an existing DT_INIT , and if everything that the dynamic / / linker ld - linux needs to perform relocations before calling DT_INIT / / resides below the first SHT_EXECINSTR Section in one PT_LOAD , then / / compress from the first executable Section to the end of that PT_LOAD. / / We must not alter anything that ld - linux might touch before it calls / / the DT_INIT function. / / / / Obviously this hack requires that the linker script put pieces / / into good positions when building the original shared library , / / and also requires ld - linux to behave. if ( elf_find_dynamic ( Elf 64 _Dyn : T_INIT ) ) { if ( elf_has_dynamic ( Elf 64 _Dyn : T_TEXTREL ) ) { throwCantPack ( "DT_TEXTREL found; re-compile with -fPIC" ) ; goto abandon; } Elf 64 _Shdr const * shdr = shdri; xct_va = ~ 0 ull; for ( j = e_shnum; --j>=0; ++shdr) { if ( Elf 64 _Shdr : : SHF_EXECINSTR & get_te 32 ( & shdr - > sh_flags ) ) { xct_va = umin 64 ( xct_va , get_te 64 ( & shdr - > sh_addr ) ) ; } } / / Rely on 0 = = elf_unsigned_dynamic ( tag ) if no such tag . upx_uint 64 _t const va_gash = elf_unsigned_dynamic ( Elf 64 _Dyn : T_GNU_HASH ) ; upx_uint 64 _t const va_hash = elf_unsigned_dynamic ( Elf 64 _Dyn : T_HASH ) ; if ( xct_va < va_gash || ( 0 = = va_gash & & xct_va < va_hash ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_STRTAB ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_SYMTAB ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_REL ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_RELA ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_JMPREL ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_VERDEF ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_VERSYM ) || xct_va < elf_unsigned_dynamic ( Elf 64 _Dyn : T_VERNEEDED ) ) { throwCantPack ( "DT_ tag above stub" ) ; goto abandon; } for ( ( shdr = shdri ) , ( j = e_shnum ) ; --j>=0; ++shdr) { upx_uint 64 _t const sh_addr = get_te 64 ( & shdr - > sh_addr ) ; if ( sh_addr = = va_gash || ( sh_addr = = va_hash & & 0 = = va_gash ) ) { shdr = & shdri[get_te 32 ( & shdr - > sh_link ) ]; / / the associated SHT_SYMTAB hatch_off = ( char * ) & ehdri.e_ident[ 11 ] - ( char * ) & ehdri; break; } } ACC_UNUSED ( shdr ) ; xct_off = elf_get_offset_from_address ( xct_va ) ; goto proceed; / / But proper packing depends on checking xct_va. } abandon : return false ; proceed : ; } |
第四部分代码,注释中已经说明其调用的是PackUnix::canPack(),函数实现位于p_unix.cpp中。该函数判断待操作文件是否具有可执行权限,大小是否大于4096,并读取最末尾的一部分数据判定是否加壳。
// XXX Theoretically the following test should be first,
// but PackUnix::canPack() wants 0!=exetype ?
if (!super::canPack())
return false;
assert(exetype == 1);
exetype = 0;
// set options
opt->o_unix.blocksize = blocksize = file_size;
return true;
}
至此,UPX对Packer的适配检查就结束了。通过适配的Packer将会被返回到doPack()中,通过调用其函数pack()进行加壳。
Linux for x86和Linux for ARM版本的PackLinuxElf32::canPack()与前例流程几乎一致。不同的是另一个版本的canPack()在第一部分的末尾额外增加了对PT_NOTE段的长度和偏移检测,并对OS ABI类型做了额外的检测。
0x05 对加壳函数的拆解分析
UPX对所有运行在其支持的架构上的Linux ELF文件都使用同一个pack(),该函数的实现位于p_unix.cpp中。pack()将很多具体操作下放到了各个子类分别实现的pack1(), pack2(), pack3(), pack4()函数中,因此其本体源码并不是很长。通过pack()中的注释我们可以发现其加壳流程大致分为初始化文件头,压缩文件本体,添加loader和修补ELF格式四部分。下面我们以pack1()至pack4()四个函数为分界线进行分析。
(1)对pack1()的分析
01 02 03 04 05 06 07 08 09 10 | void PackUnix : : pack ( OutputFile * fo ) { ...... / / set options ...... / / init compression buffers ...... fi - > seek ( 0 , SEEK_SET ) ; pack 1 ( fo , ft ) ; / / generate Elf header , etc. ...... |
位于pack()开头的这部分代码完成的主要工作为初始化了一些和加壳相关的变量,设置了区块大小并在I/O流中分配内存用于加壳,随后调用了pack1()。对于AMD 64来说其实现位于p_lx_elf.cpp中的PackLinuxElf64::pack1()中。
1 2 3 4 5 6 7 | void PackLinuxElf 64 amd : : pack 1 ( OutputFile * fo , Filter & ft ) { super : : pack 1 ( fo , ft ) ; if ( 0 ! = xct_off ) / / shared library return ; generateElfHdr ( fo , stub_amd 64 _linux_elf_fold , getbrk ( phdri , e_phnum ) ) ; } |
这个函数调用了父类的同名函数PackLinuxElf64::pack1()进行处理,随后当文件不为共享库时调用PackLinuxElf64::generateElfHdr()生成一个ELF头,所有的代码都位于文件p_lx_elf.cpp中。
首先分析PackLinuxElf64::pack1(),函数的前半部分读取了ELF头部Ehdr和程序运行时所需的信息Phdr,将标志为PT_NOTE的段保存下来(虽然好像并没有用到),计算了PT_LOAD段的page_size和page_mask。当文件为共享库时,根据前面canPack()处的说明,为了保证信息不被修改,xct_off前面的所有数据被原封不动写到输出文件中,此外写入了一个描述loader的结构体l_info
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | void PackLinuxElf 64 : : pack 1 ( OutputFile * fo , Filter & / * ft * / ) { ...... page_size = 1 u < < lg 2 _page; page_mask = ~ 0 ull < < lg 2 _page; progid = 0 ; / / getRandomId ( ) ; not useful , so do not clutter if ( 0 ! = xct_off ) { / / shared library fi - > seek ( 0 , SEEK_SET ) ; fi - > readx ( ibuf , xct_off ) ; sz_elf_hdrs = xct_off; fo - > write ( ibuf , xct_off ) ; memset ( & linfo , 0 , sizeof ( linfo ) ) ; fo - > write ( & linfo , sizeof ( linfo ) ) ; } ...... |
l_info结构体的定义位于/src/stub/src/include/linux.h中
1 2 3 4 5 6 7 8 | struct l_info / / 12 - byte trailer in header for loader ( offset 116 ) { uint 32 _t l_checksum; uint 32 _t l_magic; uint 16 _t l_lsize; uint 8 _t l_version; uint 8 _t l_format; } ; |
函数的第二部分是对UPX参数--preserve-build-id的功能实现,使用该参数后将会把.note.gnu.build-id节保存到shdrout中,在pack4()写入输出文件。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | ...... / / only execute if option present if ( opt - > o_unix.preserve_build_id ) { ...... if ( buildid ) { unsigned char * data = New ( unsigned char , buildid - > sh_size ) ; memset ( data , 0 , buildid - > sh_size ) ; fi - > seek ( 0 , SEEK_SET ) ; fi - > seek ( buildid - > sh_offset , SEEK_SET ) ; fi - > readx ( data , buildid - > sh_size ) ; buildid_data = data ; o_elf_shnum = 3 ; memset ( & shdrout , 0 , sizeof ( shdrout ) ) ; / / setup the build - id memcpy ( & shdrout.shdr[ 1 ] , buildid , sizeof ( shdrout.shdr[ 1 ] ) ) ; shdrout.shdr[ 1 ].sh_name = 1 ; / / setup the shstrtab memcpy ( & shdrout.shdr[ 2 ] , sec_strndx , sizeof ( shdrout.shdr[ 2 ] ) ) ; shdrout.shdr[ 2 ].sh_name = 20 ; shdrout.shdr[ 2 ].sh_size = 29 ; / / size of our static shstrtab } ...... } |
从该函数中我们可以看到可执行文件此时并没有获得一个文件头,因此PackLinuxElf64::pack1()对可执行文件调用了PackLinuxElf64::generateElfHdr()生成了一个包含有Ehdr和两个Phdr在内的文件头,其中两个segment均为PT_LOAD类型,第一个具有RX属性,第二个则具有RW属性。同样的,在文件头的最后也追加了一个空l_info结构体。该函数 接受了一个名为stub_amd64_linux_elf_fold的数组作为参数,该数组位于/src/stub/amd64-linux.elf-fold.h中,包含了一个预设置了一些字段的Ehdr,两个Phdr和部分数据。
对于x86和ARM来说,其可执行文件所需的PackLinuxElf32::generateElfHdr()和父类的PackLinuxElf32::pack1()与本例大同小异,仅在OS ABI的设置上有一些细节上的差别。显而易见地,子类的pack1()传递给generateElfHdr()的参数也不相同,可以在/src/stub中相对应的*-fold.h中找到。
(2) 对pack2()的分析
在生成了文件头后,pack()向输出文件追加了一个p_info结构体并填入了文件大小和块大小。
......
p_info hbuf;
set_te32(&hbuf.p_progid, progid);
set_te32(&hbuf.p_filesize, file_size);
set_te32(&hbuf.p_blocksize, blocksize);
fo->write(&hbuf, sizeof(hbuf));
该结构体同样位于/src/stub/src/include/linux.h中
1 2 3 4 5 6 | struct p_info / / 12 - byte packed program header follows stub loader { uint 32 _t p_progid; uint 32 _t p_filesize; uint 32 _t p_blocksize; } ; |
随后调用了pack2()进行文件(实际上是PT_LOAD段)压缩,并在末尾补上一个b_info结构体。
1 2 3 4 5 6 7 8 | / / append the compressed body if ( pack 2 ( fo , ft ) ) { / / write block end marker ( uncompressed size 0 ) b_info hdr; memset ( & hdr , 0 , sizeof ( hdr ) ) ; set_le 32 ( & hdr.sz_cpr , UPX_MAGIC_LE 32 ) ; fo - > write ( & hdr , sizeof ( hdr ) ) ; } ...... |
我们先将目光放在pack2()上,其实现位于p_unix.cpp中的PackLinuxElf64::pack2 ()。将UI实现部分刨去,函数的开头初始化并赋值了三个变量hdr_u_len, total_in和total_out
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | int PackLinuxElf 64 : : pack 2 ( OutputFile * fo , Filter & ft ) { Extent x; unsigned k; bool const is_shlib = ( 0 ! = xct_off ) ; ...... / / compress extents unsigned hdr_u_len = sizeof ( Elf 64 _Ehdr ) + sz_phdrs; unsigned total_in = xct_off - ( is_shlib ? hdr_u_len : 0 ) ; unsigned total_out = xct_off; uip - > ui_pass = 0 ; ft.addvalue = 0 ; ...... |
计算完变量之后筛选出PT_LOAD段,调用packExtent()进行数据打包和输出到文件。根据注释,PowerPC有时候会给.data段打上可执行标志,而打上该标志的段会被认为是代码段,在packExtent()中会调用compressWithFilters()进行压缩。然而对于过小的.data段compressWithFilters()无法压缩。因此在for循环之前初始化了一个nx标志变量记录PT_LOAD下标。当且仅当带有可执行标志的第一个PT_LOAD段适用于compressWithFilters()。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | ...... int nx = 0 ; for ( k = 0 ; k < e_phnum; + + k ) if ( PT_LOAD 64 = = get_te 32 ( & phdri[k].p_type ) ) { ...... if ( 0 = = nx || !is_shlib ) packExtent ( x , total_in , total_out , ( ( 0 = = nx & & ( Elf 64 _Phdr : F_X & get_te 64 ( & phdri[k].p_flags ) ) ) ? & ft : 0 ) , fo , hdr_u_len ) ; else total_in + = x.size; hdr_u_len = 0 ; + + nx; } ...... |
压缩结束后计算已压缩数据和未压缩数据的和是否等于原文件大小。在此之前补齐文件位数为4的倍数,并把长度记录在变量sz_pack2a中,这个变量将会在pack3()被用到。
01 02 03 04 05 06 07 08 09 10 11 12 | sz_pack 2 a = fpad 4 ( fo ) ; / / MATCH 01 ...... / / Accounting only; : : pack 3 will do the compression and output for ( k = 0 ; k < e_phnum; + + k ) { / / total_in + = find_LOAD_gap ( phdri , k , e_phnum ) ; } if ( ( off_t ) total_in ! = file_size ) throwEOFException ( ) ; return 0 ; / / omit end - of - compression bhdr for now } |
可以看到pack2()的核心是compressWithFilters(),该函数的实现位于p_unix.cpp的PackUnix::packExtent()。
当传递进来的参数hdr_u_len不为零时说明文件头(Ehdr+Phdrs)未被压缩,读取到hdr_ibuf中等待压缩。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | void PackUnix : : packExtent ( const Extent & x , unsigned & total_in , unsigned & total_out , Filter * ft , OutputFile * fo , unsigned hdr_u_len ) { ...... if ( hdr_u_len ) { hdr_ibuf.alloc ( hdr_u_len ) ; fi - > seek ( 0 , SEEK_SET ) ; int l = fi - > readx ( hdr_ibuf , hdr_u_len ) ; ( void ) l; } fi - > seek ( x. offset , SEEK_SET ) ; ...... |
进入for循环,循环读取带压缩的数据进行压缩和输出到文件操作
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | ...... for ( off_t rest = x.size; 0 ! = rest ; ) { int const filter_strategy = ft ? getStrategy ( * ft ) : 0 ; int l = fi - > readx ( ibuf , UPX_MIN ( rest , ( off_t ) blocksize ) ) ; if ( l = = 0 ) { break; } rest - = l; / / Note : compression for a block can fail if the / / file is e.g. blocksize + 1 bytes long / / compress ph.c_len = ph.u_len = l; ph.overlap_overhead = 0 ; unsigned end_u_adler = 0 ; ...... |
可执行代码适用于compressWithFilters(),否则适用于compress()
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | ...... if ( ft ) { / / compressWithFilters ( ) updates u_adler _inside_ compress ( ) ; / / that is , AFTER filtering. We want BEFORE filtering , / / so that decompression checks the end - to - end checksum. end_u_adler = upx_adler 32 ( ibuf , ph.u_len , ph.u_adler ) ; ft - > buf_len = l; / / compressWithFilters ( ) requirements? ph.filter = 0 ; ph.filter_cto = 0 ; ft - > id = 0 ; ft - > cto = 0 ; compressWithFilters ( ft , OVERHEAD , NULL_cconf , filter_strategy , 0 , 0 , 0 , hdr_ibuf , hdr_u_len ) ; } else { ( void ) compress ( ibuf , ph.u_len , obuf ) ; / / ignore return value } ...... |
UPX设计的初衷是压缩可执行文件的大小,所以这里对压缩前后的数据进行测试和比较,保留体积较小的部分。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | ...... if ( ph.c_len < ph.u_len ) { const upx_bytep tbuf = NULL; if ( ft = = NULL || ft - > id = = 0 ) tbuf = ibuf; ph.overlap_overhead = OVERHEAD; if ( !testOverlappingDecompression ( obuf , tbuf , ph.overlap_overhead ) ) { / / not in - place compressible ph.c_len = ph.u_len; } } if ( ph.c_len > = ph.u_len ) { / / block is not compressible ph.c_len = ph.u_len; memcpy ( obuf , ibuf , ph.c_len ) ; / / must update checksum of compressed data ph.c_adler = upx_adler 32 ( ibuf , ph.u_len , ph.saved_c_adler ) ; } ...... |
将结果写回文件。若hdr_u_len不为零,调用upx_compress压缩hdr_ibuf,结果写回,然后将hdr_u_len置零防止重复压缩。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | ...... / / write block sizes b_info tmp; if ( hdr_u_len ) { unsigned hdr_c_len = 0 ; MemBuffer hdr_obuf; hdr_obuf.allocForCompression ( hdr_u_len ) ; int r = upx_compress ( hdr_ibuf , hdr_u_len , hdr_obuf , & hdr_c_len , 0 , ph.method , 10 , NULL , NULL ) ; if ( r ! = UPX_E_OK ) throwInternalError ( "header compression failed" ) ; if ( hdr_c_len > = hdr_u_len ) throwInternalError ( "header compression size increase" ) ; ph.saved_u_adler = upx_adler 32 ( hdr_ibuf , hdr_u_len , init_u_adler ) ; ph.saved_c_adler = upx_adler 32 ( hdr_obuf , hdr_c_len , init_c_adler ) ; ph.u_adler = upx_adler 32 ( ibuf , ph.u_len , ph.saved_u_adler ) ; ph.c_adler = upx_adler 32 ( obuf , ph.c_len , ph.saved_c_adler ) ; end_u_adler = ph.u_adler; memset ( & tmp , 0 , sizeof ( tmp ) ) ; set_te 32 ( & tmp.sz_unc , hdr_u_len ) ; set_te 32 ( & tmp.sz_cpr , hdr_c_len ) ; tmp.b_method = ( unsigned char ) ph.method; fo - > write ( & tmp , sizeof ( tmp ) ) ; b_len + = sizeof ( b_info ) ; fo - > write ( hdr_obuf , hdr_c_len ) ; total_out + = hdr_c_len; total_in + = hdr_u_len; hdr_u_len = 0 ; / / compress hdr one time only } memset ( & tmp , 0 , sizeof ( tmp ) ) ; set_te 32 ( & tmp.sz_unc , ph.u_len ) ; set_te 32 ( & tmp.sz_cpr , ph.c_len ) ; if ( ph.c_len < ph.u_len ) { tmp.b_method = ( unsigned char ) ph.method; if ( ft ) { tmp.b_ftid = ( unsigned char ) ft - > id ; tmp.b_cto 8 = ft - > cto; } } fo - > write ( & tmp , sizeof ( tmp ) ) ; b_len + = sizeof ( b_info ) ; if ( ft ) { ph.u_adler = end_u_adler; } / / write compressed data if ( ph.c_len < ph.u_len ) { fo - > write ( obuf , ph.c_len ) ; / / Checks ph.u_adler after decompression , after unfiltering verifyOverlappingDecompression ( ft ) ; } else { fo - > write ( ibuf , ph.u_len ) ; } total_in + = ph.u_len; total_out + = ph.c_len; } } |
注意到命名为tmp的b_info结构体变量先于每一个压缩块头部被赋值并写入。b_info的定义同样位于/src/stub/src/include/linux.h
1 2 3 4 5 6 7 8 | struct b_info { / / 12 - byte header before each compressed block uint 32 _t sz_unc; / / uncompressed_size uint 32 _t sz_cpr; / / compressed_size unsigned char b_method; / / compression algorithm unsigned char b_ftid; / / filter id unsigned char b_cto 8 ; / / filter parameter unsigned char b_unused; } ; |
对于x86和ARM来说,他们相对应的pack2()与本例代码完全相同。
(3) 对pack3()的分析
......
pack3(fo, ft); // append loader
......
pack()中对pack3()的注释写着append loader,即添加loader,而实际上pack3()不仅为输出结果添加了一个loader,也将pack2()未处理的其他数据压缩后输出到结果中,并做了一系列调整。我们先从PackLinuxElf64::pack3()入手。
void PackLinuxElf64::pack3(OutputFile *fo, Filter &ft)
{
函数的第一部分调用了父类的pack3(),即PackLinuxElf::pack3()为文件添加loader,我们把对这个函数的关注暂时先放在脑后。接下来是对pack2()遗漏的文件的剩余部分进行压缩输出,随后写入一个b_info结构体。此时该结构体复用为UPX标志,在sz_cpr字段填入!UPX,其余字段清零。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | super : : pack 3 ( fo , ft ) ; / / loader follows compressed PT_LOADs / / Then compressed gaps ( including debuginfo. ) unsigned total_in = 0 , total_out = 0 ; for ( unsigned k = 0 ; k < e_phnum; + + k ) { Extent x; x.size = find_LOAD_gap ( phdri , k , e_phnum ) ; if ( x.size ) { x. offset = get_te 64 ( & phdri[k].p_offset ) + get_te 64 ( & phdri[k].p_filesz ) ; packExtent ( x , total_in , total_out , 0 , fo ) ; } } / / write block end marker ( uncompressed size 0 ) b_info hdr; memset ( & hdr , 0 , sizeof ( hdr ) ) ; set_le 32 ( & hdr.sz_cpr , UPX_MAGIC_LE 32 ) ; fo - > write ( & hdr , sizeof ( hdr ) ) ; fpad 4 ( fo ) ; |
紧接着函数修改了输出文件中第一个phdr中segment的长度,添加了一个lsize。不难猜出这个lsize为loader长度。
set_te64(&elfout.phdr[0].p_filesz, sz_pack2 + lsize);
set_te64(&elfout.phdr[0].p_memsz, sz_pack2 + lsize);
对于共享库,函数遍历其每个Phdr,当segment具有PT_INTERP属性时挪到最后,具有PT_LOAD属性时调整各项值为loader空出位置,具有PT_DYNAMIC重定位属性时修改DT_INIT项的值,使DT_INIT正确指向原先的.init段地址,并清空Ehdr中关于section的数据,将原.init段地址保存在shoff中。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | if ( 0 ! = xct_off ) { / / shared library Elf 64 _Phdr * phdr = phdri; unsigned off = fo - > st_size ( ) ; unsigned off_init = 0 ; / / where in file upx_uint 64 _t va_init = sz_pack 2 ; / / virtual address upx_uint 64 _t rel = 0 ; upx_uint 64 _t old_dtinit = 0 ; for ( int j = e_phnum; --j>=0; ++phdr) { upx_uint 64 _t const len = get_te 64 ( & phdr - > p_filesz ) ; upx_uint 64 _t const ioff = get_te 64 ( & phdr - > p_offset ) ; upx_uint 64 _t align = get_te 64 ( & phdr - > p_align ) ; unsigned const type = get_te 32 ( & phdr - > p_type ) ; if ( phdr - > T_INTERP = = type ) { / / Rotate to highest position , so it can be lopped / / by decrementing e_phnum. memcpy ( ( unsigned char * ) ibuf , phdr , sizeof (*phdr)); memmove(phdr, 1+phdr, j * sizeof(*phdr)); // overlapping memcpy(&phdr[j], (unsigned char *)ibuf, sizeof(*phdr)); --phdr; set_te16(&ehdri.e_phnum, --e_phnum); continue; } if (phdr->T_LOAD==type) { if (xct_off < ioff) { // Slide up non-first PT_LOAD. // AMD64 chip supports page sizes of 4KiB, 2MiB, and 1GiB; // the operating system chooses one. .p_align typically // is a forward-looking 2MiB. In 2009 Linux chooses 4KiB. // We choose 4KiB to waste less space. If Linux chooses // 2MiB later, then our output will not run. if ((1u<<12) < align) { align = 1u<<12; set_te64(&phdr->p_align, align); } off += (align-1) & (ioff - off); fi->seek(ioff, SEEK_SET); fi->readx(ibuf, len); fo->seek( off, SEEK_SET); fo->write(ibuf, len); rel = off - ioff; set_te64(&phdr->p_offset, rel + ioff); } else { // Change length of first PT_LOAD. va_init += get_te64(&phdr->p_vaddr); set_te64(&phdr->p_filesz, sz_pack2 + lsize); set_te64(&phdr->p_memsz, sz_pack2 + lsize); } continue; // all done with this PT_LOAD } // Compute new offset of &DT_INIT.d_val. if (phdr->T_DYNAMIC==type) { off_init = rel + ioff; fi->seek(ioff, SEEK_SET); fi->read(ibuf, len); Elf64_Dyn *dyn = (Elf64_Dyn *)(void *)ibuf; for (int j2 = len; j2 > 0; ++dyn, j2 -= sizeof(*dyn)) { if (dyn->DT_INIT==get_te64(&dyn->d_tag)) { old_dtinit = dyn->d_val; // copy ONLY, never examined unsigned const t = (unsigned char *)&dyn->d_val - (unsigned char *) ibuf; off_init + = t; break; } } / / fall through to relocate .p_offset } if ( xct_off < ioff ) set_te 64 ( & phdr - > p_offset , rel + ioff ) ; } if ( off_init ) { / / change DT_INIT.d_val fo - > seek ( off_init , SEEK_SET ) ; upx_uint 64 _t word ; set_te 64 ( & word , va_init ) ; fo - > rewrite ( & word , sizeof ( word ) ) ; fo - > seek ( 0 , SEEK_END ) ; } ehdri.e_shnum = 0 ; ehdri.e_shoff = old_dtinit; / / easy to find for unpacking } } |
分析完子类的pack3()后我们将目光转向位于同一个源码文件下的PackLinuxElf::pack3()
这个pack3()的主要工作是为输出文件补充和修正一些字段。
01 02 03 04 05 06 07 08 09 10 | void PackLinuxElf : : pack 3 ( OutputFile * fo , Filter & ft ) { unsigned disp; unsigned const zero = 0 ; unsigned len = sz_pack 2 a; / / after headers and all PT_LOAD unsigned const t = ( 4 & len ) ^ ( ( !!xct_off ) < < 2 ) ; / / 0 or 4 fo - > write ( & zero , t ) ; len + = t; |
这部分代码向输出文件中写入两个数值,第一个为输出文件中第一个b_info的偏移,第二个为截至目前为止的输出文件长度,该数值对于可执行文件来说即为loader偏移。
1 2 3 4 5 6 | set_te 32 ( & disp , 2 * sizeof ( disp ) + len - ( sz_elf_hdrs + sizeof ( p_info ) + sizeof ( l_info ) ) ) ; fo - > write ( & disp , sizeof ( disp ) ) ; / / .e_entry - & first_b_info len + = sizeof ( disp ) ; set_te 32 ( & disp , len ) ; / / distance back to beginning ( detect dynamic reloc ) fo - > write ( & disp , sizeof ( disp ) ) ; len + = sizeof ( disp ) ; |
对于共享库文件,有三个额外的数值会被写入,分别是入口函数地址距第一个PT_LOAD段的偏移,意义未明的hatch_off和.init段地址。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | if ( xct_off ) { / / is_shlib upx_uint 64 _t const firstpc_va = ( jni_onload_va ? jni_onload_va : elf_unsigned_dynamic ( Elf 32 _Dyn : T_INIT ) ) ; set_te 32 ( & disp , firstpc_va - load_va ) ; fo - > write ( & disp , sizeof ( disp ) ) ; len + = sizeof ( disp ) ; set_te 32 ( & disp , hatch_off ) ; fo - > write ( & disp , sizeof ( disp ) ) ; len + = sizeof ( disp ) ; set_te 32 ( & disp , xct_off ) ; fo - > write ( & disp , sizeof ( disp ) ) ; len + = sizeof ( disp ) ; } sz_pack 2 = len; / / 0 mod 8 |
调用父类的pack3()添加decompressor解压缩器,即UPX的loader,随后更新l_info结构体中的size字段。
1 2 3 4 5 6 7 | super : : pack 3 ( fo , ft ) ; / / append the decompressor set_te 16 ( & linfo.l_lsize , up 4 ( / / MATCH 03 : up 4 get_te 16 ( & linfo.l_lsize ) + len - sz_pack 2 a ) ) ; len = fpad 4 ( fo ) ; / / MATCH 03 ACC_UNUSED ( len ) ; } |
最关键的添加loader的函数又加深了一层。。。好吧我们继续看父类的pack3(),即PackUnix::pack3(),位于p_unix.cpp
1 2 3 4 5 6 7 8 | void PackUnix : : pack 3 ( OutputFile * fo , Filter & / * ft * / ) { upx_byte * p = getLoader ( ) ; lsize = getLoaderSize ( ) ; updateLoader ( fo ) ; patchLoaderChecksum ( ) ; fo - > write ( p , lsize ) ; } |
该函数非常简单,调用了位于packer.cpp的Packer::getLoader(),通过linker获取了loader首字节,调用了位于同一个文件下的Packer::getLoaderSize()获取了loader长度,随后调用位于p_lx_elf.cpp的PackLinuxElf64::updateLoader更新入口点。
1 2 3 4 5 | void PackLinuxElf 64 : : updateLoader ( OutputFile * / * fo * / ) { set_te 64 ( & elfout.ehdr.e_entry , sz_pack 2 + get_te 64 ( & elfout.phdr[ 0 ].p_vaddr ) ) ; } |
调用位于p_unix.cpp的PackUnix::patchLoaderChecksum()更新了l_info信息。
01 02 03 04 05 06 07 08 09 10 11 12 | void PackUnix : : patchLoaderChecksum ( ) { unsigned char * const ptr = getLoader ( ) ; l_info * const lp = & linfo; / / checksum for loader; also some PackHeader info lp - > l_magic = UPX_MAGIC_LE 32 ; / / LE 32 always set_te 16 ( & lp - > l_lsize , ( upx_uint 16 _t ) lsize ) ; lp - > l_version = ( unsigned char ) ph. version ; lp - > l_format = ( unsigned char ) ph.format; / / INFO : lp - > l_checksum is currently unused set_te 32 ( & lp - > l_checksum , upx_adler 32 ( ptr , lsize ) ) ; } |
最后将loader写入文件。
对于pack3()来说,x86与AMD64除了loader外并无区别,ARM的PackLinuxElf32::ARM_updateLoader()在入口点的设置上额外加上了_start符号的偏移。
(4) 对pack4()的分析
pack()的最后调用了pack4()对输出文件做最后的修补工作,调用了checckFinalCompressionRadio()检查压缩率。
1 2 3 4 5 6 7 8 | ...... pack 4 ( fo , ft ) ; / / append PackHeader and overlay_offset; update Elf header / / finally check the compression ratio if ( !checkFinalCompressionRatio ( fo ) ) throwNotCompressible ( ) ; } |
对于后者分析的价值不大,我们将重心放在位于p_lx_elf.cpp的PackLinuxElf64::pack4()上
void PackLinuxElf64::pack4(OutputFile *fo, Filter &ft)
{
overlay_offset = sz_elf_hdrs + sizeof(linfo);
当使用了UPX参数--preserve-build-id后保存在pack1()中赋值的数据
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | if ( opt - > o_unix.preserve_build_id ) { / / calc e_shoff here and write shdrout , then o_shstrtab / / NOTE : these are pushed last to ensure nothing is stepped on / / for the UPX structure. unsigned const len = fpad 4 ( fo ) ; set_te 64 ( & elfout.ehdr.e_shoff , len ) ; int const ssize = sizeof ( shdrout ) ; shdrout.shdr[ 2 ].sh_offset = len + ssize; shdrout.shdr[ 1 ].sh_offset = shdrout.shdr[ 2 ].sh_offset + shdrout.shdr[ 2 ].sh_size; fo - > write ( & shdrout , ssize ) ; fo - > write ( o_shstrtab , shdrout.shdr[ 2 ].sh_size ) ; fo - > write ( buildid_data , shdrout.shdr[ 1 ].sh_size ) ; } ...... |
重写了第一个PT_LOAD段的文件大小和内存个映射大小,为了避免受到SE Linux的影响将两者设置为等同,随后调用了父类的pack4()增补数据PackHeader和overlay_offset
1 2 3 4 5 6 7 8 | ...... / / Cannot pre - round .p_memsz. If .p_filesz < .p_memsz , then kernel / / tries to make .bss , which requires PF_W. / / But strict SELinux ( or PaX , grSecurity ) disallows PF_W with PF_X. set_te 64 ( & elfout.phdr[ 0 ].p_filesz , sz_pack 2 + lsize ) ; elfout.phdr[ 0 ].p_memsz = elfout.phdr[ 0 ].p_filesz; super : : pack 4 ( fo , ft ) ; / / write PackHeader and overlay_offset ...... |
重写了ELF文件头,这部分代码用到的数据都已经在pack2()和pack3()中被修改完成了。注意到为了避免SELinux不允许内存页同时具有可写和可执行的限制,作者注释掉了一行修改段属性的代码。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | ...... / / rewrite Elf header if ( Elf 64 _Ehdr : : ET_DYN = = get_te 16 ( & ehdri.e_type ) ) { upx_uint 64 _t const base = get_te 64 ( & elfout.phdr[ 0 ].p_vaddr ) ; set_te 16 ( & elfout.ehdr.e_type , Elf 64 _Ehdr : : ET_DYN ) ; set_te 16 ( & elfout.ehdr.e_phnum , 1 ) ; set_te 64 ( & elfout.ehdr.e_entry , get_te 64 ( & elfout.ehdr.e_entry ) - base ) ; set_te 64 ( & elfout.phdr[ 0 ].p_vaddr , get_te 64 ( & elfout.phdr[ 0 ].p_vaddr ) - base ) ; set_te 64 ( & elfout.phdr[ 0 ].p_paddr , get_te 64 ( & elfout.phdr[ 0 ].p_paddr ) - base ) ; / / Strict SELinux ( or PaX , grSecurity ) disallows PF_W with PF_X / / elfout.phdr[ 0 ].p_flags | = Elf 64 _Phdr : F_W; } fo - > seek ( 0 , SEEK_SET ) ; if ( 0 ! = xct_off ) { / / shared library fo - > rewrite ( & ehdri , sizeof ( ehdri ) ) ; fo - > rewrite ( phdri , e_phnum * sizeof ( * phdri ) ) ; } else { if ( Elf 64 _Phdr : T_NOTE = = get_te 64 ( & elfout.phdr[ 2 ].p_type ) ) { upx_uint 64 _t const reloc = get_te 64 ( & elfout.phdr[ 0 ].p_vaddr ) ; set_te 64 ( & elfout.phdr[ 2 ].p_vaddr , reloc + get_te 64 ( & elfout.phdr[ 2 ].p_vaddr ) ) ; set_te 64 ( & elfout.phdr[ 2 ].p_paddr , reloc + get_te 64 ( & 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的
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | PackUnix : : writePackHeader ( ) void PackUnix : : writePackHeader ( OutputFile * fo ) { unsigned char buf[ 32 ]; memset ( buf , 0 , sizeof ( buf ) ) ; const int hsize = ph.getPackHeaderSize ( ) ; assert ( ( unsigned ) hsize < = sizeof ( buf ) ) ; / / note : magic constants are always le 32 set_le 32 ( buf + 0 , UPX_MAGIC_LE 32 ) ; set_le 32 ( buf + 4 , UPX_MAGIC 2 _LE 32 ) ; checkPatch ( NULL , 0 , 0 , 0 ) ; / / reset patchPackHeader ( buf , hsize ) ; checkPatch ( NULL , 0 , 0 , 0 ) ; / / reset fo - > write ( buf , hsize ) ; } |
函数初始化了一个长度为buf[32]的数组,调用在try_pack()中初始化的PackHeader的成员函数getPackHeaderSize(),该函数位于packhead.cpp,通过对版本和架构的判断给出这个PackHeader的长度,对于Linux来说该长度为32
随后函数调用了位于packer.cpp的Packer::patchPackHeader(),而该函数进行检查后最终调用了位于packhead.cpp的PackHeader::putPackHeader()对该数组进行数据填充。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | void PackHeader : : putPackHeader ( upx_bytep p ) { assert ( get_le 32 ( p ) = = UPX_MAGIC_LE 32 ) ; if ( get_le 32 ( p + 4 ) ! = UPX_MAGIC 2 _LE 32 ) { / / fprintf ( stderr , "MAGIC2_LE32: %x %x\n" , get_le 32 ( p + 4 ) , UPX_MAGIC 2 _LE 32 ) ; throwBadLoader ( ) ; } int size = 0 ; int old_chksum = 0 ; / / the new variable length header if ( format < 128 ) { ...... } else { size = 32 ; old_chksum = get_packheader_checksum ( p , size - 1 ) ; set_le 32 ( p + 16 , u_len ) ; set_le 32 ( p + 20 , c_len ) ; set_le 32 ( 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_le 32 ( p + 8 , u_adler ) ; set_le 32 ( 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_checksum assert ( size = = getPackHeaderSize ( ) ) ; / / check old header_checksum if ( p[size - 1 ] ! = 0 ) { if ( p[size - 1 ] ! = old_chksum ) { / / printf ( "old_checksum: %d %d\n" , p[size - 1 ] , old_chksum ) ; throwBadLoader ( ) ; } } / / store new header_checksum p[size - 1 ] = get_packheader_checksum ( p , size - 1 ) ; } |
根据代码很容易整理出一个Linux通用的结构体
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | struct packHeader { uint 32 _t magic; uint 8 _t version ; uint 8 _t format; uint 8 _t method; uint 8 _t level ; uint 32 _t u_adler; uint 32 _t c_adler; uint 32 _t u_len; uint 32 _t c_len; uint 32 _t u_file_size; uint 8 _t filter; uint 8 _t filter_cto; uint 8 _t n_mru; uint 16 _t checksum; } ; |
对于ARM,PackLinuxElf32::pack4()在重写结构时若文件中有jni_onload,同样需要进行重写。
0x06 Loader的获取
至此,我们已经将UPX对输入文件的加壳流程梳理了一遍,但除了我不打算在这篇文章中讲解的其对代码段实现的Filter压缩机制外,我们还有一个问题没解决——loader从哪来?loader作为加壳后文件运行时的自解密代码,其重要性不言而喻。因此在本节中我们将追查loader的来源和构造流程。
在分析PackUnix::pack3()时,函数通过getLoader()获取了loader的首地址写入文件,这个函数位于packer.cpp,调用了linker->getLoader()获取loader,最后我们在linker.cpp中找到了“真正的”getLoader()
1 2 3 4 5 | upx_byte * ElfLinker : : getLoader ( int * llen ) const { if ( llen ) * llen = outputlen; return output; } |
这里的output和outputlen都是linker类的成员变量。显然,我们需要找到对output进行赋值的函数,满足这个条件的函数只有位于linker.cpp中的ElfLinker::addLoader()
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | int ElfLinker : : addLoader ( const char * sname ) { ...... for ( char * sect = begin; sect < end ; ) { ...... if ( sect[ 0 ] = = ' + ' ) / / alignment { ...... } else { Section * section = findSection ( sect ) ; ...... memcpy ( output + outputlen , section - > input , section - > size ) ; section - > output = output + outputlen; / / printf ( "section added: 0x%04x %3d %s\n" , 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()
01 02 03 04 05 06 07 08 09 10 11 12 | void PackLinuxElf 64 amd : : buildLoader ( const Filter * ft ) { if ( 0 ! = xct_off ) { / / shared library buildLinuxLoader ( stub_amd 64 _linux_shlib_init , sizeof ( stub_amd 64 _linux_shlib_init ) , NULL , 0 , ft ) ; return ; } buildLinuxLoader ( stub_amd 64 _linux_elf_entry , sizeof ( stub_amd 64 _linux_elf_entry ) , stub_amd 64 _linux_elf_fold , sizeof ( stub_amd 64 _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,我们先跳过这个函数继续分析。
01 02 03 04 05 06 07 08 09 10 | void PackLinuxElf 64 : : buildLinuxLoader ( upx_byte const * const proto , unsigned const szproto , upx_byte const * const fold , unsigned const szfold , Filter const * ft ) { initLoader ( proto , szproto ) ; ...... |
由于共享库不会传入fold和szfold参数,这个if语句块只对可执行文件有效。这个语句块从elf_fold中提取了ehdr+phdrs+l_info之后的所有内容进行压缩,并在压缩数据块头部补上一个b_info结构体,最后放在名为FOLDEXEC的Section内。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | ...... if ( 0 < szfold ) { struct b_info h; memset ( & h , 0 , sizeof ( h ) ) ; unsigned fold_hdrlen = 0 ; cprElfHdr 1 const * const hf = ( cprElfHdr 1 const * ) fold; fold_hdrlen = umax ( 0 x 80 , sizeof ( hf - > ehdr ) + get_te 16 ( & hf - > ehdr.e_phentsize ) * get_te 16 ( & 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_cto 8 = ( 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_te 32 ( & h.sz_cpr , h.sz_cpr ) ; set_te 32 ( & 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()
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 terminate output = new upx_byte[inputlen ? inputlen : 0 x 4000 ]; outputlen = 0 ; if ( ( int ) strlen ( "Sections:\n" "SYMBOL TABLE:\n" "RELOCATION RECORDS FOR " ) < inputlen ) { int pos = find ( input , inputlen , "Sections:\n" , 10 ) ; assert ( pos ! = -1 ) ; char * psections = ( char * ) input + pos; char * psymbols = strstr ( psections , "SYMBOL TABLE:\n" ) ; 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()
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void PackLinuxElf : : addStubEntrySections ( Filter const * ) { int all_pages = opt - > o_unix.unmap_all_pages | is_big; addLoader ( "ELFMAINX" , NULL ) ; if ( hasLoaderSection ( "ELFMAINXu" ) ) { / / brk ( ) trouble if static all_pages | = ( Elf 32 _Ehdr : : EM_ARM = = e_machine & & 0 x 8000 = = load_va ) ; addLoader ( ( all_pages ? "LUNMP000" : "LUNMP001" ) , "ELFMAINXu" , NULL ) ; } / / addLoader ( getDecompressorSections ( ) , NULL ) ; addLoader ( ( M_IS_NRV 2 E ( ph.method ) ? "NRV_HEAD,NRV2E,NRV_TAIL" : M_IS_NRV 2 D ( ph.method ) ? "NRV_HEAD,NRV2D,NRV_TAIL" : M_IS_NRV 2 B ( 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加壳后的文件格式