Unix/ELF檔案格式及病毒分析(轉)

ba發表於2007-08-12
Unix/ELF檔案格式及病毒分析(轉)[@more@]★ 介紹

本文介紹了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病毒儘管不流行,但的確可行。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-950676/,如需轉載,請註明出處,否則將追究法律責任。

相關文章