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进行编译时,编译器会报出如下错误提示:
093244uobnf2db6tt71tfd.png

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

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

wget http://pkgs.fedoraproject.org/re ... /zlib-1.2.11.tar.xz 获取最新版本的zlib库并编译安装成功后再次运行make all编译,编译器未报错,在/src/下发现编译成功的结果upx.out
093246jyyjq0ivjdck13bi.png
这个upx.out保留了符号,可以被IDA识别,方便后续进行调试。

0x02 UPX源码结构

UPX根目录包含以下文件及文件夹
093248t4e7gxzc78c24hx0.png

其中,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.  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

   // Peter S. Mazinger <[email]ps.m@gmx.net[/email]> September 2005:

   // 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 (Elf32_Ehdr::ET_DYN==get_te16(&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(Elf64_Ehdr));

       phdri= (Elf64_Phdr       *)((size_t)e_phoff + file_image);  // do not free() !!

       
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值