本文为看雪论坛精华文章
看雪论坛作者ID:ScUpax0s
本文想要达到的目的:
核心目标:了解GDB的调试原理,学习ptrace的使用。
支线目标:实现一个可以用来追踪的 tiny debugger(迷你调试器)。
-
tracer:追踪者
-
tracee:被追踪者
GDB 调试原理简述
gdb ./a.out
attach pid
gdb server的target remote
ptrace
long ptrace(enum __ptrace_request request, pid_t pid,void *addr, void *data);
The ptrace() system call provides a means by which one process
(the "tracer") may observe and control the execution of another
process (the "tracee"), and examine and change the tracee's
memory and registers. It is primarily used to implement
breakpoint debugging and system call tracing.
A tracee first needs to be attached to the tracer. Attachment
and subsequent commands are per thread: in a multithreaded
process, every thread can be individually attached to a
(potentially different) tracer, or left not attached and thus not
debugged. Therefore, "tracee" always means "(one) thread", never
"a (possibly multithreaded) process". Ptrace commands are always
sent to a specific tracee using a call of the form
While being traced, the tracee will stop each time a signal is
delivered, even if the signal is being ignored. (An exception is
SIGKILL, which has its usual effect.) The tracer will be
notified at its next call to waitpid(2) (or one of the related
"wait" system calls); that call will return a status value
containing information that indicates the cause of the stop in
the tracee. While the tracee is stopped, the tracer can use
various ptrace requests to inspect and modify the tracee. The
tracer then causes the tracee to continue, optionally ignoring
the delivered signal (or even delivering a different signal
instead).
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个字节,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个字节,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH,追踪指定pid的进程
PTRACE_DETACH, 结束追踪
(1)PTRACE_TRACEME
/**
* ptrace_traceme -- helper for PTRACE_TRACEME
*
* Performs checks and sets PT_PTRACED.
* Should be used by all ptrace implementations for PTRACE_TRACEME.
*/
static int ptrace_traceme(void)
{
int ret = -EPERM;
write_lock_irq(&tasklist_lock);
/* Are we already being traced? */
if (!current->ptrace) {
ret = security_ptrace_traceme(current->parent);
/*
* Check PF_EXITING to ensure ->real_parent has not passed
* exit_ptrace(). Otherwise we don't report the error but
* pretend ->real_parent untraces us right after return.
*/
if (!ret && !(current->real_parent->flags & PF_EXITING)) {
current->ptrace = PT_PTRACED;
__ptrace_link(current, current->real_parent);
}
}
write_unlock_irq(&tasklist_lock);
return ret;
}
(2)PTRACE_ATTACH
if (request == PTRACE_ATTACH) {
if (child == current)
goto out;
if ((!child->dumpable || //这里检查了进程权限
(current->uid != child->euid) ||
(current->uid != child->suid) ||
(current->uid != child->uid) ||
(current->gid != child->egid) ||
(current->gid != child->sgid) ||
(!cap_issubset(child->cap_permitted, current->cap_permitted)) ||
(current->gid != child->gid)) && !capable(CAP_SYS_PTRACE))
goto out;
if (child->flags & PF_PTRACED)
goto out;
child->flags |= PF_PTRACED; //设置进程标志位PF_PTRACED
write_lock_irqsave(&tasklist_lock, flags);
if (child->p_pptr != current) { //设置进程为当前进程的子进程。
REMOVE_LINKS(child);
child->p_pptr = current;
SET_LINKS(child);
}
write_unlock_irqrestore(&tasklist_lock, flags);
send_sig(SIGSTOP, child, 1); //向子进程发送一个SIGSTOP,使其停止
ret = 0;
goto out;
}
(3)PTRACE_CONT
case PTRACE_CONT:
long tmp;
ret = -EIO;
if ((unsigned long) data > _NSIG) //信号是否超过范围?
goto out;
if (request == PTRACE_SYSCALL)
child->flags |= PF_TRACESYS; //如果是PTRACE_SYSCALL就设置PF_TRACESYS标志
else
child->flags &= ~PF_TRACESYS; //如果是PF_CONT,去除PF_TRACESYS标志
child->exit_code = data; //设置继续处理的信号
tmp = get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG; //清除TRAP_FLAG
put_stack_long(child, EFL_OFFSET,tmp);
wake_up_process(child); //唤醒停止的子进程
ret = 0;
goto out;
(4)PTRACE_PEEKUSER
(5)PTRACE_SINGLESTEP
demo
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h> /* For constants ORIG_RAX etc */
#include <stdio.h>
int main()
{
char * argv[ ]={"ls","-al","/etc/passwd",(char *)0};
char * envp[ ]={"PATH=/bin",0};
pid_t child;
long orig_rax;
child = fork();
if(child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
printf("Try to call: execln");
execve("/bin/ls",argv,envp);
printf("child exitn");
}
else
{
wait(NULL); //等待子进程
orig_rax = ptrace(PTRACE_PEEKUSER,
child, 8 * ORIG_RAX,
NULL);
printf("The child made a "
"system call %ldn", orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
printf("Try to call:ptracen");
}
return 0;
root@ubuntu:~/tiny_debugger# ./demo1
Try to call: execl
The child made a system call 59
Try to call:ptrace
root@ubuntu:~/tiny_debugger# -rw-r--r-- 1 root root 2446 Dec 13 19:53 /etc/passwd
-
fork一个子进程。子进程标记为tracee。(PTRACE_TRACEME)
-
子进程调用execve,向父进程发送一个SIGCHLD。同时,子进程由于SIGTRAP停止。
-
父进程捕捉到SIGCHLD,同时使用ptrace获取子进程的系统调用号(59)
-
父进程告诉子进程继续执行(PTRACE_CONT),子进程输出ls的内容。
-
子进程的 printf("child exitn"); 不会被执行,因为execve丢弃原来的子进程execve()之后的部分,而子进程的栈、数据会被新进程的相应部分所替换。永不返回。
/**
* ptrace_event - possibly stop for a ptrace event notification
* @event: %PTRACE_EVENT_* value to report
* @message: value for %PTRACE_GETEVENTMSG to return
*
* Check whether @event is enabled and, if so, report @event and @message
* to the ptrace parent.
*
* Called without locks.
*/
static inline void ptrace_event(int event, unsigned long message)
{
if (unlikely(ptrace_event_enabled(current, event))) {
current->ptrace_message = message;
ptrace_notify((event << 8) | SIGTRAP);
} else if (event == PTRACE_EVENT_EXEC) {
/* legacy EXEC report via SIGTRAP */
if ((current->ptrace & (PT_PTRACED|PT_SEIZED)) == PT_PTRACED)
send_sig(SIGTRAP, current, 0);
}
}
-
通过 copy_from_user copy_to_user 读取与修改数据。
-
通过 copy_regset_from/to_user 访问寄存器。而寄存器数据保存在 task struct 中。
-
单步(Single Stepping):每步进(step)一次,CPU会一直执行到有分支、中断或异常。而ptrace通过设置对应的标志位在进程的thread_info.flags和MSR中打标启用单步调试。
void user_enable_single_step(struct task_struct *child)
{
enable_step(child, 0);
}
/*
* Enable single or block step.
*/
static void enable_step(struct task_struct *child, bool block)
{
/*
* Make sure block stepping (BTF) is not enabled unless it should be.
* Note that we don't try to worry about any is_setting_trap_flag()
* instructions after the first when using block stepping.
* So no one should try to use debugger block stepping in a program
* that uses user-mode single stepping itself.
*/
if (enable_single_step(child) && block)
set_task_blockstep(child, true);
else if (test_tsk_thread_flag(child, TIF_BLOCKSTEP))
set_task_blockstep(child, false);
}
void set_task_blockstep(struct task_struct *task, bool on)
{
unsigned long debugctl;
local_irq_disable();
debugctl = get_debugctlmsr();
if (on) {
debugctl |= DEBUGCTLMSR_BTF;
set_tsk_thread_flag(task, TIF_BLOCKSTEP);
} else {
debugctl &= ~DEBUGCTLMSR_BTF;
clear_tsk_thread_flag(task, TIF_BLOCKSTEP);
}
if (task == current)
update_debugctlmsr(debugctl);
local_irq_enable();
}
breakpoints(断点)
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
printf("test INT 3n");
__asm__("int $0x3");
printf("breakpoint");
return 0;
}
root@ubuntu:~/tiny_debugger# ./bp
test INT 3
Trace/breakpoint trap (core dumped)
watchpoint(硬件断点)
用ptrace实现一个tracer
On the subject of debuggers and tracers
OrangeGzY/tiny_debugger
#include <stdio.h>
int func1()
{
printf("function1");
}
void func2()
{
printf("function2");
}
void func3()
{
printf("fucntion3");
}
int main()
{
//printf("===========n");
func1();
func3();
func2();
func2();
func3();
//printf("===========n");
return 0;
}
if(child == 0){
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl(argv[1],argv[1],NULL);
perror("fail exec");
exit(-1);
}
ELF文件解析
fread(&header, sizeof(Elf64_Ehdr), 1, fp); //read elf header of target file
fseek(fp, header.e_shoff, SEEK_SET); //move the pointer to Section Header Table
for(int i=0;i < header.e_shnum;i++)
{
fread(§ion_header, sizeof(Elf64_Shdr), 1, fp);
if(section_header.sh_type == SHT_SYMTAB)
{
......
}
......
}
fseek(fp,str_table_offset,SEEK_SET); //定位到字符串表header
fread(&str_section_header, sizeof(Elf64_Shdr), 1, fp); //读取字符串表表头
int bp = 0; //global
typedef struct breakpoint{
size_t addr;
char name[25];
size_t orig_code;
}Breakpoint;
Breakpoint bp_list[N];
for(int i=0;i<sym_entry_count; i++){
//符号表中每一个元素是一个 Elf64_Sym
Elf64_Sym sym;
fread(&sym, sizeof(Elf64_Sym), 1,fp); //每次读一个Symbol
if(ELF64_ST_TYPE(sym.st_info) == STT_FUNC && sym.st_name!=0 && sym.st_value != 0){
/* 如果该符号是一个函数或其他可执行代码,在字符串表中,且虚拟地址不为0 */
long file_ops = ftell(fp); //保存此时fp的位置
fseek(fp,str_section_header.sh_offset+sym.st_name,SEEK_SET); //定位到字符串表中对应符号的位置
bp_list[bp].addr = sym.st_value;
fread(bp_list[bp].name,25,sizeof(char),fp); //读取对应的符号
bp = bp + 1;
fseek(fp,file_ops,SEEK_SET); //恢复fp到上一次读取的Symbol的位置,准备下一次读取。
}
}
断点注入
int breakpoint_injection(pid_t child){
/* 我们向每个函数的第一条指令的位置注入INT 3 即 0xCC */
for(int i=0 ; i<bp ; i++){
//使用ptrace读出一个字节存在orgi code中
bp_list[i].orig_code = ptrace(PTRACE_PEEKTEXT,child,bp_list[i].addr,0);
#ifdef DEBUG
printf("[*] Set Breakpoint:0x%llx,0x%llxn",bp_list[i].addr,bp_list[i].orig_code);
#endif
ptrace(PTRACE_POKETEXT, child, bp_list[i].addr, (bp_list[i].orig_code & 0xFFFFFFFFFFFFFF00) | INT_3); //将最低为的字节打入 int 3
}
printf("n");
#ifdef DEBUG
check_bp(child);
#endif
}
断点追踪
if(WIFSTOPPED(status))
{
/* 如果是STOP信号 */
if(WSTOPSIG(status)==SIGTRAP)
{ //如果是触发了SIGTRAP,说明碰到了断点
ptrace(PTRACE_GETREGS,child,0,®s); //读取此时用户态寄存器的值,准备为回退做准备
//printf("[+] SIGTRAP rip:0x%llxn",regs.rip);
/* 将此时的addr与我们bp_list中维护的addr做对比,如果查找到,说明断点命中 */
if((hit_index=if_bp_hit(regs))==-1)
{
/*未命中*/
printf("MISS, fail to hit:0x%llxn",regs.rip);
exit(-1);
}
else
{
/*如果命中*/
/*输出命中信息*/
printf("%s()n",bp_list[hit_index].name);
/*把INT 3 patch 回本来正常的指令*/
ptrace(PTRACE_POKETEXT,child,bp_list[hit_index].addr,bp_list[hit_index].orig_code);
/*执行流回退,重新执行正确的指令*/
regs.rip = bp_list[hit_index].addr;
ptrace(PTRACE_SETREGS,child,0,®s);
/*单步执行一次,然后恢复断点*/
ptrace(PTRACE_SINGLESTEP,child,0,0);
wait(NULL);
/*恢复断点*/
ptrace(PTRACE_POKETEXT, child, bp_list[hit_index].addr, (bp_list[hit_index].orig_code & 0xFFFFFFFFFFFFFF00) | INT_3);
}
}
}
ptrace(PTRACE_CONT,child,0,0);
最终输出:
可以看到,整个流程非常清晰。
参考
看雪ID:ScUpax0s
https://bbs.pediy.com/user-home-876323.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
本文始发于微信公众号(看雪学院):一窥GDB原理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论