近年來Linux操作系統以其開源,免費等特性得到了廣泛的應用,Linux平臺上的應用軟件數量也以極快的速度增加,這其中既有大量的開源軟件和免費軟件,也有為數不少的商業軟件。在開發過程中,經常需要在沒有沒有源代碼的情況下測試可執行文件,應此需要在不影響程序原有功能的前提下,在目標程序的可執行文件中插入與測試有關的代碼。另外,可執行文件的代碼嵌入技術在二進制文件加解密、版權保護等領也被廣泛地應用。本文主要對Linux平臺下可執行文件的代碼嵌入技術進行了研究,并給出一個實現方法。
1 ELF文件格式及動態加載原理分析
代碼嵌入是指將二進制可執行代碼插入到可執行文件內部。需要認真考慮將代
碼插入到文件的什么位置。任意插入代碼會破壞原來可執行文件的結構,導致
程序執行錯誤,或者目標文件卻可能會因受損而無法執行。因此,需要對可執
行文件的組織結構及裝載過程進行分析。
1.1 ELF文件格式
與Windows操作系統上的PE格式的可執行文件不同,當今Linux操作系統上廣泛
使用的是ELF(Executable and Linkable Format)格式的可執行文件,ELF格式有三
個略有不同的類型:重定位的,可執行的,和共享目標(shared object)。這里
主要討論可執行的ELF文件格式。
ELF格式具有不尋常的雙重特性,如圖1所示。編譯器、匯編器和鏈接器將這
個文件看作是被節(section)頭部表描述的一系列邏輯區段的集合,而系統加
載器將文件看成是由程序頭部表描述的一系列段(segment)的集合。一個段
(segment)通常會由多個節組成。例如,一個“可加載只讀”段可以由可執行代
碼區段、只讀數據區段和動態鏈接器需要的符號組成?芍囟ㄎ晃募哂袇^段
表,可執行程序具有程序頭部表,而共享目標文件兩者都有。區段(section)是用于鏈接器后續處理的,而段(segment)會被映射到內存中[1]。
圖1 ELF文件組織結構示意圖
在ELF文件的頭部是一個elfhdr結構的文件頭部,結構中的e_entry成員保持著程
序開始執行時的入口地址,e_phoff成員保持著段頭表在文件中的偏移量,
e_shoff成員保持著節頭表在文件中的偏移量。每個節和段的位置、大小、屬性
都在節頭表項或段頭表項中有所體現[2]。
在intel 32位平臺上段頭表項的定義為
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
其中,p_offset給出了該段的從文件開始計數的字節偏移量。p_filesz給出了該段
在文件中的大小。
節頭表項的定義為
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
與段頭表類似,節頭表中sh_offset給出了該節在文件中的位置,sh_size給出了該節在文件中的大小。
1.2 ELF可執行文件加載過程及原理:
為了減小文件大小,當今ELF可執行文件普遍采用動態鏈接庫文件的方式,動
態鏈接的ELF可執行文件的裝載是通過內核load_elf_binary函數與動態連接器ld.so共同完成的[3]。首先,內核讀入文件頭部128個字節內容,組建一個elfhdr結構
體。然后根據elfhdr結構中的e_phentsize ,e_phoff以及e_phnum從文件中讀入段頭表。
size = elf_ex.e_phentsize * elf_ex.e_phnum;
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
retval = kernel_read(bprm->file, elf_ex.e_phoff, (char *) elf_phdata, size);
然后循環遍歷段頭表項,找到可加載的段,并調用elf_map將其映射到進程虛存空間中,相關的內核源代碼如下[4]:
for(i = 0, elf_ppnt = elf_phdata; i < elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long vaddr;
if (elf_ppnt->p_type != PT_LOAD)
continue;
……
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags);
在這里,elf_flags參數是根據該段的p_flags成員得到的,決定了映射區域的讀寫
以及執行屬性。
同時,找到p_type為PT_INTERP的段,將動態連接器ld.so映射到地址空間的一
個合適的位置,然后內核把控制入口轉入動態連接器,從動態連接器開始執
行。并在棧中放入鏈接器所需要的輔助向量。其中包括程序的起始地址
e_entry。當動態鏈接器完成了初始化工作之后,就會跳轉到這個地址去執行。
2 代碼嵌入的方法及嵌入模塊的實現
2.1 代碼嵌入的方法分析
通過ELF文件格式及動態加載原理的分析,就可以考慮將可執行的代碼嵌入到
原來可執行文件的方法。在ELF的段頭每一個表項中的p_type成員指明了該段是否
能被內核加載進內存,p_vaddr成員指明了該段將被加載的虛擬地址。通常后
一個可加載段的加載地址緊鄰著上一加載段的結尾。如果將代碼插入到原來可
執行文件中的某個可加載段中,該段增加這部分代碼的地址就會與后面可加載
段原來的地址相沖突。因而必須對這種方法進行改進。
由于Linux內核是分段將ELF可執行文件載入內存的,因此可以考慮為嵌入代碼
增加一個可加載段,相應地也增加一個段頭表項。在Intel平臺上,進程空間大
小一共4GB,地址范圍從0到0xFFFFFFFF。內核加載ELF文件時,將代碼作為一個可加載段從0x8048000開始向上讀入內存[5]。為了避免內核將新增加的段讀入
原來程序的地址空間,新程序段只能放置在代碼段裝載地址0x8048000的前面。
為了不影響原來可執行文件各段和節的偏移,需要將插入代碼可以插入到可執
行文件所有數據的后面,并且在段頭表中增加一個p_type = PT_LOAD的表項,
這樣在內核執行該可執行文件時,該段就可以被加載進內存。然后修改ELF頭
部中相關的的成員。使插入部分的代碼可以首先獲得執行權。
2.2代碼嵌入模塊的實現
首先,為了使原來的程序可以恢復執行,需要保存可執行文件的原入口地址,
找到文件頭部,并保存程序原入口地址。當插入代碼執行完成以后,控制權將
移交給原程序,因此需要對插入代碼進行處理,在代碼最后添加跳轉指令,使
程序跳轉到程序原入口地址去執行。
然后,需要在原來的段頭表基礎上增加一個段頭表項。而增加的段頭表項會覆
蓋這兩個節區的數據。通過分析ELF文件的內容,可以發現緊鄰段頭表的兩個
節區為interp和note.ABI-tag節,前者指明了動態加載器ld.so的路徑,后者為輔助
信息節區。由于內核需要將動態連接器映射到地址空間的一個合適的位置,考
慮到note.ABI-tag節是輔助信息的節區,因此可以把interp節區的信息復制到
note.ABI-tag節區中,這樣,就可保證可加載段的總長度保持不變。找到段頭表的
結尾,在ELF文件中,這里是interp節區,保存的是動態加載器的路徑。為了在
這里增加段表項,將該節區的內容后移一個段表項的大小,覆蓋后面的
note.ABI-tag節部分內容。同時,為了能正確地加載動態連接器,找到p_type為
PT_INTERP的段表項,修改其代表文件偏移的p_offset成員為新的偏移量。段頭表由
多個表項組成,因此,實現代碼在這里使用循環處理。
elf_ppnt = elf_phdr;
for (i = 0; i < elf_head.e_phnum; i++, elf_ppnt++)
{
if (elf_ppnt->p_type == PT_INTERP)
{
elf_interpreter = (char *)malloc(elf_ppnt->p_filesz);
lseek(newfd, elf_ppnt->p_offset, SEEK_SET);
len = read(newfd, elf_interpreter, elf_ppnt->p_filesz);
lseek(newfd, elf_ppnt->p_offset + elf_head.e_phentsize, SEEK_SET);
len = write(newfd, elf_interpreter, elf_ppnt->p_filesz);
elf_ppnt->p_offset += elf_head.e_phentsize;
lseek(newfd, elf_head.e_phoff + (i * elf_head.e_phentsize), SEEK_SET);
len = write(newfd, elf_ppnt, elf_head.e_phentsize);
}
}
在ELF規范指出p_vaddr mod PAGE_SIZE == p_offset mod PAGE_SIZE[3]。 這里
的PAGE_SIZE為相應硬件平臺上虛擬內存中一頁的大小。應此,內核映射的段
的虛擬地址對頁面大小的余數必須和段在文件的偏移量對頁大小的余數相等。
因此,設置新增加段的p_vaddr成員為0x48000,當文件執行時,該段就被加載
到地址0x48000。這樣,可增加的代碼長度最大為0x8000000字節。這個大小可
以滿足大部分嵌入代碼的長度。由于新增加的段被加載到地址0x48000,因
此,只需把代碼插入到文件末尾并且偏移為整數頁的地方。并保存代碼插入的
偏移到變量 new_offset中。在得到new_offset以后,就可以設置新插入的段頭表
項的成員。關鍵部分的實現代碼如下:
elf_ppnt = elf_phdr + elf_head.e_phnum;
elf_ppnt->p_type = PT_LOAD;
elf_ppnt->p_offset = new_offset;
elf_ppnt->p_vaddr = 0X48000;
elf_ppnt->p_paddr = 0X48000;
elf_ppnt->p_filesz = codelen;
elf_ppnt->p_memsz = codelen;
elf_ppnt->p_flags = PF_X | PF_R;
elf_ppnt->p_align = PAGE_SIZE;
lseek(newfd, elf_head.e_phoff + elf_head.e_phnum * elf_head.e_phentsize, SEEK_SET);
write(newfd, elf_ppnt, elf_head.e_phentsize);
嵌入代碼前后可執行文件的內存布局如圖 2 所示。
圖2 代碼嵌入前后內存空間布局
當代碼插入完成以后,需要修改ELF文件中各頭部中的信息。對于ELf文件頭
部,修改程序入口(e_entry)為0x48000。這樣,目標程序在執行時,嵌入的
代碼將首先被運行。由于增加了一個段頭表項,其代表段頭表項個數的
e_phnum成員的值也要修改。
elf_head.e_phnum++;
elf_head.e_entry = 0x48000;
lseek(newfd, 0, SEEK_SET);
len = write(newfd, &elf_head, elf_head.e_ehsize);
2.3 代碼嵌入的效果,存在的不足和改進思路
在這里,作為測試,插入部分的代碼保存在文件insertcode中,其二進制數據為,
0: b8 04 00 00 00 mov $0x4,%eax
5: bb 01 00 00 00 mov $0x1,%ebx
a: b9 1d 80 04 00 mov $0x4801d,%ecx
f: ba 0c 00 00 00 mov $0xc,%edx
14: cd 80 int $0x80
16: bd 2a 80 04 00 mov $0x4802a,%ebp
1b: ff e5 jmp *%ebp
該段代碼顯示一行字符串“befor main\n”
在shell下執行效果如圖3所示
圖3 代碼插入模塊在shell下的測試結果
由圖3可見,模塊對可執行代碼的插入結果比較令人滿意。
代碼嵌入模塊功能上也存在著一些不足。最明顯的一點是它改變了動態連接器
節區和輔助信息節區在文件中的偏移,在使用一些工具查看可執行文件的信息
時可能會得到錯誤的信息。其次,插入部分的代碼很難利用原可執行文件中動
態鏈接的函數。
一些可能的改進思路包括:
(1)為了滿足ELF規范,可加載段之間一般會有以0為數據的填充區,而輔助
信息節區的大小很小。應此可以找到可執行段的結尾的位置,在填充數據區中
保存原來輔助信息的節區。同時修改ELF文件中節名字符串表節中的信息,使
之能準確反應被修改的兩個節區的內容在目標文件內的起始位置。
(2)為插入的代碼部分添加功能,通過調用Linux系統的ld.so獲得動態鏈接庫函數入口地址[6],使之在可執行文件裝入后能查找原來執行映像中動態鏈接函數的
重定位地址。
3 結束語
隨著Linux操作系統的普及,需要對Linux平臺下的可執行文件的研究也愈發迫
切。本文針對Linux可執行文件進行了研究,提出了一種嵌入代碼的方法,并取
得了較好的效果。然而對比傳統Windows平臺下PE文件的研究,Linux下的可執
行文件相關技術的研究還遠遠沒有PE文件的研究那么成熟。希望此文能夠起到
拋磚引玉的作用,從而推動Linux平臺下可執行文件的相關研究工作。
參考文獻 (References)
[1]John R Levine.Linkers & Loaders[M]. Elsevier Science Ltd,2005.
[2]何先波, 唐寧九, 呂方, 袁敏.ELF文件格式及應用[j].計算機應用研究,2001(11).
[3]Tool Interface Standard(TIS) Executable and Linking Format(ELF) Specification V1.2.
[4]Bovet D P Understand the Linux Kernel 2nd ed[M]. O'Reilly Media, Inc 2003.
[5]康曉寧,蔣東興,劉啟新 Linux-i386平臺可執行文件代碼保護技術 計算機工程,2005,31(4).
[6]張和君,張躍.Linux動態鏈接機制研究及應用.計算機工程,2006,32(22).
[1]ELF文件格式及應用 The Format and Application of ELF Files
[3]Linux-i386平臺可執行文件代碼保護技術 The Protection Technology of Linux Executable Files on i386 paltform
[4]Linux動態鏈接機制研究及應用 the Research and Application of Linux Dynamic linking mechanism
作者簡介:
龍文策,男,四川大學計算機學院碩士研究生,研究方向為軟件工程與工具。
唐寧九,男,四川大學計算機學院教授,研究方向為軟件工程與計算機網絡。
林濤,男,四川大學計算機學院副教授,研究方向為數字多媒體技術。09861