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,執行命令
這個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. 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][email protected][/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() !!
shdri= (Elf64_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(Elf64_Shdr::SHT_DYNSYM);
if (sec_dynsym)
sec_dynstr = get_te64(&sec_dynsym->sh_link) + shdri;
int j= e_phnum;
phdr= phdri;
for (; --j>=0; ++phdr)
if (Elf64_Phdr:T_DYNAMIC==get_te32(&phdr->p_type)) {
dynseg= (Elf64_Dyn const *)(get_te64(&phdr->p_offset) + file_image);
break;
}
// elf_find_dynamic() returns 0 if 0==dynseg.
dynstr= (char const *)elf_find_dynamic(Elf64_Dyn:T_STRTAB);
dynsym= (Elf64_Sym const *)elf_find_dynamic(Elf64_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.
Elf64_Rela const *
rela= (Elf64_Rela const *)elf_find_dynamic(Elf64_Dyn:T_RELA);
Elf64_Rela const *
jmprela= (Elf64_Rela const *)elf_find_dynamic(Elf64_Dyn:T_JMPREL);
for ( int sz = elf_unsigned_dynamic(Elf64_Dyn:T_PLTRELSZ);
0 < sz;
(sz -= sizeof(Elf64_Rela)), ++jmprela
) {
unsigned const symnum = get_te64(&jmprela->r_info) >> 32;
char const *const symnam = get_te32(&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.26.1)?
// Check the general case, too.
for ( int sz = elf_unsigned_dynamic(Elf64_Dyn:T_RELASZ);
0 < sz;
(sz -= sizeof(Elf64_Rela)), ++rela
) {
unsigned const symnum = get_te64(&rela->r_info) >> 32;
char const *const symnam = get_te32(&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函式中對待操作檔案是否為共享庫的判定條件。
// 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(Elf64_Dyn:T_INIT)) {
if (elf_has_dynamic(Elf64_Dyn:T_TEXTREL)) {
throwCantPack("DT_TEXTREL found; re-compile with -fPIC");
goto abandon;
}
Elf64_Shdr const *shdr = shdri;
xct_va = ~0ull;
for (j= e_shnum; --j>=0; ++shdr) {
if (Elf64_Shdr::SHF_EXECINSTR & get_te32(&shdr->sh_flags)) {
xct_va = umin64(xct_va, get_te64(&shdr->sh_addr));
}
}
// Rely on 0==elf_unsigned_dynamic(tag) if no such tag.
upx_uint64_t const va_gash = elf_unsigned_dynamic(Elf64_Dyn:T_GNU_HASH);
upx_uint64_t const va_hash = elf_unsigned_dynamic(Elf64_Dyn:T_HASH);
if (xct_va < va_gash || (0==va_gash && xct_va < va_hash)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_STRTAB)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_SYMTAB)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_REL)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_RELA)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_JMPREL)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_VERDEF)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_VERSYM)
|| xct_va < elf_unsigned_dynamic(Elf64_Dyn:T_VERNEEDED) ) {
throwCantPack("DT_ tag above stub");
goto abandon;
}
for ((shdr= shdri), (j= e_shnum); --j>=0; ++shdr) {
upx_uint64_t const sh_addr = get_te64(&shdr->sh_addr);
if ( sh_addr==va_gash
|| (sh_addr==va_hash && 0==va_gash) ) {
shdr= &shdri[get_te32(&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()的分析
void PackUnix::pack(OutputFile *fo)
{
......
// set options
......
// init compression buffers
......
fi->seek(0, SEEK_SET);
pack1(fo, ft); // generate Elf header, etc.
......
位於pack()開頭的這部分程式碼完成的主要工作為初始化了一些和加殼相關的變數,設定了區塊大小並在I/O流中分配記憶體用於加殼,隨後呼叫了pack1()。對於AMD 64來說其實現位於p_lx_elf.cpp中的PackLinuxElf64::pack1()中。
void PackLinuxElf64amd::pack1(OutputFile *fo, Filter &ft)
{
super::pack1(fo, ft);
if (0!=xct_off) // shared library
return;
generateElfHdr(fo, stub_amd64_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
void PackLinuxElf64::pack1(OutputFile *fo, Filter & /*ft*/)
{
......
page_size = 1u <<lg2_page;
page_mask = ~0ull<<lg2_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中
struct l_info // 12-byte trailer in header for loader (offset 116)
{
uint32_t l_checksum;
uint32_t l_magic;
uint16_t l_lsize;
uint8_t l_version;
uint8_t l_format;
};
函式的第二部分是對UPX引數--preserve-build-id的功能實現,使用該引數後將會把.note.gnu.build-id節儲存到shdrout中,在pack4()寫入輸出檔案。
......
// 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中
struct p_info // 12-byte packed program header follows stub loader
{
uint32_t p_progid;
uint32_t p_filesize;
uint32_t p_blocksize;
};
隨後呼叫了pack2()進行檔案(實際上是PT_LOAD段)壓縮,並在末尾補上一個b_info結構體。
// append the compressed body
if (pack2(fo, ft)) {
// write block end marker (uncompressed size 0)
b_info hdr; memset(&hdr, 0, sizeof(hdr));
set_le32(&hdr.sz_cpr, UPX_MAGIC_LE32);
fo->write(&hdr, sizeof(hdr));
}
......
我們先將目光放在pack2()上,其實現位於p_unix.cpp中的PackLinuxElf64::pack2 ()。將UI實現部分刨去,函式的開頭初始化並賦值了三個變數hdr_u_len, total_in和total_out
int PackLinuxElf64::pack2(OutputFile *fo, Filter &ft)
{
Extent x;
unsigned k;
bool const is_shlib = (0!=xct_off);
......
// compress extents
unsigned hdr_u_len = sizeof(Elf64_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()。
......
int nx = 0;
for (k = 0; k < e_phnum; ++k) if (PT_LOAD64==get_te32(&phdri[k].p_type)) {
......
if (0==nx || !is_shlib)
packExtent(x, total_in, total_out,
((0==nx && (Elf64_Phdr:F_X & get_te64(&phdri[k].p_flags)))
? &ft : 0 ), fo, hdr_u_len);
else
total_in += x.size;
hdr_u_len = 0;
++nx;
}
......
壓縮結束後計算已壓縮資料和未壓縮資料的和是否等於原檔案大小。在此之前補齊檔案位數為4的倍數,並把長度記錄在變數sz_pack2a中,這個變數將會在pack3()被用到。
sz_pack2a = fpad4(fo); // MATCH01
......
// Accounting only; ::pack3 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中等待壓縮。
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迴圈,迴圈讀取帶壓縮的資料進行壓縮和輸出到檔案操作
......
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()
......
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_adler32(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設計的初衷是壓縮可執行檔案的大小,所以這裡對壓縮前後的資料進行測試和比較,保留體積較小的部分。
......
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_adler32(ibuf, ph.u_len, ph.saved_c_adler);
}
......
將結果寫回檔案。若hdr_u_len不為零,呼叫upx_compress壓縮hdr_ibuf,結果寫回,然後將hdr_u_len置零防止重複壓縮。
......
// 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_adler32(hdr_ibuf, hdr_u_len, init_u_adler);
ph.saved_c_adler = upx_adler32(hdr_obuf, hdr_c_len, init_c_adler);
ph.u_adler = upx_adler32(ibuf, ph.u_len, ph.saved_u_adler);
ph.c_adler = upx_adler32(obuf, ph.c_len, ph.saved_c_adler);
end_u_adler = ph.u_adler;
memset(&tmp, 0, sizeof(tmp));
set_te32(&tmp.sz_unc, hdr_u_len);
set_te32(&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_te32(&tmp.sz_unc, ph.u_len);
set_te32(&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_cto8 = 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
struct b_info { // 12-byte header before each compressed block
uint32_t sz_unc; // uncompressed_size
uint32_t sz_cpr; // compressed_size
unsigned char b_method; // compression algorithm
unsigned char b_ftid; // filter id
unsigned char b_cto8; // 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,其餘欄位清零。
super::pack3(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_te64(&phdri[k].p_offset) +
get_te64(&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_le32(&hdr.sz_cpr, UPX_MAGIC_LE32);
fo->write(&hdr, sizeof(hdr));
fpad4(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中。
if (0!=xct_off) { // shared library
Elf64_Phdr *phdr = phdri;
unsigned off = fo->st_size();
unsigned off_init = 0; // where in file
upx_uint64_t va_init = sz_pack2; // virtual address
upx_uint64_t rel = 0;
upx_uint64_t old_dtinit = 0;
for (int j = e_phnum; --j>=0; ++phdr) {
upx_uint64_t const len = get_te64(&phdr->p_filesz);
upx_uint64_t const ioff = get_te64(&phdr->p_offset);
upx_uint64_t align= get_te64(&phdr->p_align);
unsigned const type = get_te32(&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_te64(&phdr->p_offset, rel + ioff);
}
if (off_init) { // change DT_INIT.d_val
fo->seek(off_init, SEEK_SET);
upx_uint64_t word; set_te64(&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()的主要工作是為輸出檔案補充和修正一些欄位。
void PackLinuxElf::pack3(OutputFile *fo, Filter &ft)
{
unsigned disp;
unsigned const zero = 0;
unsigned len = sz_pack2a; // 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偏移。
set_te32(&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_te32(&disp, len); // distance back to beginning (detect dynamic reloc)
fo->write(&disp, sizeof(disp));
len += sizeof(disp);
對於共享庫檔案,有三個額外的數值會被寫入,分別是入口函式地址距第一個PT_LOAD段的偏移,意義未明的hatch_off和.init段地址。
if (xct_off) { // is_shlib
upx_uint64_t const firstpc_va = (jni_onload_va
? jni_onload_va
: elf_unsigned_dynamic(Elf32_Dyn:T_INIT) );
set_te32(&disp, firstpc_va - load_va);
fo->write(&disp, sizeof(disp));
len += sizeof(disp);
set_te32(&disp, hatch_off);
fo->write(&disp, sizeof(disp));
len += sizeof(disp);
set_te32(&disp, xct_off);
fo->write(&disp, sizeof(disp));
len += sizeof(disp);
}
sz_pack2 = len; // 0 mod 8
呼叫父類的pack3()新增decompressor解壓縮器,即UPX的loader,隨後更新l_info結構體中的size欄位。
super::pack3(fo, ft); // append the decompressor
set_te16(&linfo.l_lsize, up4( // MATCH03: up4
get_te16(&linfo.l_lsize) + len - sz_pack2a));
len = fpad4(fo); // MATCH03
ACC_UNUSED(len);
}
最關鍵的新增loader的函式又加深了一層。。。好吧我們繼續看父類的pack3(),即PackUnix::pack3(),位於p_unix.cpp
void PackUnix::pack3(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更新入口點。
void PackLinuxElf64::updateLoader(OutputFile * /*fo*/)
{
set_te64(&elfout.ehdr.e_entry, sz_pack2 +
get_te64(&elfout.phdr[0].p_vaddr));
}
呼叫位於p_unix.cpp的PackUnix::patchLoaderChecksum()更新了l_info資訊。
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_LE32; // LE32 always
set_te16(&lp->l_lsize, (upx_uint16_t) lsize);
lp->l_version = (unsigned char) ph.version;
lp->l_format = (unsigned char) ph.format;
// INFO: lp->l_checksum is currently unused
set_te32(&lp->l_checksum, upx_adler32(ptr, lsize));
}
最後將loader寫入檔案。
對於pack3()來說,x86與AMD64除了loader外並無區別,ARM的PackLinuxElf32::ARM_updateLoader()在入口點的設定上額外加上了_start符號的偏移。
(4) 對pack4()的分析
pack()的最後呼叫了pack4()對輸出檔案做最後的修補工作,呼叫了checckFinalCompressionRadio()檢查壓縮率。
......
pack4(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()中賦值的資料
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 = fpad4(fo);
set_te64(&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
......
// 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_te64(&elfout.phdr[0].p_filesz, sz_pack2 + lsize);
elfout.phdr[0].p_memsz = elfout.phdr[0].p_filesz;
super::pack4(fo, ft); // write PackHeader and overlay_offset
......
重寫了ELF檔案頭,這部分程式碼用到的資料都已經在pack2()和pack3()中被修改完成了。注意到為了避免SELinux不允許記憶體頁同時具有可寫和可執行的限制,作者註釋掉了一行修改段屬性的程式碼。
......
// rewrite Elf header
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 library
fo->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的
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 le32
set_le32(buf+0, UPX_MAGIC_LE32);
set_le32(buf+4, UPX_MAGIC2_LE32);
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()對該陣列進行資料填充。
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\n", get_le32(p+4), UPX_MAGIC2_LE32);
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_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_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通用的結構體
struct packHeader{
uint32_t magic;
uint8_t version;
uint8_t format;
uint8_t method;
uint8_t level;
uint32_t u_adler;
uint32_t c_adler;
uint32_t u_len;
uint32_t c_len;
uint32_t u_file_size;
uint8_t filter;
uint8_t filter_cto;
uint8_t n_mru;
uint16_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()
px_byte *ElfLinker::getLoader(int *llen) const {
if (llen)
*llen = outputlen;
return output;
}
這裡的output和outputlen都是linker類的成員變數。顯然,我們需要找到對output進行賦值的函式,滿足這個條件的函式只有位於linker.cpp中的ElfLinker::addLoader()
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()
void PackLinuxElf64amd::buildLoader(const Filter *ft)
{
if (0!=xct_off) { // shared library
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(),對於共享庫,