提到进程注入,常规的方案就是使用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
我们的目标有两个:
- 修改该进程的变量12345678为其进程pid。
- 跳出死循环。
先来完成第一个目标。让我们先运行该进程:
[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文件。
浙江温州皮鞋湿,下雨进水不会胖。