修改任意Linux进程地址空间实施代码注入

提到进程注入,常规的方案就是使用ptrace,其POKEDATA,POKETEXT命令选项单从名字上就知道是干什么的,这里不再赘述。

然而ptrace是个系统化的东西,太复杂,不适合玩手艺,有没有什么适合手工玩的东西呢?当然有!

正如读写/dev/mem可以手工完成crash+gdb的功能hack内核一样,每一个用户态进程也有一个mem文件,即 /proc/$pid/mem

我不敢保证每一个系统该文件都可写,但一旦它可写就好玩了。

/proc/$pid/mem文件抽象的一个进程的地址空间,直接写该文件就可以实现进程的注入。

一切皆文件。线性的/proc/$pid/mem文件与平坦的进程地址空间所对应。

让我们一步一步从最简单的场景来玩起。

首先看下面的代码:

// psss.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	int tf = atoi(argv[1]);
	int a = 12345678;
	while (tf) {
    
    
		printf("value:%d   address:%p  pid:%d\n", a, &a, getpid());
		sleep(1);
	}

	printf("break the loop!\n");
}

很简单的程序,我们编译它:

[root@localhost test]# gcc psss.c -o psss -O0

我们的目标有两个:

  1. 修改该进程的变量12345678为其进程pid。
  2. 跳出死循环。

先来完成第一个目标。让我们先运行该进程:

[root@localhost test]# ./psss 1
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446

由于地址已经直接打印出来了,我们直接编个程序写该地址即可:

// wmem.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	char name[32];
	int fd;
	int pid;
	char *addr;

	pid = atoi(argv[1]);
	addr = (char *)strtoul(argv[2], NULL, 16);

	sprintf(name, "/proc/%d/mem", pid);
	fd = open(name, O_RDWR);
	lseek(fd, addr , SEEK_SET);

	write(fd, &pid, sizeof(pid));
}

来看效果:

[root@localhost test]# ./wmem 5446 0x7ffd4943cf18

# 运行psss的终端显示如下:
...
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:12345678   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
...

成功修改。

接下来我们修改它的指令,使其跳出死循环。

首先拿到它的text映像映射的位置信息:

[root@localhost test]# cat /proc/5446/smaps |grep -A2 r-xp.*/usr/test/psss
00400000-00401000 r-xp 00000000 fd:00 75128761                           /usr/test/psss
Size:                  4 kB
Rss:                   4 kB

为了详细分析其指令,找出要修改的位置,我们把它的live映像dd出来以便离线分析:

# 4194304是0x00400000的十进制
[root@localhost test]# dd if=/proc/5446/mem of=./psss.dd skip=4194304 bs=1 count=4096
记录了4096+0 的读入
记录了4096+0 的写出
4096字节(4.1 kB)已复制,0.013757 秒,298 kB/秒

注意,以上这一步操作非常适合我们拿不到二进制ELF程序的情况下来objdump一个内存中的进程映像,接下来我们就来objdump这个live映像的汇编指令:

# x86_64平台适用
[root@localhost test]# objdump -D -Mintel,x86-64 -b binary -m i386 ./psss.dd >./psss.obj

OK,psss.obj就是了。基于我们对Intel x86_64指令的熟悉,很容易在其中找到了下面的位置:

68f:   bf 01 00 00 00          mov    edi,0x1
     694:   b8 00 00 00 00          mov    eax,0x0
     699:   e8 92 fe ff ff          call   0x530
     69e:   83 7d fc 00             cmp    DWORD PTR [rbp-0x4],0x0
     6a2:   75 c7                   jne    0x66b
     6a4:   bf 5f 07 40 00          mov    edi,0x40075f
     6a9:   e8 32 fe ff ff          call   0x4e0
     6ae:   c9                      leave
     6af:   c3                      ret

注意,就是0x6a2的位置处的指令,我们将其修改:

# 代码就不展示了,很简单,就是把75 c7改成75 00即可。
[root@localhost test]# ./a.out 5446 4006a3 # base(0x400000)+offset(0x6a2+1)

看看psss的情况:

value:5446   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
value:5446   address:0x7ffd4943cf18  pid:5446
break the loop!

成功跳出了死循环!

接下来玩一个简单的进程注入,来调用一个僵尸函数。

请看原始程序代码:

// pokestack.c
#include <stdio.h>
#include <stdlib.h>

void say_hi()
{
    
    
	printf("skinshoe\n");
	exit(0);
}

int main(int argc, char **argv)
{
    
    
	int tf = atoi(argv[1]);
	long a = 0x1122334455667788;
	while (tf) {
    
    
		printf("value:%lx   address:%p  pid:%d\n", a, &a, getpid());
		sleep(120); // 在120秒内完成手工操作。用getchar亦可。
	}

	printf("break the loop!\n");
}

请注意,say_hi函数没有任何地方调用它,我们接下来就通过修改其mem文件,让这个程序调用say_hi。

先把它跑起来:

[root@localhost test]# ./pokestack 1
value:1122334455667788   address:0x7fff75a4ba80  pid:5553

按照老样子,这次我们定位其stack中被压栈的sleep返回地址的位置,我们的目标就是改掉它。

如果你不想每次都去查stack的位置,你可以关闭一些ASLR的保护,比如:

sysctl -w kernel.randomize_va_space=0

但是这里为了让事情更加真实,不采用这个伎俩,在真实的环境中,也几乎没有关闭ASLR的。

所以我们依然要查stack的位置,毕竟它被随机化了,每次都不一样:

[root@localhost test]# cat /proc/5553/smaps |grep -E \\[stack\\]
7fff75a2c000-7fff75a4d000 rw-p 00000000 00:00 0                          [stack]
[root@localhost test]# dd if=/proc/5553/mem of=./pokestack.dd skip=140735166988288 bs=1 count=557056
记录了135168+0 的读入
记录了135168+0 的写出
135168字节(135 kB)已复制,0.165683 秒,816 kB/秒
[root@localhost test]# hexdump ./pokestack.dd >./pokestack.hex

我们开始寻找pokestack.hex里面sleep的return address的位置,找到了下面的行:

001fa60 bb70 75a4 7fff 0000 06ff 0040 0000 0000

4006ff就是了。

至于说是如何找到的,不是本文的内容,无非就是按照模式去匹配了:

  • 返回main函数肯定是400000附近…

让我们改掉它,改成say_hi的地址:

# 0x7FFF75A4BA68是stack基地址和0x001fa60+0x8的加和。
# 0x000000000040067d是say_hi的地址。
# 将0x7FFF75A4BA68位置的值改成0x000000000040067d
[root@localhost test]# ./a.out 5553 0x7FFF75A4BA68 0x000000000040067d

等待sleep返回后,看效果:

value:1122334455667788   address:0x7fff75a4ba80  pid:5553

skinshoe
[root@localhost test]#

效果不错。

接下来让我们把一段独立的代码注入到一个已经运行的进程:

// pokestack.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	__asm("mov $123, %rdi\n"); // 这句是为了导出exit系统调用参数123的指令码。。。
	while (1) {
    
    
		printf("pid:%d\n", getpid());
		sleep(120);
	}
}

导出stack的信息:

[root@localhost test]# cat /proc/6033/smaps |grep -E -A2 \\[stack\\]
7ffe66868000-7ffe66889000 rw-p 00000000 00:00 0                          [stack]
Size:                136 kB
Rss:                  16 kB

注意,stack没有可执行权限,所以要在binary的text段进行注入。

如果stack可执行,那么事情就会简单的多,直接将要注入的代码覆盖stack的起始位置就好了(由于stack从高向低伸展,一般伸展不到这么远的地方),让stack变得可执行也不是那么困难,用下面的方式编译即可:

[root@localhost test]# gcc -O0 -z execstack  pokestack.c -o pokestack

但是这里为了让事情更加真实,不采用这个伎俩,在真实的环境中,也几乎没有让stack可执行的。

现在我们看一下binary的text段信息:

[root@localhost test]# cat /proc/6033/smaps |grep -A2 r-xp.*pokestack
00400000-00401000 r-xp 00000000 fd:00 75346995                           /usr/test/pokestack
Size:                  4 kB
Rss:                   4 kB

下面是实施注入的代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	char name[32];
	int fd;
	int pid;
	unsigned long caddr, raddr;
	unsigned long ret_addr;
	// code 仅仅是一个exit(123)的系统调用
	char code[14] = {
    
    0x48, 0xc7, 0xc7, 0x7b, 0x00, 0x00, 0x00, 0xb8, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x05};

	pid = atoi(argv[1]);
	caddr = strtoul(argv[2], NULL, 16);
	raddr = strtoul(argv[3], NULL, 16);

	sprintf(name, "/proc/%d/mem", pid);
	fd = open(name, O_RDWR);

	lseek(fd, caddr, SEEK_SET);
	write(fd, &code, sizeof(code));

	lseek(fd, raddr, SEEK_SET);
	write(fd, &caddr, sizeof(caddr));
}

不再解释代码,我们直接看效果:

# 0x7FFE66887998依然是用dd方法导出的stack找到的return address地址位置。
[root@localhost test]# ./a.out 6033 0x00400000 0x7FFE66887998

看看6033号进程的结局:

[root@localhost test]# ./pokestack
pid:6033
[root@localhost test]# echo $?
123

成功以123退出。

下面,我们来个打印怎么样?来打印一堆a。

我们需要注入printf的调用,然而一般都是用相对偏移调用的call,校准的过程并不容易,因此我们采用下面的方法:

push printf@GLIBC
ret

然而printf何在?于是乎,先简单一点,我们直接调用write系统调用。

先看原始代码:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	while (1) {
    
    
		printf("pid:%d\n", getpid());
		getchar();
	}
}

跑起来:

[root@localhost test]# ./pokestack
pid:14917

pid:14917
... # 继续敲任意键,依然会getchar,这就是个死循环

一步一步地获取信息:

[root@localhost test]# cat /proc/14917/smaps |grep -E \\[stack\\]
7ffc08ac1000-7ffc08ae2000 rw-p 00000000 00:00 0                          [stack]
[root@localhost test]# dd if=/proc/14917/mem of=./pokestack.dd skip=140720453980160 bs=1 count=557056
[root@localhost test]# hexdump ./pokestack.dd >./pokestack.hex
...

在pokestack.hex里找到了下面的行:

001f4a0 0000 0000 0000 0000 05dc 0040 0000 0000

拼接偏移:

0x7ffc08ac1000 + 0x001f4a0 + 0x08 = 0x7FFDF4F9E818

来制作我们的注入code并且实施注入,下面是代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	char name[32];
	int fd;
	int pid;
	unsigned long caddr, raddr;
	unsigned long ret_addr;
	char code[58] = {
    
    /*0x00*/ 0x61, 0x61, 0x61, 0x0a,
					 /*0x04*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x08*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x0c*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x10*/ 0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov 1, rdi
					 		  0x48, 0xc7, 0xc6, 0x00, 0x00, 0x40, 0x00, // mov 400000, rsi
					 		  0x48, 0xc7, 0xc2, 0x04, 0x00, 0x00, 0x00, // mov 4, rdx
							  0xb8, 0x01, 0x00, 0x00, 0x00,				// mov 1, eax
					 		  0x0f, 0x05,						// write syscall
							  0x48, 0xc7, 0xc7, 0x7b, 0x00, 0x00, 0x00, // mov 123, rdi
							  0xb8, 0x3c, 0x00, 0x00, 0x00, 			// mov 60, eax
							  0x0f, 0x05};						// exit syscall

	pid = atoi(argv[1]);
	caddr = strtoul(argv[2], NULL, 16);
	raddr = strtoul(argv[3], NULL, 16);

	sprintf(name, "/proc/%d/mem", pid);
	fd = open(name, O_RDWR);

	lseek(fd, caddr, SEEK_SET);
	write(fd, &code[0], sizeof(code));

	caddr += 0x10; // 从mov 1, rdi开始执行
	lseek(fd, raddr, SEEK_SET);
	write(fd, &caddr, sizeof(caddr));
}

来实施注入:

[root@localhost test]# ./a.out 14917 0x00400000 0x7FFC08AE04A8

看效果:

[root@localhost test]# ./pokestack
pid:14917

pid:14917

aaa
[root@localhost test]# echo $?
123
[root@localhost test]#

不错,跳出了死循环并以123退出。

是不是比ptrace好玩呢?

我们还是希望可以直接调用GLIBC的库函数,而不是直接调用系统调用,其实这并不难。

先看下面的代码:

#include <stdio.h>
char abc1[] = "aaaa\n";
int main()
{
    
    
	printf("\n\n"); // 为了让该程序加载时解析到printf函数
	__asm("mov $0x000000000060102c, %rdi"); // abc1的地址给rdi寄存器作为参数
	__asm("push $0x40053d"); // main函数_asm("ret")后面的位置
	__asm("push $0x0000000000400400"); // printf的位置
	__asm("ret");
}

其中的数字都是从objdump里手工找到的。

执行一下试试看,看是不是打印出了aaaa呢。

我们就用此方法来实施注入,还是原来的那个程序,我再贴一遍:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	while (1) {
    
    
		printf("pid:%d\n", getpid());
		getchar();
	}
}

跑起来:

[root@localhost test]# ./pokestack
pid:28814

pid:28814
... # 继续敲任意键,依然会getchar,这就是个死循环

获取信息:

[root@localhost test]# cat /proc/28814/smaps |grep \\[stack\\]
7fff155c8000-7fff155e9000 rw-p 00000000 00:00 0                          [stack]
[root@localhost test]# dd if=/proc/28814/mem of=./pokestack.dd skip=140733551771648 bs=1 count=557056
记录了135168+0 的读入
记录了135168+0 的写出
135168字节(135 kB)已复制,0.150816 秒,896 kB/秒
[root@localhost test]# hexdump ./pokestack.dd >./pokestack.hex
...

下面是实现注入代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    
    
	char name[32];
	int fd;
	int pid;
	unsigned long caddr, raddr;
	unsigned long ret_addr;
	char code[48] = {
    
    /*0x00*/ 0x0a, 0x61, 0x61, 0x0a,
					 /*0x04*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x08*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x0c*/ 0x00, 0x00, 0x00, 0x00,
					 /*0x10*/ 0x48, 0xc7, 0xc7, 0x7b, 0x00, 0x00, 0x00, // mov 123, rdi
							  0xb8, 0x3c, 0x00, 0x00, 0x00, 			// mov 60, eax
							  0x0f, 0x05,								// exit syscall
							  0x48, 0xc7, 0xc7, 0x00, 0x00, 0x40, 0x00,
					 		  0x68, 0x10, 0x00, 0x40, 0x00,
					 		  0x68, 0x80, 0x04, 0x40, 0x00,
							  0xc3};

	pid = atoi(argv[1]);
	caddr = strtoul(argv[2], NULL, 16);
	raddr = strtoul(argv[3], NULL, 16);

	sprintf(name, "/proc/%d/mem", pid);
	fd = open(name, O_RDWR);

	lseek(fd, caddr, SEEK_SET);
	write(fd, &code[0], sizeof(code));

	caddr += 30; // 从0x48, 0xc7, 0xc7开始
	lseek(fd, raddr, SEEK_SET);
	write(fd, &caddr, sizeof(caddr));
}

实施注入:

[root@localhost test]# ./a.out 28814 0x00400000 0x7FFF155E66D8

看28814号进程的结局:

pid:28814


aa
[root@localhost test]# echo $?
123
[root@localhost test]#


如此一来,利用这种机制,Linux是不是也可以像Windows那样CreateRemoteThread呢?当然是可以的,只要插入一些fork/clone的调用即可了,另外mmap/brk也是需要的,这种操作必须在进程自己的上下文中执行,因此也就必然要注入咯。

有人会说,这个和ROP貌似有点关联,其实没什么关联,ROP只是一种栈编程的套路而已,那是一件具体的事情,我这里介绍的是一个修改进程地址空间的通用方法,仅此而已。

感谢procfs导出了/proc/$pid/mem文件。


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

猜你喜欢

转载自blog.csdn.net/dog250/article/details/108618568