debugger-impl

How debuggers work: Part 1 - Basics - Eli Bendersky’s website
单纯的复现此文章。

# single step

首先是 ptrace

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

对于第一个 request 的参数

  • PTRACE_TRACEME  request, which means that this child process asks the OS kernel to let its parent trace it.
  • 这个参数只用于子进程,所以这里是在 fork 之后

第三个对地址进行和第四个是地址操作相关,根据 request 的类型,是对这个 addr 进行 poke,或者 peek 这个数据到 data 里(或者直接 peek 到返回值)。

Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(). Also, all subsequent calls to exec() by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution. A process probably shouldn’t make this request if its parent isn’t expecting to trace it. (pid, addr, and data are ignored.)

wait () 函数等待子进程终止,并设置相应的状态码。(不过这里貌似不是终止,而是 STOP)感觉是英文的 terminate 包括 stop、
WIFSTOPPED (sstatus) 判读是不是被信号传递而停止

   WIFSTOPPED(wstatus)
          returns true if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or
          when the child is being traced (see ptrace(2)).

子进程利用 (ptrace(PTRACE_TRACEME, 0, 0, 0) 向 OS 发送请求要求父进程 watch 自己,然后 execl 的时候发送 SIGTRAP 给父进程。
然后父进程里就不断发送单步信号和 wait 等待。
父进程中循环结束的条件是子进程发送 exit 的信号,WIFEXITED。

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
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/types.h"
#include <sys/ptrace.h>
#define procmsg(s...) printf(s)
#define TODO do {\
printf("TODO: %s\n", __FUNCTION__); \
exit(1); \
} while (0);

void run_target(char * programname){
procmsg("target started. will run '%s'\n", programname);

/* Allow tracing of this process */
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
perror("ptrace");
return;
}

/* Replace this process's image with the given program */
execl(programname, programname, 0);
}
void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
procmsg("debugger started\n");

/* Wait for child to stop on its first instruction after execl */
wait(&wait_status);

while (WIFSTOPPED(wait_status)) {
icounter++;
/* Make the child execute another instruction */
if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
perror("ptrace");
return;
}

/* Wait for child to stop on its next instruction */
wait(&wait_status);
}

procmsg("the child executed %u instructions\n", icounter);
}
int main(int argc, char** argv)
{
pid_t pid;

if (argc < 2) {
fprintf(stderr, "Expected a program name as argument\n");
return -1;
}

pid = fork();
if (pid == 0) /* child process */
run_target(argv[1]);
else if (pid > 0) /* parent process */
run_debugger(pid);
else {
perror("fork");
return -1;
}

return 0;
}

对于它监听的 victim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.intel_syntax noprefix
.section .text
.global _start

_start:
mov rdi, 1
lea rsi, .msg[rip]
mov rdx, 20
mov rax, 1 // sys_write callno
syscall

mov rax, 60
syscall

.section .data
.msg:
.string "hello debugger\n\0"
// as -o victim.o -s victim.S
// ld -o victim victim.o

然后引入一个 sys/user.h 的 header 用于调试。

/* The whole purpose of this file is for GDB and GDB only.
Don’t read too much into it. Don’t use it for
anything other than GDB unless know what you are
doing. */

PTRACE_PEEKTEXT 进行指定地址的内存获取

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
void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
pntmsg("debugger started\n");

/* Wait for child to stop on its first instruction after execl */
wait(&wait_status);

while (WIFSTOPPED(wait_status)) {
icounter++;
struct user_regs_struct regs;

ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, 0);
pntmsg("icounter = %u, instr = 0x%08x, rip = 0x%08x, rax = 0x%08x\n",
icounter, instr, regs.rip, regs.rax
);
/* Make the child execute another instruction */
Ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0);

/* Wait for child to stop on its next instruction */
wait(&wait_status);
}

pntmsg("the child executed %u instructions\n", icounter);
}

一次返回值是 4 字节

1
2
3
4
5
6
7
8
9
10
11
[PNT] debugger started
[CHD] target started. will run './victim'
[PNT] icounter = 1, instr = 0x01c7c748, rip = 0x00401000, rax = 0x00000000
[PNT] icounter = 2, instr = 0xf2358d48, rip = 0x00401007, rax = 0x00000000
[PNT] icounter = 3, instr = 0x14c2c748, rip = 0x0040100e, rax = 0x00000000
[PNT] icounter = 4, instr = 0x01c0c748, rip = 0x00401015, rax = 0x00000000
[PNT] icounter = 5, instr = 0xc748050f, rip = 0x0040101c, rax = 0x00000001
hello debugger
[PNT] icounter = 6, instr = 0x3cc0c748, rip = 0x0040101e, rax = 0x00000014
[PNT] icounter = 7, instr = 0x0000050f, rip = 0x00401025, rax = 0x0000003c
[PNT] the child executed 7 instructions

# breakpoint

中断分为软中断和硬中断.
硬中断有专门的电器设备去处理。
对于一条指令,debugger 会把它的首指令替换为 int 3 (0xcc).

objdump 出指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> objdump -d victim      

victim: file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
401000: 48 c7 c7 01 00 00 00 mov $0x1,%rdi
401007: 48 8d 35 f2 0f 00 00 lea 0xff2(%rip),%rsi # 402000 <.msg>
40100e: 48 c7 c2 14 00 00 00 mov $0x14,%rdx
401015: 48 c7 c0 01 00 00 00 mov $0x1,%rax
40101c: 0f 05 syscall
40101e: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
401025: 0f 05 syscall

获取第一条指令地址 401000

在这条地址上取出指令 设置为 0xcc 然后写回

  • PTRACE_CONT : Restart the stopped tracee process.
  • PTRACE_POKETEXT, PTRACE_POKEDATA 这两个是 equivalent 的
  • strsignal 一定要包含头文件 string.h 否则会引用到内核头文件中去。
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
Ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
unsigned instr = Ptrace(PTRACE_PEEKTEXT, child_pid,(void *)addr,(void *)0);

/* modify instruction in addr 0x401000 to 0xcc */
unsigned trap_instr = (instr & 0xffffff00) | 0xcc;
Ptrace(PTRACE_POKETEXT, child_pid, (void *)addr, trap_instr);
unsigned readback = Ptrace(PTRACE_PEEKTEXT, child_pid,(void *)addr,(void *)0);

pntmsg("instruction 0x%08x in addr 0x%08x\n", readback, addr);
// pause();

Ptrace(PTRACE_CONT, child_pid, 0, 0);

wait(&wait_status);
if (WIFSTOPPED(wait_status)) {
pntmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));
}
else {
perror("wait");
return;
}

/* See where the child is now */
Ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
pntmsg("Child stopped at RIP = 0x%08x\n", regs.rip);
pause();
1
2
3
4
5
[PNT] debugger started
[CHD] target started. will run './victim'
[PNT] instruction 0x01c7c7cc in addr 0x00401000
[PNT] Child got a signal: Trace/breakpoint trap
[PNT] Child stopped at RIP = 0x00401001

可以看到 rip 移动了一位,需要移动回去,然后覆写这原来的位置,就能继续运行了

1
2
3
4
5
6
7
8
9
10
11
12
regs.rip -= 1;
Ptrace(PTRACE_SETREGS, child_pid, 0, &regs);
Ptrace(PTRACE_POKETEXT, child_pid, regs.rip, instr);
pntmsg("icounter = %u, instr = 0x%08x, rip = 0x%08x\n",
icounter, instr, regs.rip
);
// pause();
/* Make the child execute another instruction */
Ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0);
pause();
/* Wait for child to stop on its next instruction */
wait(&wait_status);

另外 这个提取 rip 然后加减写的操作可以被封装好

  • create_breakpoint
  • resume_from_breakpoint
    对于 ptrace 很坑的一点在于,有的时候返回值为负数不正常,有的时候却正常。需要区别对待
    现在写一个新的 victim 实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void do_stuff()
{
printf("Hello, ");
}

int main()
{
for (int i = 0; i < 4; ++i)
do_stuff();
printf("world!\n");
return 0;
}
// gcc -o victim victim.c -no-pie
1
2
3
4
5
6
7
8
9
10
0000000000401156 <do_stuff>:
401156: f3 0f 1e fa endbr64
40115a: 55 push %rbp
40115b: 48 89 e5 mov %rsp,%rbp
40115e: 48 8d 3d 9f 0e 00 00 lea 0xe9f(%rip),%rdi # 402004 <_IO_stdin_used+0x4>
401165: b8 00 00 00 00 mov $0x0,%eax
40116a: e8 f1 fe ff ff callq 401060 <printf@plt>
40116f: 90 nop
401170: 5d pop %rbp
401171: c3 retq

如果要命中一个断点多次,就是先清除这个断点,然后让它回滚,再单步,再写回这个断点

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
/*
1. get regs
2. rollback rip and write back
3. single step
4. re-enable breakpoint
*/
int resume_from_breakpoint(pid_t pid, debug_breakpoint* dbp){
struct user_regs_struct regs;
int wait_status;

ptrace(PTRACE_GETREGS, pid, 0, &regs);
printf("[DBG] 0x%08x 0x%08x\n", dbp->addr, regs.rip);
assert(dbp->addr + 1 == regs.rip);

regs.rip -= 1;
ptrace(PTRACE_SETREGS, pid, 0, &regs);
disable_breakpoint(pid, dbp);
printf("[DBG] before single step %p\n", get_child_rip(pid));

if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0) < 0) {
perror("ptrace");
return -1;
}

wait(&wait_status);
printf("[DBG] %s at (%s:%u)\n", strsignal(WSTOPSIG(wait_status)), __FILE__, __LINE__);

if (WIFEXITED(wait_status)) {
return 0;
}

enable_breakpoint(pid, dbp);

if (ptrace(PTRACE_CONT, pid, 0, 0) < 0) {
perror("ptrace");
return -1;
}
wait(&wait_status);

if(WIFEXITED(wait_status)) {
return 0;
}
else if (WIFSTOPPED(wait_status)) {
return 1;
}
else
return -1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (1) {
pntmsg("child stopped at breakpoint. RIP = %p\n", get_child_rip(child_pid));
pntmsg("resuming\n");
int rc = resume_from_breakpoint(child_pid, dbp);

if (rc == 0) {
pntmsg("child exited\n");
break;
}
else if (rc == 1) {
continue;
}
else {
pntmsg("unexpected: %d\n", rc);
break;
}
}

# debug info

ELF 中用的 debug 信息格式是 DWARF
其中类似 DW_TAG_compile_unit 的是 dwarf 的 tag
ps: readelf 也能看到 readelf --debug-dump ./traceproc2 > readelf
ps: gcc 产生 dwarf gcc -gdwarf-4 -no-pie -o traceproc2 traceproc2.c

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
> objdump --dwarf=info ./traceproc2 

./traceproc2: file format elf64-x86-64

Contents of the .debug_info section:

Compilation Unit @ offset 0x0:
Length: 0x343 (32-bit)
Version: 4
Abbrev Offset: 0x0
Pointer Size: 8
<0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
<c> DW_AT_producer : (indirect string, offset: 0x18): GNU C17 9.4.0 -mtune=generic -march=x86-64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection
<10> DW_AT_language : 12 (ANSI C99)
<11> DW_AT_name : (indirect string, offset: 0x143): traceproc2.c
<15> DW_AT_comp_dir : (indirect string, offset: 0x1bd): /home/squ/proj/debugger-impl/debugger-impl/subject
<19> DW_AT_low_pc : 0x401136 <-< do_stuff
<21> DW_AT_high_pc : 0x60
<29> DW_AT_stmt_list : 0x0
<1><2d>: Abbrev Number: 2 (DW_TAG_typedef)
<2e> DW_AT_name : (indirect string, offset: 0xc5): size_t
<32> DW_AT_decl_file : 2
<33> DW_AT_decl_line : 209
<34> DW_AT_decl_column : 23
<35> DW_AT_type : <0x39>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0000000000401136 <do_stuff>:
401136: f3 0f 1e fa endbr64
40113a: 55 push %rbp
40113b: 48 89 e5 mov %rsp,%rbp

(···)

40117b: c9 leaveq
40117c: c3 retq

000000000040117d <main>:
40117d: f3 0f 1e fa endbr64
401181: 55 push %rbp

(···)

401195: c3 retq
401196: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)

再往下能看到两个 subprogram tab 的,记录了函数名 函数长度 和函数起始地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<1><2e2>: Abbrev Number: 16 (DW_TAG_subprogram)
<2e3> DW_AT_external : 1
<2e3> DW_AT_name : (indirect string, offset: 0x1f0): main <<- name
<2e7> DW_AT_decl_file : 1
<2e8> DW_AT_decl_line : 14
<2e9> DW_AT_decl_column : 5
<2ea> DW_AT_type : <0x65>
<2ee> DW_AT_low_pc : 0x40117d <<- addr
<2f6> DW_AT_high_pc : 0x19 <<- offset of end
<2fe> DW_AT_frame_base : 1 byte block: 9c (DW_OP_call_frame_cfa)
<300> DW_AT_GNU_all_tail_call_sites: 1
<1><300>: Abbrev Number: 17 (DW_TAG_subprogram)
<301> DW_AT_external : 1
<301> DW_AT_name : (indirect string, offset: 0x1a1): do_stuff
<305> DW_AT_decl_file : 1
<306> DW_AT_decl_line : 4
<307> DW_AT_decl_column : 6
<308> DW_AT_prototyped : 1
<308> DW_AT_low_pc : 0x401136
<310> DW_AT_high_pc : 0x47
<318> DW_AT_frame_base : 1 byte block: 9c (DW_OP_call_frame_cfa)
  • 第一个 <> 为嵌套深度
  • 对于标记了 DW_TAG_variable tab 的变量。DW_AT_type 能根据第二列 < > 里的数字找到对应的定义(大小类型)
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
<1><300>: Abbrev Number: 17 (DW_TAG_subprogram)
<301> DW_AT_external : 1
<301> DW_AT_name : (indirect string, offset: 0x1a1): do_stuff
<305> DW_AT_decl_file : 1
<306> DW_AT_decl_line : 4
<307> DW_AT_decl_column : 6
<308> DW_AT_prototyped : 1
<308> DW_AT_low_pc : 0x401136
<310> DW_AT_high_pc : 0x47
<318> DW_AT_frame_base : 1 byte block: 9c (DW_OP_call_frame_cfa)
<31a> DW_AT_GNU_all_tail_call_sites: 1
<2><31a>: Abbrev Number: 18 (DW_TAG_formal_parameter)
<31b> DW_AT_name : (indirect string, offset: 0x11a): my_arg
<31f> DW_AT_decl_file : 1
<320> DW_AT_decl_line : 4
<321> DW_AT_decl_column : 19
<322> DW_AT_type : <0x65>
<326> DW_AT_location : 2 byte block: 91 5c (DW_OP_fbreg: -36)
<2><329>: Abbrev Number: 19 (DW_TAG_variable)
<32a> DW_AT_name : (indirect string, offset: 0xcc): my_local
<32e> DW_AT_decl_file : 1
<32f> DW_AT_decl_line : 6
<330> DW_AT_decl_column : 9
<331> DW_AT_type : <0x65>
<335> DW_AT_location : 2 byte block: 91 6c (DW_OP_fbreg: -20)
<2><338>: Abbrev Number: 20 (DW_TAG_variable)
<339> DW_AT_name : i
<33b> DW_AT_decl_file : 1
<33c> DW_AT_decl_line : 7
<33d> DW_AT_decl_column : 9
<33e> DW_AT_type : <0x65>
<342> DW_AT_location : 2 byte block: 91 68 (DW_OP_fbreg: -24)
1
2
3
4
<1><65>: Abbrev Number: 5 (DW_TAG_base_type)      <- 65
<66> DW_AT_byte_size : 4
<67> DW_AT_encoding : 5 (signed)
<68> DW_AT_name : int

DW_AT_locationDW_OP_fbreg 记录了相对某个给定的 stack frame 的偏移 (说是就是 rbp 然后 ± 一点 offset,但是我没找到)

The DW_OP_fbreg operation provides a signed LEB128 offset from the address specified by the location description in the DW_AT_frame_base attribute of the current function. (This is typically a “stack pointer” register plus or minus some offset. On more sophisticated systems it might be a location list that adjusts the offset according to changes in the stack pointer as the PC changes.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0000000000401136 <do_stuff>:
401136: f3 0f 1e fa endbr64
40113a: 55 push %rbp
40113b: 48 89 e5 mov %rsp,%rbp
40113e: 48 83 ec 20 sub $0x20,%rsp
401142: 89 7d ec mov %edi,-0x14(%rbp)
401145: 8b 45 ec mov -0x14(%rbp),%eax
401148: 83 c0 02 add $0x2,%eax
40114b: 89 45 fc mov %eax,-0x4(%rbp)
40114e: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
401155: eb 1a jmp 401171 <do_stuff+0x3b>
401157: 8b 45 f8 mov -0x8(%rbp),%eax
40115a: 89 c6 mov %eax,%esi
40115c: 48 8d 3d a1 0e 00 00 lea 0xea1(%rip),%rdi # 402004 <_IO_stdin_used+0x4>
401163: b8 00 00 00 00 mov $0x0,%eax
401168: e8 d3 fe ff ff callq 401040 <printf@plt>
40116d: 83 45 f8 01 addl $0x1,-0x8(%rbp)
401171: 8b 45 f8 mov -0x8(%rbp),%eax
401174: 3b 45 fc cmp -0x4(%rbp),%eax
401177: 7c de jl 401157 <do_stuff+0x21>
401179: 90 nop
40117a: 90 nop
40117b: c9 leaveq
40117c: c3 retq

c 行数与汇编的映射关系通过以下得到

1
2
3
4
5
6
7
8
9
10
11
12
13
> objdump --dwarf=decodedline ./traceproc2 

./traceproc2: file format elf64-x86-64

Contents of the .debug_line section:

CU: ./traceproc2.c:
File name Line number Starting address View Stmt
traceproc2.c 5 0x401136 x
traceproc2.c 6 0x401145 x
traceproc2.c 9 0x40114e x
(··· ··· ···)
traceproc2.c 18 0x401196 x