在x64 linux下实现简易调试器

两只羊 Lv3

test

前言

这个项目很久之前写了,支持对x64的linux程序进行断点写hook脚本,trace等操作,曾经有那么一段时间,成为了我在CTF下带杀器。但非常可惜的是,AI时代到来,已经基本上没有用了,唉,时代的眼泪

那么我当时为什么要弄这个东西呢,这就说说我当时的一个需求了

比如说一个rc4加密,我想要直接拿到xor_stream,在0x158e偏移处,分别读取al和byte [rbp - 0x115]

image-20260511101539906

这个其实非常好做,在ida里下个断点,然后添加idapython脚本就行了

但问题来了,一个断点还好,如果三个甚至多个断点呢,那管理和编写起来就非常麻烦了。

那我为什么不用Frida hook呢,原因就在于fridahook是通过inlinehook,如果只是hook函数开头还好,但是hook中间的偏移,非常容易因为寄存器状态没恢复好,直接就崩了

然后我又学习到了unicorn框架,里面添加hook点简直舒服得我想哭,但在正式启动前,先把环境配好再说吧。

那么基于unicorn的qiling框架呢,很可惜在高版本libc下没办法正常运行,直接寄。

后面

而且在linux下,直接trace硬撕的脚本似乎也没有

那么综合以上所有问题,我萌生了自己写一个框架的想法

代码实现

首先来看调试器的两种启动模式,在debugger_init.c

解析参数就不说了,主要是调试器主体fork出子进程,然后进行一些初始化的操作

像读取基地址,存入子进程pid,初始化反汇编器和断点

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
int debugger_init(Debugger* dbg, const char* filepath, const char* params)
{
// 1. 【关键】先获取绝对路径
// 这解决了 "failed to find main module" 的问题
char abs_path[PATH_MAX];
if (realpath(filepath, abs_path) == NULL) {
perror("realpath failed (file not found?)");
return -1;
}

pid_t child = fork();
if (child == 0) {
// --- Child Process ---
ptrace(PTRACE_TRACEME, 0, NULL, NULL);

// 2. 【关键】构建参数数组
// 这解决了 "unexpected argument" 的问题
char* argv[256]; // 假设参数不超过 256 个
parse_args_for_exec(abs_path, params, argv, 256);

// 使用 execv 替代 execl
execv(abs_path, argv);

// 如果 execv 返回,说明出错了
perror("execv failed");
exit(1);
} else {
// --- Parent Process ---
int status;
waitpid(child, &status, 0);

// 保存绝对路径,而不是相对路径
// 这样后续 get_module_by_name 比较时才能匹配上 /proc/pid/maps 里的路径
dbg->filepath = strdup(abs_path);
dbg->child = child;
dbg->stat = CONTINUE;
dbg->cur_bk = NULL;

disasm_init(dbg);
get_modules(dbg);

// 现在使用绝对路径查找,应该能找到了
Module* main_module = get_module_by_name(dbg, dbg->filepath);
if (!main_module) {
fprintf(stderr, "failed to find main module: %s\n", dbg->filepath);
// 调试用:打印一下找到的所有模块路径,看看为什么匹配不上
// for(int i=0; i<dbg->module_count; i++) printf("Mod: %s\n", dbg->modules[i].name);
return -1;
}

dbg->base_addr = main_module->base_addr;
dbg->end_addr = main_module->end_addr;
dbg->breakpoints_count = 0;

dbg_printf("base_address => %p \n", dbg->base_addr);

QWORD entry = get_file_entry_point(dbg->filepath, dbg->base_addr);
dbg_printf("ELF entry_point => %p \n", (void*)entry);
dbg->entry_point = entry;

// 如果你需要在这里下断点,确保 hook_add 实现正确
hook_add(dbg, entry, hook_entry_point);
}
return 0;
}

然后就是启动循环的大循环体

调试器通过waitpid接受子进程传来的信号,并根据我们调试器自己的状态设置来决定

比如我设置了断点模式,就发送PTRACE_SINGLESTEP,如果单步时遇到syscall,还需要单独传PTRACE_SYSCALL信号

然后如果接收到异常,像是EXITED和SIGNALED,基本就是程序出问题或者别的原因退出了,需要直接中止调试。

WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP就是int3断点的信号

进入了处理断点的地方,也就是执行我们hook脚本的地方,当然如果没有的话,也直接恢复进程

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
int debugger_start(Debugger* dbg)
{
int status;
pid_t child = dbg->child;
while (1) {


switch (dbg->stat)
{
case CONTINUE:
{
ptrace(PTRACE_CONT, child, NULL, NULL);
waitpid(dbg->child, &status, 0);
break;
}
case TRACE:
case SINGLE:
{
ptrace(PTRACE_GETREGS, child, NULL, &dbg->regs);

QWORD bytes= mem_read_qword(dbg, dbg->regs.rip);
//printf("RIP = 0x%llx \n", dbg->regs.rip);
if(1)
{
if(!disasm_start(dbg, dbg->regs.rip, (BYTE*)&bytes, 8)) {
BYTE* _bytes[0x10];
mem_read_bytes(dbg, dbg->regs.rip,_bytes, 0x10);
disasm_start(dbg, dbg->regs.rip, _bytes, 0x10);
}
//printf("offset = 0x%llx \n", regs.rip - dbg->base_addr);
if(dbg->stat == SINGLE) {
getchar();
}
//dbg->stat = CONTINUE;
//break;
}



if ((bytes & 0xFFFF) == 0x050F || (bytes & 0xFFFF) == 0x80cd) { // syscall int80 opcode
//dbg_printf("syscall => %d \n", dbg->regs.rax);
if(dbg->regs.rax == SYS_exit_group) {
return 0;
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
} else {
ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
}
waitpid(child, NULL, 0);


break;
}

default:
dbg_printf("unknown stat \n");
exit(1);
}




if (WIFEXITED(status)) {
dbg_printf("child exit\n");
break;
}


if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
dbg_printf("child killed by signal %d\n", sig);
break;
}

if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
//dbg_printf("WIFSTOPPED => %d \n", WSTOPSIG(status));
handle_breakpoint(dbg);
}
}
}

接下来就看看添加breakpoint的实现

这里我是支持一个breakpoint添加多个hook脚本的。也就是执行多个回调函数的

主要就是把保存目标地址机器码,然后改为0xcc,也就是int3

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
int hook_add_with_args(struct Debugger* dbg, BK_TYPE type, QWORD addr, BK_callback callback)
{
if(dbg->breakpoints_count >= MAX_BREAKPOINTS) {
dbg_printf("breakpoints_count has been max \n");
return 0;
}

bool is_hook = false;
Breakpoint *bk = NULL;

for(int i = 0; i < dbg->breakpoints_count; i++) {
if(addr == dbg->breakpoints[i].addr ) {

//printf("%p \n", addr);
if(dbg->breakpoints[i].callback_count > MAX_BREAKPOINTS_CALLBACK) {
return 0;
}
//printf("bbbb \n");
is_hook = true;
bk = &dbg->breakpoints[i];

break;
}
}


if(is_hook == false) {
bk = &dbg->breakpoints[dbg->breakpoints_count];
bk->callback_count = 0;
bk->addr = addr;
bk->enable = 1;
BYTE data = mem_read_byte(dbg, addr);
bk->save_byte = data;
//dbg_printf("breakpoint => %p, %02x \n",addr, data);
mem_write_byte(dbg, addr, 0xCC);

} else {

}



bk->callback[bk->callback_count] = callback;
bk->type[bk->callback_count] = type;
bk->args[bk->callback_count] = NULL;


if(is_hook == false) {
dbg->breakpoints_count += 1;
}
bk->callback_count += 1;


return 0;
}

当前面在debugger_start的大循环体里接收到断点异常时,通过handle_breakpoint开始处理

首先会通过PTRACE_GETREGS来获取异常发生的寄存器状态,然后判断这个断点的发生位置,是否属于我们的断点列表,当找到对应的断点后,开始轮番执行hook的callback,执行完后,通过PTRACE_SETREGS,修改新的寄存器状态,默认情况下,新的rip即为断点发生是的rip-1,也就是断点地址

然后根据enable来判断是否要进行恢复断点

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
void handle_breakpoint(Debugger *dbg) {
pid_t child = dbg->child;
ptrace(PTRACE_GETREGS, child, NULL, &dbg->regs);

QWORD bk_addr = dbg->regs.rip - 1;
dbg->regs.rip = bk_addr;


Breakpoint *bk = NULL;

for(int i = 0; i < dbg->breakpoints_count; i++) {
if(bk_addr == dbg->breakpoints[i].addr) {
bk = &dbg->breakpoints[i];
break;

}
}

if(bk) {
dbg->cur_bk = bk;
//dbg_printf("bk->callback_count => %d \n", bk->callback_count);
for(int i = 0; i < bk->callback_count; i++) {
if(bk->type[i] == START) {
//printf("%p => \n", bk->callback[i]);
(bk->callback[i])(dbg, bk->args[i]);
} else if(bk->type[i] == RET) {
QWORD rsp = reg_read(dbg, REG_RSP);
QWORD ret_addr = mem_read_qword(dbg, rsp);
//dbg_printf("ret_addr => %p \n", ret_addr);
hook_add(dbg, ret_addr, bk->callback[i]);
}

}

mem_write_byte(dbg, bk_addr, bk->save_byte);
ptrace(PTRACE_SETREGS, child, NULL, &dbg->regs);

// 单步执行原始指令
ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
waitpid(child, NULL, 0);

if(bk->enable == 1)
mem_write_byte(dbg, bk_addr, 0xCC); // 恢复 int3 断点

//ptrace(PTRACE_GETREGS, child, NULL, &regs);
//dbg_printf("next rip => %p \n", dbg->regs.rip);
}

}

后面关于像reg_read,mem_read这些函数的封装就不一一列举了

总的来说大概如下,内存读写通过通过PTRACE_PEEKDATA和PTRACE_POKEDATA

寄存器读写直接操作reg结构体就好了

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
QWORD mem_read_qword(struct Debugger* dbg, QWORD addr)
{

QWORD data = ptrace(PTRACE_PEEKDATA, dbg->child, addr, NULL);
//dbg_printf("%p %llx \n",addr, data);
if (data == -1 && errno) {
//perror("PTRACE_PEEKDATA");
//exit(1);
}
return data;
}

int mem_write_byte(struct Debugger* dbg, QWORD addr, BYTE data)
{
QWORD init_data = mem_read_qword(dbg, addr);
QWORD patch_data = (init_data & ~0xff) | data;
//dbg_printf("data with bk => %llx \n", patch_data);
ptrace(PTRACE_POKEDATA, dbg->child, (void*)addr, patch_data);
return 1;
}

QWORD reg_read(Debugger* dbg, reg_name_t reg)
{
switch (reg) {
// ---------------- 64-bit ----------------
case REG_RAX: return dbg->regs.rax;
case REG_RBX: return dbg->regs.rbx;
case REG_RCX: return dbg->regs.rcx;
case REG_RDX: return dbg->regs.rdx;
case REG_RSI: return dbg->regs.rsi;
case REG_RDI: return dbg->regs.rdi;
case REG_RBP: return dbg->regs.rbp;
...
}
  • 标题: 在x64 linux下实现简易调试器
  • 作者: 两只羊
  • 创建于 : 2026-05-09 23:44:47
  • 更新于 : 2026-05-11 18:07:54
  • 链接: https://twogoat.github.io/2026/05/09/在x64-linux下实现简易调试器/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
在x64 linux下实现简易调试器