test
前言
这个项目很久之前写了,支持对x64的linux程序进行断点写hook脚本,trace等操作,曾经有那么一段时间,成为了我在CTF下带杀器。但非常可惜的是,AI时代到来,已经基本上没有用了,唉,时代的眼泪
那么我当时为什么要弄这个东西呢,这就说说我当时的一个需求了
比如说一个rc4加密,我想要直接拿到xor_stream,在0x158e偏移处,分别读取al和byte [rbp - 0x115]

这个其实非常好做,在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) { 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) { ptrace(PTRACE_TRACEME, 0, NULL, NULL);
char* argv[256]; parse_args_for_exec(abs_path, params, argv, 256);
execv(abs_path, argv);
perror("execv failed"); exit(1); } else { int status; waitpid(child, &status, 0);
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); 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(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); 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); } if(dbg->stat == SINGLE) { getchar(); } }
if ((bytes & 0xFFFF) == 0x050F || (bytes & 0xFFFF) == 0x80cd) { 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) { 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 ) { if(dbg->breakpoints[i].callback_count > MAX_BREAKPOINTS_CALLBACK) { return 0; } 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; 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; for(int i = 0; i < bk->callback_count; i++) { if(bk->type[i] == START) { (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); 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);
} }
|
后面关于像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); if (data == -1 && errno) { } 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; ptrace(PTRACE_POKEDATA, dbg->child, (void*)addr, patch_data); return 1; }
QWORD reg_read(Debugger* dbg, reg_name_t reg) { switch (reg) { 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; ... }
|