Unix ELF檔案格式及病毒分析
★ 介紹
本文介紹了Unix病毒機制、具體實現以及ELF檔案格式。簡述了Unix病毒檢測和反檢測技術,提供了Linux/i386架構下的一些例子。需要一些初步的Unix編程經驗,能夠理解Linux/i386下組合語言,如果理解ELF本身更好。
本文沒有任何實際意義上的病毒編程技術,僅僅是把病毒原理應用到Unix環境下。這裡也不打算從頭介紹ELF規範,感興趣的讀者請自行閱讀ELF規範。
★ 感染 ELF 格式檔案
進程映象包含"文本段"和"資料區段",文本段的記憶體保護屬性是r-x,因此一般自修改代碼不能用於文本段。資料區段的記憶體保護屬性是rw-。
段並不要求是頁尺寸的整數倍,這裡用到了填充。
關鍵字:
[...] 一個完整的頁
M 已經使用了的記憶體
P 填充
頁號
#1 [PPPPMMMMMMMMMMMM] \
#2 [MMMMMMMMMMMMMMMM] |-- 一個段
#3 [MMMMMMMMMMMMPPPP] /
段並沒有限制一定使用多個頁,因此單頁的段是允許的。
頁號
#1 [PPPPMMMMMMMMPPPP] <-- 一個段
典型的,資料區段不需要從頁邊界開始,而文本段要求起始頁邊界對齊,一個進程映象的記憶體布局可能如下:
關鍵字:
[...] 一個完整的頁
T 文本段內容
D 資料區段內容
P 填充
頁號
#1 [TTTTTTTTTTTTTTTT] <-- 文本段內容
#2 [TTTTTTTTTTTTTTTT] <-- 文本段內容
#3 [TTTTTTTTTTTTPPPP] <-- 文本段內容(部分)
#4 [PPPPDDDDDDDDDDDD] <-- 資料區段內容(部分)
#5 [DDDDDDDDDDDDDDDD] <-- 資料區段內容
#6 [DDDDDDDDDDDDPPPP] <-- 資料區段內容(部分)
頁1、2、3組成了文本段
頁4、5、6組成了資料區段
從現在開始,為簡便起見,段描述圖表用單頁,如下:
頁號
#1 [TTTTTTTTTTTTPPPP] <-- 文本段
#2 [PPPPDDDDDDDDPPPP] <-- 資料區段
在i386下,堆棧段總是在資料區段被給予足夠空間之後才定位的,一般堆棧位於記憶體高端,它是向低端增長的。在ELF檔案中,可裝載段都是物理映象:
ELF Header
.
.
Segment 1 <-- 文本段
Segment 2 <-- 資料區段
.
.
每個段都有一個定位自身起始位置的虛擬位址。可以在代碼中使用這個地址。
為了插入寄生代碼,必須保證原來的代碼不被破壞,因此需要擴充相應段所需記憶體。
文本段事實上不僅僅包含代碼,還有 ELF 頭,其中包含動態連結資訊等等。如果直接擴充文本段插入寄生代碼,帶來的問題很多,比如引用絕對位址等問題。可以考慮保持文本段不變,額外增加一個段存放寄生代碼。然而引入一個額外的段的確容易引起懷疑,很容易被發現。
向高端擴充文本段或者向低端擴充資料區段都有可能引起段重疊,在記憶體中重定位一個段又會使那些引用了絕對位址的代碼產生問題。可以考慮向高端擴充資料區段,這不是個好主意,有些Unix完整地實現了記憶體保護機制,資料區段是不可執行檔。
段邊界上的頁填充提供了插入寄生代碼的地方,只要空間允許。在這裡插入寄生代碼不破壞原有段內容,不要求重定位。文本段結尾處的頁填充是個很好的地方,最後看上去象下面這個樣子:
關鍵字:
[...] 一個完整的頁
V 寄生代碼
T 文本段內容
D 資料區段內容
P 填充
頁號
#1 [TTTTTTTTTTTTVVPP] <-- 文本段
#2 [PPPPDDDDDDDDPPPP] <-- 資料區段
一個更完整的ELF可執行布局如下:
ELF Header
Program header table
Segment 1
Segment 2
Section header table
Section 1
.
.
Section n
典型的,額外的節(那些沒有相應段的節)用於存放調試資訊、符號表等等。
下面是一些來自 ELF 規範的內容:
ELF 頭位於最開始,儲存一張"road map",描述了檔案的組織圖。節儲存大量連結資訊、符號表、重定位資訊等等。
如果存在一個"program header table",將告訴作業系統如何建立進程映象(執行一個程式)。
可執行檔必須有一個"program header table",可重定位的檔案不需要該表。"section header table"描述了檔案的節組織。每個節在該表中都有一個表項,表項包含了諸如節名、節尺寸等資訊。
連結過程中被用到的檔案自身必須有一個"section header table",其他目標檔案可有可無該表。
插入寄生代碼之後,ELF 檔案布局如下:
ELF Header
Program header table
Segment 1 - 文本段(主體代碼)
- 寄生代碼
Segment 2
Section header table
Section 1
.
.
Section n
寄生代碼必須物理插入到ELF檔案中,文本段必須擴充以包含新代碼。
下面的資訊來自/usr/include/elf.h
/* The ELF file header. This appears at the start of every
ELF file. */
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT];
/* Magic number and other info */
Elf32_Half e_type;
/* Object file type */
Elf32_Half e_machine;
/* Architecture */
Elf32_Word e_version;
/* Object file version */
Elf32_Addr e_entry;
/* Entry point virtual address */
Elf32_Off e_phoff;
/* Program header table file offset */
Elf32_Off e_shoff;
/* Section header table file offset */
Elf32_Word e_flags;
/* Processor-specific flags */
Elf32_Half e_ehsize;
/* ELF header size in bytes */
Elf32_Half e_phentsize;
/* Program header table entry size */
Elf32_Half e_phnum;
/* Program header table entry count */
Elf32_Half e_shentsize;
/* Section header table entry size */
Elf32_Half e_shnum;
/* Section header table entry count */
Elf32_Half e_shstrndx;
/* Section header string table index */
} Elf32_Ehdr;
e_entry 儲存了程式進入點的虛擬位址。
e_phoff 是"program header table"在檔案中的位移。因此為了讀取"program header table",需要調用lseek()定位該表。e_shoff 是"section header table"在檔案中的位移。該表位於檔案尾部,在文本段尾部插入寄生代碼之後,必須更新e_shoff指向新的位移。
/* Program segment header. */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
可裝載段(文本段/資料區段)在"program header"中由成員變數p_type標識出是可裝載的,其值為PT_LOAD (1)。與"ELF header"中的e_shoff一樣,這裡的p_offset成員必須在插入寄生代碼後更新以指向新位移。
p_vaddr 指定了段的起始虛擬位址。以p_vaddr為基地址,重新計算e_entry,就可以指定程式流從何處開始。可以利用p_vaddr指定程式流從何處開始。p_filesz 和 p_memsz 分別對應該段佔用的檔案尺寸和記憶體尺寸。.bss 節對應資料區段裡未初始化的資料部分。我們不想讓未初始化的資料佔用檔案空間,但是進程映象必須保證能夠分配足夠的記憶體空間。
.bss 節位於資料區段尾部,任何超過檔案尺寸的定位都假設位於該節中。
/* Section header. */
typedef struct
{
Elf32_Word sh_name;
/* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr;
/* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info;
/* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize;
/* Entry size if section holds table */
} Elf32_Shdr;
sh_offset 指定了節在檔案中的位移。
為了在文本段末尾插入寄生代碼,我們必須做下列事情:
* 修正"ELF header"中的 p_shoff
* 定位"text segment program header"
* 修正 p_filesz
* 修正 p_memsz
* 對於文本段phdr之後的其他phdr
* 修正 p_offset
* 對於那些因插入寄生代碼影響位移的每節的shdr
* 修正 sh_offset
* 在檔案中物理地插入寄生代碼到這個位置
text segment p_offset + p_filesz (original)
這裡存在一個大問題,ELF 規範中指出,
p_vaddr mod PAGE_SIZE == p_offset mod PAGE_SIZE
為了滿足這個要求:
* 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小
* 定位"text segment program header"
* 修正 p_filesz
* 修正 p_memsz
* 對於文本段phdr之後的其他phdr
* 修正 p_offset ,增加 PAGE_SIZE 大小
* 對於那些因插入寄生代碼影響位移的每節的shdr
* 修正 sh_offset ,增加 PAGE_SIZE 大小
* 在檔案中物理地插入寄生代碼以及填充(確保構成
一個完整頁)到這個位置
text segment p_offset + p_filesz (original)
我們還需要修正程式進入點的虛擬位址,使得寄生代碼先於宿主代碼執行。同時需要
在寄生代碼尾部能夠跳回宿主代碼原進入點繼續正常流程。
* 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小
* 修正寄生代碼的尾部,使之能夠跳回宿主代碼原進入點
* 定位"text segment program header"
* 修正 "ELF header"中的 e_entry ,指向
p_vaddr + p_filesz
* 修正 p_filesz
* 修正 p_memsz
* 對於文本段phdr之後的其他phdr
* 修正 p_offset ,增加 PAGE_SIZE 大小
* 對於文本段的最後一個shdr
* 修正sh_len(應該是sh_size吧,不確定),增加寄生代
碼大小
* 對於那些因插入寄生代碼影響位移的每節的shdr
* 修正 sh_offset ,增加 PAGE_SIZE 大小
* 在檔案中物理地插入寄生代碼以及填充(確保構成一個完整頁)
到這個位置text segment p_offset + p_filesz (original)
病毒可以隨機遍曆一個分類樹,尋找那些e_type等於 ET_EXEC或者 ET_DYN 的檔案,加以感染,這分別是可執行檔和動態連結程式庫檔案。
★ 分析Linux病毒
病毒要求不使用庫,避開libc,轉而使用系統調用機制。
為了動態申請堆記憶體用於phdr table和shdr table,應該使用brk系統調用。利用與緩衝區溢位相同的技術取得常量字串的地址。
使用gcc -S編譯c代碼,觀察調整asm代碼。
注意在進入/離開寄生代碼的時候儲存/恢複寄存器。
利用objdump -D觀察調整一些需要確定的位移量。
★ 檢測病毒
這裡描述的病毒很容易檢測。最顯眼的是程式進入點不在常規節中,甚至乾脆不在任何節中。清理病毒的過程和感染病毒的過程類似。
用objdump --all-headers很容易定位程式進入點,用objdump --disassemble-all跟蹤下去就可以得到程式原進入點。
預設程式進入點是_start,但是可以在連結的時候更改它。
★ 結論
Unix病毒儘管不流行,但的確可行。