这次学习Linux进程调试相关的知识。

调试

对于二进制选手来说,调试的重要性不言而喻,对于Linux来说,基本就是 gdb 一家独大,其余插件只是给gdb起了锦上添花的一些作用罢了,那么下面就来学习一下 gdb 的内核。

ptrace

在Linux调试程序,离不开一个系统调用就是 ptrace(%rax=101,%eax=26),来看看这个函数原型:

1
2
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);

request 参数是一个枚举类,共有下面的几种情况(问chatgpt的):

请求 (Request) 说明
PTRACE_TRACEME 0 使调用进程变为被跟踪进程。在子进程调用 ptrace(PTRACE_TRACEME, ...) 后,父进程可以使用 PTRACE_ATTACH 进行跟踪。
PTRACE_PEEKTEXT 1 从目标进程的内存中读取一个字
PTRACE_PEEKDATA 2 从目标进程的内存中读取一个字
PTRACE_PEEKUSER 3 从目标进程的用户区域读取一个字
PTRACE_POKETEXT 4 向目标进程的内存写入一个字
PTRACE_POKEDATA 5 向目标进程的内存写入一个字
PTRACE_POKEUSER 6 向目标进程的用户区域写入一个字
PTRACE_CONT 7 继续执行目标进程。
PTRACE_KILL 8 终止目标进程。
PTRACE_SINGLESTEP 9 使目标进程执行单步操作。
PTRACE_ATTACH 16 附加到目标进程。
PTRACE_DETACH 17 从目标进程分离。
PTRACE_SYSCALL 24 让目标继续运行,直到进入和退出系统调用的时候停止。

主要内容也就是以上的表,下面我们可以一步步试试这个的用法。

附加,脱离,继续运行

PTRACE_TRACEME 主要应用的原因是会等待父进程的 ATTACH,因为有可能在附加的时候程序没有运行到指定的位置,因此需要使用这个断下来保证程序附加的位置正确。

PTRACE_ATTACH 会使得该进程成为附加进程的父进程并使得附加的进程马上停止运行,并等待父进程的下一步命令。

PTRACE_DETACH 会分离目标进程并让它继续运行。

PTRACE_CONT 会在附加的情况下让程序继续运行,一般来说,除非运行到断点或者发生致命错误又或者是正常退出时,调试进程才会收到结果。

示例:

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

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
while(cnt<5){
write(1,"1\n",2);
cnt++;
sleep(2);
}
exit(0);
}
else{
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
printf("I'll start subporcess after 4 seconds\n");
sleep(4);
ptrace(PTRACE_CONT,pid,NULL,NULL);
wait(NULL);
}
}

运行结果:

被附加程序在收到 SIGTRAP 信号的时候,会挂起进程并使得父进程的 WAIT 阻塞返回,此时父进程可以继续运行代码并指示子进程下一步的操作,这个信号可以是程序自主发出的,也可以是其它程序给它外加的(kill命令),同时也可以是它自己执行了断点指令 int 3

示例:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
while(cnt<5){
write(1,"1\n",2);
cnt++;
printf("kill myself by SIGTRAP\n");
kill(getpid(),SIGTRAP);
printf("instruction int 3 was executed\n");
asm("int $3\n");
}
exit(0);
}
else{
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
for(;;){
printf("I'll start subporcess after 1 seconds\n");
sleep(1);
ptrace(PTRACE_CONT,pid,NULL,NULL);
wait(&status);
if(WIFEXITED(status)){
printf("process exit\n");
break;
}
}
}
}

运行结果:

下面我们可以试试 PTRACE_DETACH,让主程序执行五次 PTRACE_CONT 之后马上分离进程,这样的话子进程再收到 SIG_TRAP 信号之后就不会通知父进程,而是根据 SIGTRAP 信号原本的处理方式进行了,SIGTRAP 默认是退出进程并转储核心。

示例:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
while(cnt<5){
write(1,"1\n",2);
cnt++;
printf("kill myself by SIGTRAP\n");
kill(getpid(),SIGTRAP);
printf("instruction int 3 was executed\n");
asm("int $3\n");
}
exit(0);
}
else{
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
for(int i=0;i<5;i++){
printf("I'll start subporcess after 1 seconds\n");
sleep(1);
ptrace(PTRACE_CONT,pid,NULL,NULL);
wait(&status);
if(WIFEXITED(status)){
printf("process exit\n");
break;
}
}
printf("Detach the subprocess\n");
ptrace(PTRACE_DETACH,pid,NULL,NULL);
wait(&status);
}
}

运行结果:

这里一直有一个之前的盲点,int 3 指令是指是触发一个异常,调试器附加会为这个异常创建一个管理函数,让异常捕获到调试器内,因此在没有附加调试器的情况下执行 int 3 指令会直接崩溃。

接管系统调用

主要是 PTRACE_SYSCALL 选项,它会让目标继续运行,知道发生系统调用的时候停止,并向调试进程发送信号。

示例程序:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>
#define STANDARD_SYSCALL(x) \
(\
x == __NR_read || \
x == __NR_write || \
x == __NR_open || \
x == __NR_execve|| \
x == __NR_fork \
)
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
int k;
char buffer[0x20];
while(cnt<2){
int fd=open("/etc/passwd",O_RDONLY);
read(fd,buffer,0x20);
printf(buffer);
sleep(0x5);
cnt++;
}
exit(0);
}
else{
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
for(;;){
long rax,rdi,rsi,rdx,rcx,rip;
ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
wait(&status);
if(WIFEXITED(status)){
printf("process exit\n");
break;
}
printf("system call appeared\n");
}
}
}

运行结果:

出现了这么多次系统调用的原因是因为程序在很多时刻都需要与操作系统进行交互,并且之前提到,进入和调用结束都会停止,下面我们可以进一步探究这些系统调用。

获取程序状态

程序运行状态最重要的就是寄存器了,使用 PTRACE_GETREGS 获取进程的寄存器情况,在第四个参数中,用一个 user_regs_struct 结构体指针保存,这里我们筛选系统调用,只留下 OPEN,READ,WRITEEXECVE

示例程序:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>
#define STANDARD_SYSCALL(x) \
(\
x == __NR_read || \
x == __NR_write || \
x == __NR_open || \
x == __NR_execve|| \
x == __NR_fork || \
x == __NR_openat \
)
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
int k;
char buffer[0x20];
while(cnt<2){
int fd=open("/etc/passwd",O_RDONLY);
read(fd,buffer,0x20);
printf(buffer);
sleep(0x5);
cnt++;
}
execve("/bin/ls","ls",NULL);
exit(0);
}
else{
int in_syscall=0;
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
for(;;){
long rax,rdi,rsi,rdx,rcx,rip;
ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
wait(&status);
if(WIFEXITED(status)){
printf("process exit\n");
break;
}
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
rax = regs.orig_rax;
rdi=regs.rdi;
rsi=regs.rsi;
rdx=regs.rdx;
rip=regs.rip;
if(!STANDARD_SYSCALL(rax))continue;
printf("syscall %ld %p %p %p rip=%p\n",rax,rdi,rsi,rdx,rip);
}
}
}

运行结果:

可以发现,被我们筛选之后,输出少了很多,同时也可以发现,每个系统调用出现了两次,因为进入和退出都会被捕获。最明显的应该是 write 系统调用了,所有的 IO 函数,只要涉及到输出,不管是终端输出还是文件输出,一定是用 write 去输出的,可以发现输出前后就被 write 的系统调用输出包围,说明进去的时候暂停了一次,出来的时候也暂停了一次,为了避免重复一般是用一个变量判断是否处于系统调用中。

这个写起来也很简单,就不放示例了,直接看结果:

读取目标进程内存

这个参数主要是 PTRACE_PEEKDATA,学完这个,对 ptrace 的掌握就差不多了,它是通过返回值来传递读取结果的,读取结果为 long 的值。

示例程序:

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>
#define STANDARD_SYSCALL(x) \
(\
x == __NR_read || \
x == __NR_write || \
x == __NR_open || \
x == __NR_execve|| \
x == __NR_fork || \
x == __NR_openat \
)
void readnbytes(int pid,void *addr,char *buffer,size_t nbytes){
long d=0;
for(int i=0;i<nbytes;i+=8){
d=ptrace(PTRACE_PEEKDATA,pid,addr+i,NULL);
*(long *)(buffer+i)=d;
}
}
void readstr(int pid,void *addr,char *buffer,size_t size){
long d;
for(int i=0;;i++){
d=ptrace(PTRACE_PEEKDATA,pid,addr+i*8,NULL);
*((long *)buffer+i)=d;
if(strlen(&d)<8||i*8>size)break;//检查零字节和缓冲区大小
}
}
int main() {
pid_t pid;
struct user_regs_struct regs;
int status;

pid=fork();
if(pid==0){
ptrace(PTRACE_TRACEME,0,NULL,NULL);
int cnt=0;
int k;
char buffer[0x20];
while(cnt<2){
int fd=open("/etc/passwd",O_RDONLY);
read(fd,buffer,0x20);
printf(buffer);
sleep(0x1);
cnt++;
close(fd);
}
execve("/bin/ls","ls",NULL);
exit(0);
}
else{
int in_syscall=0;
ptrace(PTRACE_ATTACH,pid,NULL,NULL);
for(;;){
long rax,rdi,rsi,rdx,rcx,rip;
ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
wait(&status);
if(WIFEXITED(status)){
printf("process exit\n");
break;
}

ptrace(PTRACE_GETREGS, pid, NULL, &regs);
rax = regs.orig_rax;
rdi=regs.rdi;
rsi=regs.rsi;
rdx=regs.rdx;
rip=regs.rip;
if(in_syscall){
char buffer[0x50]={0};
in_syscall=0;
long d=0,addr;
if(rax==__NR_openat){
readstr(pid,rsi,buffer,sizeof(buffer));
printf("subprocess call openat(%d,\"%s\",%d)\n",rdi,buffer,rdx);
}
else if(rax==__NR_read){
readnbytes(pid,rsi,buffer,rdx);
printf("read %d bytes from fd=%d\n",rdx,rdi);
printf("HEX MODE: \"");
for(int i=0;i<rdx;i++){
printf("\\x%02x",(unsigned char)buffer[i]);
}
putchar('"');
putchar(10);
printf("RAW MODE: ");
for(int i=0;i<rdx;i++){
if(buffer[i]>0x20&&buffer[i]<0x7F)putchar(buffer[i]);
}
putchar(10);
}
else if(rax==__NR_write){
readnbytes(pid,rsi,buffer,rdx);
printf("write %d bytes to fd=%d\n",rdx,rdi);
printf("HEX MODE: \"");
for(int i=0;i<rdx;i++){
printf("\\x%02x",(unsigned char)buffer[i]);
}
putchar('"');
putchar(10);
printf("RAW MODE: ");
for(int i=0;i<rdx;i++){
if(buffer[i]>0x20&&buffer[i]<0x7F)putchar(buffer[i]);
}
putchar(10);
}
else if(rax==__NR_execve){
readstr(pid,rdi,buffer,sizeof(buffer));
printf("CALL execve(\"%s\",%p,%p)\n",buffer,rsi,rdx);
}
continue;
}
in_syscall=1;
if(!STANDARD_SYSCALL(rax))continue;
}
}
}

运行结果:

总结

❀完结撒花❀