ptrace函数

ptrace 函数简介

函数原型

1
2
3
#include <sys/ptrace.h>

long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);

参数意义

  • request:请求ptrace执行的操作

  • pid:目标进程的ID

  • addr:目标进程的地址

  • data:根据request的不同而变化,向目标进程写入数据时,data存放要写入的数据的地址;从目标进程中读数据时,data接收返回数据的地址


request参数决定ptrace的行为以及后续的参数是如何被使用的,参数request的常用的值如下:

更多参数请移步官方文档   参数对应值参考/usr/include/linux/ptrace.h源码

描述

  ptrace()系统调用函数提供了一个进程(the “tracer”监视者)监察和控制另一个进程(the “tracee”被监视者)的方法。并且可以检查和改变tracee进程的内存和寄存器里的数据。它可以用来实现断点调试和系统调用跟踪。
  tracee首先需要被附着到tracer。在多线程进程中,每个线程都可以被附着到一个tracerptrace命令总是以ptrace(PTARCE_foo,pid,..)的形式发送到tracee进程。pidtracee线程ID
  一个进程可以通过调用fork函数创建子进程并让子进程执行PTRACE_TRACEME来初始化一个ptrace,然后通常子进程再调用execve()(如果当前进程被ptrace,execve()成功执行后 SIGTRAP信号量会被发送到当前进程)。一个进程也可以使用PTRACE_ATTACH或者PTRACE_SEIZE来跟踪另一个进程。
  当进程被跟踪后,每当信号量传给当前进程,甚至信号量被忽略时,tracee会暂停(SIGKILL除外)。tracer会在下次调用waitpid(或者其它wait系统调用)处被通知。该调用会返回一个包含tracee暂停原因信息的状态码。当tracee暂停后,tracer可以使用一系列ptrace请求来查看和修改tracee中的信息。tracer接着可以让tracee继续执行。tracee传递给tracer中的信号量通常被忽略(即使是一个不同的信号)。
  当PTRACE_O_TRACEEXEC项未起作用时,所有成功执行execve()tracee进程会被发送一个 SIGTRAP信号量后暂停,在新程序执行之前,父进程将会取得该进程的控制权。
  当tracer结束跟踪后,可以通过调用PTRACE_DETACHtracee在未被trace下继续执行。

  那么,ptrace会在什么时候出现呢?在执行系统调用之前,内核会先检查当前进程是否处于被“跟踪”(traced)的状态。如果是的话,内核暂停当前进程并将控制权交给跟踪进程,使跟踪进程得以察看或者修改被跟踪进程的寄存器。

示例

以下示例若无特殊说明均在该系统中测试

1
2
3
Operating System: Ubuntu 19.04
Kernel: Linux 5.0.0-19-generic
Architecture: x86-64

追踪系统调用号

系统调用

  操作系统提供了一种标准的服务来让程序员实现对底层硬件和服务的控制(比如文件系统),叫做系统调用(system calls)。当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后以不同方式调用,就像一个让程序得以接触到内核模式的窗口,程序将参数和系统调用号交给内核,内核来完成系统调用的执行。

详见附录 不同平台的系统调用方式 不同平台使用的参数寄存器

比如Write(2, “Hello”, 5)i386汇编形式大概是这样的

1
2
3
4
5
movl   $4, %eax
movl $2, %ebx
movl $hello, %ecx
movl $5, %edx
int $0x80

这里的$hello指向的是标准字符串”Hello”

开始追踪

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 <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h> /* For constants ORIG_RAX etc */

int main()
{
pid_t child;
long orig_rax;
child = fork();
if (child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else
{
wait(NULL);
orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
printf("The child made a system call %ld\n", orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
}
}

使用gcc编译该文件gcc ptrace.c -o ptrace 然后运行

ptrcer通过PTRACE_PEEKUSER访问user结构体读取RAX的值 第二个参数指定RAXuser中的偏移量。

reg.h中定义了各个变量在user.h中的偏移量,
/usr/include/x86_64-linux-gnu/reg.h源码
/usr/include/x86_64-linux-gnu/user.h源码

59是execve的系统调用号,这是该程序调用的第一个系统调用。系统调用号的详细内容,
64位linux请察看 /usr/include/x86_64-linux-gnu/asm/unistd_64.h
32位linux请察看 /usr/include/i386-linux-gnu/asm/unistd_32.h

  execl()函数对应的系统调用为__NR_execve,系统调用值为59。父进程通过调用fork()来创建子进程。在子进程中,先运行patrce().请求参数设为PTRACE_TRACE,来告诉内核当前进程被父进程trace,每当有信号量传递到当前进程,该进程会暂停,提醒父进程在wait()调用处继续执行。然后再调用execl()。当execl()函数成功执行后,继续运行之前,SIGTRAP信号量会被发送到该进程,让子进程停止,这时父进程会在wait相关调用处被通知,获取子进程的控制权,可以查看子进程内存和寄存器相关信息。当检查完系统调用之后,调用ptrace并设置参数PTRACE_CONT让子进程继续进行。

读取系统调用参数

1
ptrace(PTRACE_GETREGS, child, NULL, &regs);

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
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
pid_t child;
long orig_rax;
int status;
int iscalling = 0;
struct user_regs_struct regs;

child = fork();
if (child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", "-l", "-h", NULL);
}
else
{
while (1)
{
wait(&status);
if (WIFEXITED(status))
break;
orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
if (orig_rax == SYS_write)
{
ptrace(PTRACE_GETREGS, child, NULL, &regs);
if (!iscalling)
{
iscalling = 1;
printf("SYS_write call with %lld, %lld, %lld\n", regs.rdi, regs.rsi, regs.rdx);
}
else
{
printf("SYS_write call return %lld\n", regs.rax);
iscalling = 0;
}
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

结果

大部分的代码还是比较好懂,部分需要说明一下

对于PTRACE_STSCALL参数,该参数会像PTRACE_CONT一样使暂停的子进程继续执行,并在子进程下次进行系统调用前或系统调后,向子进程发送SINTRAP信号量,让子进程暂停。

WIFEXITED函数(宏)函数用来检查子进程是暂停还准备退出。

SYS_write被定义在/usr/include/x86_64-linux-gnu/bits/syscall.h里面# define SYS_write __NR_writeunistd.h对应  源码

修改子进程系统调用参数

1
2
val = ptrace(PTRACE_PEEKDATA,child,addr,NULL)
val = ptrace(PTRACE_PEEKTEXT,child,addr,NULL)

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/wait.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define long_size sizeof(long)

void reverse(char *str)
{
int i, j;
char temp;
for (i = 0, j = strlen(str) - 2; i <= j; ++i, --j)
{
temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}

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 * long_size, NULL);
if (data.val == -1)
{
if (errno)
{
printf("READ error: %s\n", strerror(errno));
}
}
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 * long_size, 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 * long_size, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if (j != 0)
{
//注意:由于写入时也是按字写入的,所以正确的做法是先将该字的高地址数据读出保存在data的高地址上 ,然后将该字再写入
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, addr + i * long_size, data.val);
}
}

int main()
{
pid_t child;
int status;
struct user_regs_struct regs;
child = fork();
if (child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else
{
long orig_eax;

char *str, *laddr;
int toggle = 0;
while (1)
{
wait(&status);
if (WIFEXITED(status))
break;
orig_eax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
if (orig_eax == SYS_write)
{
if (toggle == 0)
{
toggle = 1;
ptrace(PTRACE_GETREGS, child, NULL, &regs);

str = (char *)calloc((regs.rdx + 1), sizeof(char));
getdata(child, regs.rsi, str, regs.rdx);
reverse(str);
putdata(child, regs.rsi, str, regs.rdx);
}
else
{
toggle = 0;
}
}

ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

结果

向其它程序注入指令

1
ptrace(PTRACE_ATTACH, pid, NULL, NULL)


使pid进程成为被追踪的tracee进程。tracee进程会被发送一个SIGTOP信号量,tracee进程不会立即停止,直到完成本次系统调用。如果要结束追踪,则调用PTRACE_DETACH即可。

debug 设置断点的功能可以通过ptrace实现。原理是ATTACH正在运行的进程使其停止。然后读取该进程的指令寄存器IR(32位x86为EIP,64位的是RIP)内容所指向的指令,备份后替换成目标指令,再使其继续执行,此时被追踪进程就会执行我们替换的指令,运行完注入的指令之后,我们再恢复原进程的IR
,从而达到改变原程序运行逻辑的目的。

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

int main()
{
int i=0;
while(1)
{
printf("Hello,ptrace! [pid:%d]! num is %d\n",getpid(),i++);
sleep(2);
}
return 0;
}
tracer
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
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#define long_size sizeof(long)

void getdata(pid_t child, long addr, char *str, int len)
{
char *laddr = str;
int i, j;
union u {
long val;
char chars[long_size];
} data;
i = 0;
j = len / long_size;

while (i < j)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + long_size * i, NULL);
if (data.val == -1)
{
if (errno)
{
printf("READ error: %s\n", strerror(errno));
}
}
memcpy(laddr, data.chars, long_size);
++i;
laddr = laddr + long_size;
}

j = len % long_size;
if (j != 0)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + long_size * i, NULL);
if (data.val == -1)
{
if (errno)
{
printf("READ error: %s\n", strerror(errno));
}
}
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}

void putdata(pid_t child, long addr, char *str, int len)
{
char *laddr = str;
int i, j;
j = len / long_size;
i = 0;
union u {
long val;
char chars[long_size];
} data;
while (i < j)
{
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child, addr + long_size * i, data.val);
++i;
laddr = laddr + long_size;
}
j = len % long_size;
if (j != 0)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + long_size * i, NULL);
if (data.val == -1)
{
if (errno)
{
printf("READ error: %s\n", strerror(errno));
}
}

memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, addr + long_size * i, data.val);
}
}

int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage: %s pid\n", argv[0]);
}
pid_t tracee = atoi(argv[1]);
struct user_regs_struct regs;
/*int 80(系统调用) int 3(断点)*/
unsigned char code[] = {0xcd, 0x80, 0xcc, 0x00, 0, 0, 0, 0}; //八个字节,等于long 型的长度
char backup[8]; //备份读取的指令
ptrace(PTRACE_ATTACH, tracee, NULL, NULL);
long inst; //用于保存指令寄存器所指向的下一条将要执行的指令的内存地址

wait(NULL);
ptrace(PTRACE_GETREGS, tracee, NULL, &regs);
inst = ptrace(PTRACE_PEEKTEXT, tracee, regs.rip, NULL);
printf("tracee:RIP:0x%llx INST: 0x%lx\n", regs.rip, inst);
//读取子进程将要执行的 7 bytes指令并备份
getdata(tracee, regs.rip, backup, 7);
//设置断点
putdata(tracee, regs.rip, code, 7);
//让子进程继续执行并执行“int 3”断点指令停止
ptrace(PTRACE_CONT, tracee, NULL, NULL);

wait(NULL);
long rip = ptrace(PTRACE_PEEKUSER, tracee, 8 * RIP, NULL); //获取子进程停止时,rip的值
long inst2 = ptrace(PTRACE_PEEKTEXT, tracee, rip, NULL);
printf("tracee:RIP:0x%lx INST: 0x%lx\n", rip, inst2);

printf("Press Enter to continue tracee process\n");
getchar();
putdata(tracee, regs.rip, backup, 7); //重新将备份的指令写回寄存器
ptrace(PTRACE_SETREGS, tracee, NULL, &regs); //设置会原来的寄存器值
ptrace(PTRACE_CONT, tracee, NULL, NULL);
ptrace(PTRACE_DETACH, tracee, NULL, NULL);
return 0;
}

结果

在运行ptracerptracee被插入一个int 3断点

注意:ptracer应在root权限下运行

附录

不同平台的系统调用方式

不同平台的系统调用方式

不同平台使用的参数寄存器

不同平台使用的参数寄存器

参考文档

http://man7.org/linux/man-pages/man2/syscall.2.html
http://man7.org/linux/man-pages/man2/ptrace.2.html
https://blog.csdn.net/u012417380/article/details/60470075