UPX源码分析——加壳篇

本文属于i春秋原创文章现金奖励计划,未经许可严禁转载。

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


wget http://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的函数。

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块构成
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
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()进行判断。
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 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;
}


当且仅当其返回true,函数不返回空,此时visitAllPackers()的对应if分支被执行,packer被传递回PackMaster::pack()执行Packer::doPack()开始加壳。
跳转到位于同一个源码文件下的函数try_pack()
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是否覆盖了整个文件的头部。
union {
     unsigned char buf[sizeof(Elf64_Ehdr) + 14*sizeof(Elf64_Phdr)];
     //struct { Elf64_Ehdr ehdr; Elf64_Phdr phdr; } e;
 } u;
 COMPILE_TIME_ASSERT(sizeof(u) <= 1024)
 
 
 fi->readx(u.buf, sizeof(u.buf));
 fi->seek(0, SEEK_SET);
 Elf64_Ehdr const *const ehdr = (Elf64_Ehdr *) u.buf;
 
 
 // now check the ELF header
 if (checkEhdr(ehdr) != 0)
     return false;
 
 
 // additional requirements for linux/elf386
 if (get_te16(&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_LOAD64 must cover the beginning of the file (0==p_offset).
 Elf64_Phdr const *phdr = phdri;
 for (unsigned j=0; j < e_phnum; ++phdr, ++j) {
     if (j >= 14)
         return false;
     if (phdr->T_LOAD64 == get_te32(&phdr->p_type)) {
         load_va = get_te64(&phdr->p_vaddr);
         upx_uint64_t file_offset = get_te64(&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标号处

   
// 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.  Als
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值