文章目录
  1. 1. patrace简介
  2. 2. 小试牛刀–简单的PTRACE_ME
  3. 3. ptrace实现进程中断
  4. 4. ptrace实现进程代码注入
  5. 5. 玩转系统调用
  6. 6. 参考

patrace简介

使用过Linux系统多多少少会接触方便我们查看执行的程序的系统调用的strace命令或者编程时使用gdb进行程序调试。他们幕后原理工作其实就是ptrace完成的。

我们通过man ptrace命令可以查看ptrace的使用说明。
ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。

ptrace函数的定义

1
2
#include <sys/ptrace.h>
long ptrace(int request, int pid, int addr, int data);

函数描述:
request参数决定了系统调用的功能:

PTRACE_TRACEME
本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。

PTRACE_PEEKTEXT, PTRACE_PEEKDATA
从内存地址中读取一个字节,内存地址由addr给出。

PTRACE_PEEKUSR
从USER区域中读取一个字节,偏移量为addr。

PTRACE_POKETEXT, PTRACE_POKEDATA
往内存地址中写入一个字节。内存地址由addr给出。

PTRACE_POKEUSR
往USER区域中写入一个字节。偏移量为addr。

PTRACE_SYSCALL, PTRACE_CONT
重新运行。

PTRACE_KILL
杀掉子进程,使它退出。

PTRACE_SINGLESTEP
设置单步执行标志

PTRACE_ATTACH
跟踪指定pid 进程。

PTRACE_DETACH
结束跟踪

Intel386特有:

PTRACE_GETREGS
读取寄存器

PTRACE_SETREGS
设置寄存器

 PTRACE_GETFPREGS
读取浮点寄存器

 PTRACE_SETFPREGS
设置浮点寄存器

返回值

成功返回0,出错返回-1;

注意:init进程不可以使用此函数

ptrace通过如下调用进入内核的

1
2
// 在linux/kernel/ptrace.c文件中
SYSCALL_DEFINE4(ptrace, long, request, long, pid, long, addr, long, data)

小试牛刀–简单的PTRACE_ME

这里首先使用PTRACE_ME实现父进程跟踪子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <asm/ptrace-abi.h> /*ORIG_EAX*/

int main()
{

pid_t child;
long orig_eax;

child = fork(); /*创建子进程*/
if(child == 0){
ptrace(PTRACE_TRACEME, 0, NULL, NULL);/*通知内核trace me*/
execl("/bin/ls","ls",NULL);
} else {
wait(NULL); /*等待子进程的通知*/
/*获取子进程的控制权后,就开始查看子进程的寄存器等值*/
orig_eax = ptrace(PTRACE_PEEKUSER, child, 4*ORIG_EAX, NULL);
printf("The child made a system call %d\n",orig_eax);
/*查完信息后,将控制权还给子进程,让其接着运行*/
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}

运行结果:

1
2
3
~/code/ptrace# ./ptrace1 
The child mad a system call 11
~/code/ptrace# ptrace1 ptrace1.c //此是ls执行结果

上述程序首先通过fork创建了一个子进程,子进程中通过PTRACE_TRACEME来告诉内核,让其他程序来跟踪我,然后通过execl函数执行ls命令;父进程中通过wait等待来自内核的通知,收到通知后立即接管子进程,然后查看其eax的值即系统调用号,完成对子进程的操作后,通过PTRACE_CON将控制权还给子进程,子进程接着执行ls命令。

从上面的结果可知子进程在执行ls命令时,执行了11号系统调用。通过查看/usr/include/asm/unistd.h文件可知11是函数execve的系统调用号。

在出现系统调用后,内核会将eax(存放系统调用号)保存起来,所有通过使用PTRACE_PEEKUSER可以读取出这个值。

ptrace实现进程中断

我们知道gdb在调试程序的时候可以让其停留在端点处。下面我通过ptrace来实现其基本原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <asm/ptrace-abi.h>
#include <string.h>
#include <stdio.h>

const int long_size = sizeof(long);

/*主要通过PTRACE_PEEKDATA获取内存中的内容*/
void getdata(pid_t child, long addr,
char *str, int len)

{

char *laddr;
int i, j;
union u{
long val;
char chars[long_size];
}data;

i = 0;
j = len / long_size;
laddr = str;

while(i < j) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = ' ';
}

/*与getdata相反,主要通过PTRACE_POKEDATA向内存写内容*/
void putdata(pid_t child, long addr,
char *str, int len)

{

char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;

i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}

int main(int argc, char *argv[])
{

pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
/* int 0x80, int3 */
char code[] = {0xcd,0x80,0xcc,0}; //将要嵌入的代码
char backup[4]; //用于保存原内存内容便于恢复
if(argc != 2) {
printf("Usage: %s <pid to be traced> ",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
/*attack 目标进程traced_process*/
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL); //等待系统通知
/*通过PTRACE_GETREGS获取目标进程的所有寄存器值*/
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
/* 将原指令备份到backup中 */
getdata(traced_process, regs.eip, backup, 3);
/* 将端点指令写入内存中 */
putdata(traced_process, regs.eip, code, 3);
/* 让目标进程继续执行并执行我们插入的int 3指令 */
ptrace(PTRACE_CONT, traced_process, NULL, NULL);
wait(NULL);
printf("The process stopped, putting back "
"the original instructions ");
printf("Press <enter> to continue ");
getchar();
/*将备份的原指令恢复到内存中*/
putdata(traced_process, regs.eip, backup, 3);
/* 让eip寄存器执向的内存恢复到执行原始位置,让目标进程继续执行 */
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
/* detach 目标进程*/
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;

}

目标测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<unistd.h>
#include<stdio.h>

int main()
{

int i;
printf("pid=%d\n",getpid());
for(i = 0;i < 10; ++i) {
printf("My counter: %d \n", i);
sleep(2);
}
return 0;
}

先运行目标程序

1
2
3
4
5
6
7
8
9
10
11
12
13
~/code/ptrace# ./test
pid=29158
My counter: 0
My counter: 1
My counter: 2
My counter: 3
//此处中断
My counter: 4
My counter: 5
My counter: 6
My counter: 7
My counter: 8
My counter: 9

test执行中执行ptrace程序

1
2
~/code/ptrace# ./ptrace2 30323
The process stopped, putting back the original instructions Press <enter> to continue //回车后中断结束

ptrace实现进程代码注入

上面的例子我们使用了我们仅仅向内存中插入了int 3指令,是进程中断。我们当然可以将一段shellcode写入执行…

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <asm/ptrace-abi.h>
#include <string.h>
#include <stdio.h>

const int long_size = sizeof(long);

void getdata(pid_t child, long addr,
char *str, int len)

{

char *laddr;
int i, j;
union u{
long val;
char chars[long_size];
}data;

i = 0;
j = len / long_size;
laddr = str;

while(i < j) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = ' ';
}

void putdata(pid_t child, long addr,
char *str, int len)

{

char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;

i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}
long freespaceaddr(pid_t pid)
{

FILE *fp;
char filename[30];
char line[85];
long addr;
char str[20];
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 85, fp) != NULL) {
sscanf(line, "%lx-%*lx %*s %*s %s", &addr,
str, str, str, str);
if(strcmp(str, "00:00") == 0)
break;
}
fclose(fp);
return addr;
}
int main(int argc, char *argv[])
{

pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
int len = 41;
char insertcode[] =
"\xeb\x15\x5e\xb8\x04\x00"
"\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba"
"\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff"
"\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f"
"\x72\x6c\x64\x0a\x00";
char backup[len];
if(argc != 2) {
printf("Usage: %s <pid to be traced> ",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
getdata(traced_process, regs.eip, backup, len);
putdata(traced_process, regs.eip,
insertcode, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
ptrace(PTRACE_CONT, traced_process,
NULL, NULL);
wait(NULL);
printf("The process stopped, Putting back "
"the original instructions ");
putdata(traced_process, regs.eip, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
printf("Letting it continue with "
"original flow ");
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}

这里我们只是注如了一个hello world程序进行测试。
这里总体思路与前小节一样。由于上面往内存中嵌入的只是四个字节,而当我们将把一段代码直接像之前一样简单的写入正在执行的指令流中是不现实的,所以这里我们需要将代码指令插入到进程的自由内存空间中。通过查看/proc/PID/maps文件就可以获得可用的进程自由空间分布。

这里我们先介绍下进程的内存映射

查看进程内存映射命令

cat /proc/进程id/maps

我们运行上面的test程序然后查看其内存映射如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~/code/ptrace# cat /proc/30617/maps 
005bf000-005c0000 r-xp 00000000 00:00 0 [vdso]
009a4000-00af7000 r-xp 00000000 08:01 134385 /lib/tls/i686/cmov/libc-2.11.1.so
00af7000-00af8000 ---p 00153000 08:01 134385 /lib/tls/i686/cmov/libc-2.11.1.so
00af8000-00afa000 r--p 00153000 08:01 134385 /lib/tls/i686/cmov/libc-2.11.1.so
00afa000-00afb000 rw-p 00155000 08:01 134385 /lib/tls/i686/cmov/libc-2.11.1.so
00afb000-00afe000 rw-p 00000000 00:00 0
00bf8000-00c13000 r-xp 00000000 08:01 129989 /lib/ld-2.11.1.so
00c13000-00c14000 r--p 0001a000 08:01 129989 /lib/ld-2.11.1.so
00c14000-00c15000 rw-p 0001b000 08:01 129989 /lib/ld-2.11.1.so
08048000-08049000 r-xp 00000000 08:01 149844 /root/code/ptrace_1/test1
08049000-0804a000 r--p 00000000 08:01 149844 /root/code/ptrace_1/test1
0804a000-0804b000 rw-p 00001000 08:01 149844 /root/code/ptrace_1/test1
b77d5000-b77d6000 rw-p 00000000 00:00 0
b77e3000-b77e6000 rw-p 00000000 00:00 0
bfa31000-bfa46000 rw-p 00000000 00:00 0 [stack]

显然/proc/PID/maps是个临时的文件,进程结束也就消失了。
查看该文件,瞅瞅进程的虚拟地址空间是如何使用的。该文件一共分为6列,每一列具体含义如下:

地址 权限 偏移量 设备 节点 路径
库在进程中的内存范围 虚拟内存的权限 虚拟内存区域在映射文件中的偏移 映射文件的主/次设备号 映射文件的节点号 映射文件路径

函数freespaceaddr的主要功能就是查找空闲的内存,具体就不分析了。

执行测试测试程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
~/code/ptrace# ./test
pid=31166
My counter: 0
My counter: 1
My counter: 2
My counter: 3
My counter: 4
Hello World
My counter: 5
My counter: 6
My counter: 7
My counter: 8
My counter: 9
1
2
~/code/ptrace# ./ptrace3 31166
The process stopped, Putting back the original instructions Letting it continue with original flow

由上面的结果可以看出我们已将一段指令代码插入到了目标进程中。

注:VSDS(Virtual Dynamically-lined Shared Object),这是一个由内核提供的虚拟.so文件,它不在磁盘上,而在内核里,由内核将其映射到一个地址空间中,被所有的程序共享

玩转系统调用

第一个例子中我们可以窥视了一个进程的系统调用号。

那我们可以过滤我们希望的进程调用,然后干一些好玩的事。例如我们对某一进程的write系统调用感兴趣。

首先,我们了解write函数

1
ssize_t write(int handle, void *buf, int nbyte);

handle 是文件描述符;
buf 是指定的缓冲区,即指针,指向一段内存单元;
nbyte 是要写入文件指定的字节数;
返回值:写入文档的字节数(成功);-1(出错)

我们在当前工作目录下,通过strace ls命令可以得到

1
2
3
4
5
6
7

...
write(1, "ptrace1 ptrace2 ptrace3\t p"..., 47ptrace1 ptrace2 ptrace3 ptrace4 test
) = 47
write(1, "ptrace1.c ptrace2.c ptrace3.c "..., 51ptrace1.c ptrace2.c ptrace3.c ptrace4.c test.c
) = 51
...

我们直接看下ls命令的结果:

1
2
3
~/code/ptrace# ls
ptrace1 ptrace2 ptrace3 ptrace4 test
ptrace1.c ptrace2.c ptrace3.c ptrace4.c test.c

然后,我们知道一个系统掉用时传参会依次存放在EBX、ECX、EDX而系统调用号会保存在EAX寄存器中,所以我们可以先获取EAX中的值,过滤出我们感兴趣的系统调用,这里我们过滤SYS_write系统调用,然后通过EAX、EBX、ECX中的值来改变传入参数…

我们依然创建一个子进程执行ls命令,然后过滤lswrite系统调用,改变其参数。。。
测测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <asm/ptrace-abi.h>
#include <sys/syscall.h>

const int long_size = sizeof(long);

void getdata(pid_t child, long addr,
char *str, int len)

{ char *laddr;

int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
data.val = ptrace(PTRACE_PEEKDATA,
child, addr + i * 4,
NULL);
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
data.val = ptrace(PTRACE_PEEKDATA,
child, addr + i * 4,
NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}
void putdata(pid_t child, long addr,
char *str, int len)

{ char *laddr;

int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}
int main()
{

pid_t child;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else {
long orig_eax;
long params[3];
int status;
char *str, *laddr;
int toggle = 0;
while(1) {
wait(&status);
if(WIFEXITED(status))
break;
orig_eax = ptrace(PTRACE_PEEKUSER,
child, 4 * ORIG_EAX,
NULL);
if(orig_eax == SYS_write) { //过滤出SYS_write系统调用
if(toggle == 0) {
toggle = 1;
params[0] = ptrace(PTRACE_PEEKUSER,
child, 4 * EBX,
NULL);
params[1] = ptrace(PTRACE_PEEKUSER,
child, 4 * ECX,
NULL);
params[2] = ptrace(PTRACE_PEEKUSER,
child, 4 * EDX,
NULL);
str = (char *)malloc((params[2]+1)
* sizeof(char));
getdata(child, params[1], str,
params[2]);
//printf("para 0:%d\n",params[0]);
//printf("para 1:%x\n",params[1]);
//printf("para 2:%d\n",params[2]);
//printf("length of str:%d %s\n",strlen(str),str);
char *hi_str = "Hi,Monkee here!\t ";
strncpy(str,hi_str,strlen(hi_str)+1);
putdata(child, params[1], str,strlen(hi_str)+1);
//ptrace(PTRACE_POKEUSER, child, 4*EDX, params[2]);
}
else {
toggle = 0;
}
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

运行结果:

1
2
./ptrace4 
Hi,Monkee here! Hi,Monkee here!

可以看到,我们用Hi,Monkee here!替换了正常输出。

参考

文章目录
  1. 1. patrace简介
  2. 2. 小试牛刀–简单的PTRACE_ME
  3. 3. ptrace实现进程中断
  4. 4. ptrace实现进程代码注入
  5. 5. 玩转系统调用
  6. 6. 参考