- Learning Linux Binary Analysis
- Ryan “elfmaster” O'Neill
- 664字
- 2021-07-16 12:56:55
A simple ptrace-based debugger
Let's look at a code example that makes use of ptrace
to create a debugger program:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <signal.h> #include <elf.h> #include <sys/types.h> #include <sys/user.h> #include <sys/stat.h> #include <sys/ptrace.h> #include <sys/mman.h> typedef struct handle { Elf64_Ehdr *ehdr; Elf64_Phdr *phdr; Elf64_Shdr *shdr; uint8_t *mem; char *symname; Elf64_Addr symaddr; struct user_regs_struct pt_reg; char *exec; } handle_t; Elf64_Addr lookup_symbol(handle_t *, const char *); int main(int argc, char **argv, char **envp) { int fd; handle_t h; struct stat st; long trap, orig; int status, pid; char * args[2]; if (argc < 3) { printf("Usage: %s <program> <function>\n", argv[0]); exit(0); } if ((h.exec = strdup(argv[1])) == NULL) { perror("strdup"); exit(-1); } args[0] = h.exec; args[1] = NULL; if ((h.symname = strdup(argv[2])) == NULL) { perror("strdup"); exit(-1); } if ((fd = open(argv[1], O_RDONLY)) < 0) { perror("open"); exit(-1); } if (fstat(fd, &st) < 0) { perror("fstat"); exit(-1); } h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (h.mem == MAP_FAILED) { perror("mmap"); exit(-1); } h.ehdr = (Elf64_Ehdr *)h.mem; h.phdr = (Elf64_Phdr *)(h.mem + h.ehdr->e_phoff); h.shdr = (Elf64_Shdr *)(h.mem + h.ehdr->e_shoff); if+ (h.mem[0] != 0x7f || strcmp((char *)&h.mem[1], "ELF")) { printf("%s is not an ELF file\n",h.exec); exit(-1); } if (h.ehdr->e_type != ET_EXEC) { printf("%s is not an ELF executable\n", h.exec); exit(-1); } if (h.ehdr->e_shstrndx == 0 || h.ehdr->e_shoff == 0 || h.ehdr->e_shnum == 0) { printf("Section header table not found\n"); exit(-1); } if ((h.symaddr = lookup_symbol(&h, h.symname)) == 0) { printf("Unable to find symbol: %s not found in executable\n", h.symname); exit(-1); } close(fd); if ((pid = fork()) < 0) { perror("fork"); exit(-1); } if (pid == 0) { if (ptrace(PTRACE_TRACEME, pid, NULL, NULL) < 0) { perror("PTRACE_TRACEME"); exit(-1); } execve(h.exec, args, envp); exit(0); } wait(&status); printf("Beginning analysis of pid: %d at %lx\n", pid, h.symaddr); if ((orig = ptrace(PTRACE_PEEKTEXT, pid, h.symaddr, NULL)) < 0) { perror("PTRACE_PEEKTEXT"); exit(-1); } trap = (orig & ~0xff) | 0xcc; if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { perror("PTRACE_POKETEXT"); exit(-1); } trace: if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) { perror("PTRACE_CONT"); exit(-1); } wait(&status); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { if (ptrace(PTRACE_GETREGS, pid, NULL, &h.pt_reg) < 0) { perror("PTRACE_GETREGS"); exit(-1); } printf("\nExecutable %s (pid: %d) has hit breakpoint 0x%lx\n", h.exec, pid, h.symaddr); printf("%%rcx: %llx\n%%rdx: %llx\n%%rbx: %llx\n" "%%rax: %llx\n%%rdi: %llx\n%%rsi: %llx\n" "%%r8: %llx\n%%r9: %llx\n%%r10: %llx\n" "%%r11: %llx\n%%r12 %llx\n%%r13 %llx\n" "%%r14: %llx\n%%r15: %llx\n%%rsp: %llx", h.pt_reg.rcx, h.pt_reg.rdx, h.pt_reg.rbx, h.pt_reg.rax, h.pt_reg.rdi, h.pt_reg.rsi, h.pt_reg.r8, h.pt_reg.r9, h.pt_reg.r10, h.pt_reg.r11, h.pt_reg.r12, h.pt_reg.r13, h.pt_reg.r14, h.pt_reg.r15, h.pt_reg.rsp); printf("\nPlease hit any key to continue: "); getchar(); if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, orig) < 0) { perror("PTRACE_POKETEXT"); exit(-1); } h.pt_reg.rip = h.pt_reg.rip - 1; if (ptrace(PTRACE_SETREGS, pid, NULL, &h.pt_reg) < 0) { perror("PTRACE_SETREGS"); exit(-1); } if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) { perror("PTRACE_SINGLESTEP"); exit(-1); } wait(NULL); if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { perror("PTRACE_POKETEXT"); exit(-1); } goto trace; } if (WIFEXITED(status)) printf("Completed tracing pid: %d\n", pid); exit(0); } Elf64_Addr lookup_symbol(handle_t *h, const char *symname) { int i, j; char *strtab; Elf64_Sym *symtab; for (i = 0; i < h->ehdr->e_shnum; i++) { if (h->shdr[i].sh_type == SHT_SYMTAB) { strtab = (char *)&h->mem[h->shdr[h->shdr[i].sh_link].sh_offset]; symtab = (Elf64_Sym *)&h->mem[h->shdr[i].sh_offset]; for (j = 0; j < h->shdr[i].sh_size/sizeof(Elf64_Sym); j++) { if(strcmp(&strtab[symtab->st_name], symname) == 0) return (symtab->st_value); symtab++; } } } return 0; } }
Using the tracer program
To compile the preceding source code, use this:
gcc tracer.c –o tracer
Keep in mind that tracer.c
locates the symbol table by finding and referencing the SHT_SYMTAB
type section header, so it will not work on executables that have been stripped of the SHT_SYMTAB
symbol table (although they may have SHT_DYNSYM
). This actually makes sense, because usually we are debugging programs that are still in their development phase, so they usually do have a complete symbol table.
The other limitation is that it doesn't allow you to pass arguments to the program you are executing and tracing. So, it wouldn't do well in a real debugging situation, where you may need to pass switches or command-line options to your program that is being debugged.
As an example of the ./tracer
program that we designed, let's try it on a very simple program that calls a function called print_string(char *)
twice, and passes to it the Hello 1
string on the first round and Hello 2
on the second.
Here's an example of using the ./tracer
code:
$ ./tracer ./test print_string Beginning analysis of pid: 6297 at 40057d Executable ./test (pid: 6297) has hit breakpoint 0x40057d %rcx: 0 %rdx: 7fff4accbf18 %rbx: 0 %rax: 400597 %rdi: 400644 %rsi: 7fff4accbf08 %r8: 7fd4f09efe80 %r9: 7fd4f0a05560 %r10: 7fff4accbcb0 %r11: 7fd4f0650dd0 %r12 400490 %r13 7fff4accbf00 %r14: 0 %r15: 0 %rsp: 7fff4accbe18 Please hit any key to continue: c Hello 1 Executable ./test (pid: 6297) has hit breakpoint 0x40057d %rcx: ffffffffffffffff %rdx: 7fd4f09f09e0 %rbx: 0 %rax: 9 %rdi: 40064d %rsi: 7fd4f0c14000 %r8: ffffffff %r9: 0 %r10: 22 %r11: 246 %r12 400490 %r13 7fff4accbf00 %r14: 0 %r15: 0 %rsp: 7fff4accbe18 Hello 2 Please hit any key to continue: Completed tracing pid: 6297
As you can see, a breakpoint was set on print_string
, and each time the function was called, our ./tracer
program caught the trap, printed the register values, and then continued executing after we hit a character. The ./tracer
program is a good example of how a debugger such as gdb
works. Although it is much simpler, it demonstrates process tracing, breakpoints, and symbol lookup.
This program works great if you want to execute a program and trace it all at once. But what about tracing a process that is already running? In such a case, we would want to attach to the process image with PTRACE_ATTACH
. This request sends a SIGSTOP
to the process we are attaching to, so we use wait
or waitpid
to wait for the process to stop.
- ServiceNow Application Development
- ExtGWT Rich Internet Application Cookbook
- Ext JS Data-driven Application Design
- Visual Basic程序設計教程
- Data Analysis with IBM SPSS Statistics
- Spring實戰(第5版)
- Mastering AndEngine Game Development
- Visual C++應用開發
- ASP.NET程序設計教程
- Apache Spark 2.x for Java Developers
- Visual Basic程序設計
- PhoneGap 4 Mobile Application Development Cookbook
- 計算機應用基礎案例教程(第二版)
- Java設計模式深入研究
- iOS Development with Xamarin Cookbook