
This article is a highlight from the KX forum
Author from KX forumID: ScUpax0s
The goals of this article are:
Main goal: Understand the debugging principles of GDB and learn the use of ptrace.
Secondary goal: Implement a tiny debugger that can be used for tracing.
-
tracer: The one who traces
-
tracee: The one being traced

Brief Overview of GDB Debugging Principles


gdb ./a.out
attach pid
gdb server’s 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, This process is being traced by its parent process. The parent process should wish to trace the child process PTRACE_PEEKTEXT, Read a byte from the memory address given by addr PTRACE_PEEKDATA, Same as above PTRACE_PEEKUSER, Can check the user mode memory area (USER area), read a byte from the USER area, offset by addr PTRACE_POKETEXT, Write a byte to the memory address given by addr PTRACE_POKEDATA, Write a byte to the memory address given by addr PTRACE_POKEUSER, Write a byte to the USER area, offset by addr PTRACE_GETREGS, Read registers PTRACE_GETFPREGS, Read floating-point registers PTRACE_SETREGS, Set registers PTRACE_SETFPREGS, Set floating-point registers PTRACE_CONT, Continue running PTRACE_SYSCALL, Continue running PTRACE_SINGLESTEP, Set single-step execution flag PTRACE_ATTACH, Trace the specified pid PTRACE_DETACH, End tracing
(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 || // Check process permissions (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; // Set process flag PF_PTRACED write_lock_irqsave(&tasklist_lock, flags); if (child->p_pptr != current) { // Set the process as the current process's child. REMOVE_LINKS(child); child->p_pptr = current; SET_LINKS(child); } write_unlock_irqrestore(&tasklist_lock, flags); send_sig(SIGSTOP, child, 1); // Send a SIGSTOP to the child process to stop it ret = 0; goto out; }
(3) PTRACE_CONT
case PTRACE_CONT: long tmp; ret = -EIO; if ((unsigned long) data > _NSIG) // Check if the signal exceeds the range? goto out; if (request == PTRACE_SYSCALL) child->flags |= PF_TRACESYS; // If it is PTRACE_SYSCALL, set the PF_TRACESYS flag else child->flags &= ~PF_TRACESYS; // If it is PF_CONT, remove the PF_TRACESYS flag child->exit_code = data; // Set the signal to continue processing tmp = get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG; // Clear TRAP_FLAG put_stack_long(child, EFL_OFFSET,tmp); wake_up_process(child); // Wake up the stopped child process 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: execl\n"); execve("/bin/ls",argv,envp); printf("child exit\n"); } else { wait(NULL); // Wait for child process orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL); printf("The child made a " "system call %ld\n", orig_rax); ptrace(PTRACE_CONT, child, NULL, NULL); printf("Try to call:ptrace\n"); } return 0;
root@ubuntu:~/tiny_debugger# ./demo1Try to call: execlThe child made a system call 59Try to call:ptraceroot@ubuntu:~/tiny_debugger# -rw-r--r-- 1 root root 2446 Dec 13 19:53 /etc/passwd
-
Fork a child process. The child process is marked as tracee. (PTRACE_TRACEME)
-
The child process calls execve, sending a SIGCHLD to the parent process. Meanwhile, the child process stops due to SIGTRAP.
-
The parent process captures SIGCHLD and uses ptrace to obtain the system call number (59) of the child process.
-
The parent process tells the child process to continue executing (PTRACE_CONT), and the child process outputs the content of ls.
-
The child’s printf(“child exit\n”); will not be executed because execve discards the part after execve() in the original child process, and the stack and data of the child process will be replaced by the corresponding parts of the new process. It never returns.
/** * 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); }}


-
Reading and modifying data using copy_from_user and copy_to_user.
-
Accessing registers through copy_regset_from/to_user. The register data is stored in the task struct.
-
Single stepping: Each step (step) once, the CPU will continue executing until there is a branch, interrupt, or exception. ptrace enables single-step debugging by setting the corresponding flags in the process’s thread_info.flags and 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 3\n"); __asm__("int $0x3"); printf("breakpoint"); return 0;}
root@ubuntu:~/tiny_debugger# ./bptest INT 3Trace/breakpoint trap (core dumped)


Watchpoints (Hardware Breakpoints)


Implementing a Tracer with ptrace

#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 File Parsing
fread(&header, sizeof(Elf64_Ehdr), 1, fp); //read elf header of target filefseek(fp, header.e_shoff, SEEK_SET); //move the pointer to Section Header Table
for(int i=0;i < header.e_shnum;i++){ fread(&section_header, sizeof(Elf64_Shdr), 1, fp); if(section_header.sh_type == SHT_SYMTAB) { ...... } ......}
fseek(fp,str_table_offset,SEEK_SET); //定位到字符串表headerfread(&str_section_header, sizeof(Elf64_Shdr), 1, fp); //读取字符串表表头
int bp = 0; //globaltypedef 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的位置,准备下一次读取。 } }

Breakpoint Injection
int breakpoint_injection(pid_t child){ /* We inject INT 3 (0xCC) at the first instruction of each function */ 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%llx\n",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}

Breakpoint Tracking
if(WIFSTOPPED(status)) { /* If it is a STOP signal */ if(WSTOPSIG(status)==SIGTRAP) { // If SIGTRAP was triggered, it indicates a breakpoint hit ptrace(PTRACE_GETREGS,child,0,&regs); // Read the current user-mode register values, preparing for rollback //printf("[+] SIGTRAP rip:0x%llx\n",regs.rip); /* Compare the current addr with the addr maintained in our bp_list. If found, it indicates a hit on the breakpoint */ if((hit_index=if_bp_hit(regs))==-1) { /* Missed */ printf("MISS, fail to hit:0x%llx\n",regs.rip); exit(-1); } else { /* If hit */ /* Output hit info */ printf("%s()\n",bp_list[hit_index].name); /* Restore the original instruction at the addr where INT 3 was patched */ ptrace(PTRACE_POKETEXT,child,bp_list[hit_index].addr,bp_list[hit_index].orig_code); /* Roll back the execution flow, re-executing the correct instruction */ regs.rip = bp_list[hit_index].addr; ptrace(PTRACE_SETREGS,child,0,&regs); /* Single-step once, then restore the breakpoint */ ptrace(PTRACE_SINGLESTEP,child,0,0); wait(NULL); /* Restore the breakpoint */ ptrace(PTRACE_POKETEXT, child, bp_list[hit_index].addr, (bp_list[hit_index].orig_code & 0xFFFFFFFFFFFFFF00) | INT_3); } } } ptrace(PTRACE_CONT,child,0,0);
Final output:


It can be seen that the entire process is very clear.
References
KX ID: ScUpax0s
https://bbs.pediy.com/user-home-876323.htm
# Previous Recommendations
-
Detailed Analysis Series of Linux ptrace (II) -
Firm-AFL: Efficient IoT Firmware Gray Box Fuzzing
-
Google CTF 2020 Qualifying Round reverse_android
-
CVE-2020-12351: Analysis of Linux Bluetooth Module Denial of Service Vulnerability
-
Windows Application Responding to GPIO (SCI) Device Interrupts Hardware Part


Share This

Like This

Watch This

Click “Read Original” to learn more!