ELF 调试器示例代码

Table Of Contents

这份调试器的示例代码来自《Learning Linux Binary Analysis》 书中第三章的“A simple ptrace-based debugger”一节,添加了一些调试打印和关于代码细节的注释。其他相关内容可查阅此书。

  1#include <stdio.h>
  2#include <string.h>
  3#include <stdlib.h>
  4#include <unistd.h>
  5#include <fcntl.h>
  6#include <errno.h>
  7#include <signal.h>
  8#include <elf.h>
  9#include <sys/wait.h>
 10#include <sys/types.h>
 11#include <sys/user.h>
 12#include <sys/stat.h>
 13#include <sys/ptrace.h>
 14#include <sys/mman.h>
 15
 16typedef struct handle {
 17    Elf64_Ehdr *ehdr;
 18    Elf64_Phdr *phdr;
 19    Elf64_Shdr *shdr;
 20    uint8_t *mem;
 21    char *symname;
 22    Elf64_Addr symaddr;
 23    struct user_regs_struct pt_reg;
 24    char *exec;
 25} handle_t;
 26
 27Elf64_Addr lookup_symbol(handle_t *, const char *);
 28
 29int main(int argc, char **argv, char **envp)
 30{
 31    int fd;
 32    handle_t h;
 33    struct stat st;
 34    long trap, orig;
 35    int status, pid;
 36    char * args[2]; // for child process's execv()
 37
 38    if (argc < 3) {
 39        printf("Usage: %s <program> <function>\n", argv[0]);
 40        exit(0);
 41    }
 42    if ((h.exec = strdup(argv[1])) == NULL) {
 43        perror("strdup");
 44        exit(-1);
 45    }
 46    args[0] = h.exec;
 47    args[1] = NULL;
 48    if ((h.symname = strdup(argv[2])) == NULL) {
 49        perror("strdup");
 50        exit(-1);
 51    }
 52    if ((fd = open(argv[1], O_RDONLY)) < 0) {
 53        perror("open");
 54        exit(-1);
 55    }
 56    if (fstat(fd, &st) < 0) {
 57        perror("fstat");
 58        exit(-1);
 59    }
 60
 61    h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
 62    if (h.mem == MAP_FAILED) {
 63        perror("mmap");
 64        exit(-1);
 65    }
 66    h.ehdr = (Elf64_Ehdr *)h.mem;
 67    h.phdr = (Elf64_Phdr *)(h.mem + h.ehdr->e_phoff);
 68    h.shdr = (Elf64_Shdr *)(h.mem + h.ehdr->e_shoff);
 69    /*
 70     * Using strcmp() here would cause undefined behavior,
 71     * since &h.mem[1] is not terminated by NULL.
 72     */
 73    if (h.mem[0] != 0x7f || strncmp((char *)&h.mem[1], "ELF", 3)) {
 74        printf("%s is not an ELF file\n", h.exec);
 75        exit(-1);
 76    }
 77    if (h.ehdr->e_type != ET_EXEC) { // ET_DYN for WSL
 78        printf("%s is not an ELF executable\n", h.exec);
 79        exit(-1);
 80    }
 81    if (h.ehdr->e_shstrndx == 0 || h.ehdr->e_shoff == 0 || h.ehdr->e_shnum == 0) {
 82        printf("Section header table not found\n");
 83        exit(-1);
 84    }
 85    if ((h.symaddr = lookup_symbol(&h, h.symname)) == 0) {
 86        printf("Unable to find symbol: %s not found in executable\n", h.symname);
 87        exit(-1);
 88    }
 89    /*
 90     * You may close the file right after mmap().
 91     * https://stackoverflow.com/a/17490185
 92     */
 93    close(fd);
 94
 95    if ((pid = fork()) < 0) {
 96        perror("fork");
 97        exit(-1);
 98    }
 99    if (pid == 0) {
100        /* Make the child proc traceable (other args are ignored) */
101        if (ptrace(PTRACE_TRACEME, pid, NULL, NULL) < 0) {
102            perror("PTRACE_TRACEME");
103            exit(-1);
104        }
105        execve(h.exec, args, envp);
106        printf("Should never reach here\n");
107        exit(0);
108    }
109    wait(&status);
110
111    /*
112     * From execve(2):
113     *   If the current program is being ptraced, a SIGTRAP signal is sent
114     *   to it after a successful execve().
115     */
116    if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
117        printf("Yes the child process is indeed stopped by SIGTRAP\n");
118    }
119
120    printf("Beginning analysis of pid: %d at %lx\n", pid, h.symaddr);
121    /*
122     * Read (and backup) a word at target function address
123     * (in child's address space, same for the following comments so will be omitted)
124     */
125    if ((orig = ptrace(PTRACE_PEEKTEXT, pid, h.symaddr, NULL)) < 0) {
126        perror("PTRACE_PEEKTEXT");
127        exit(-1);
128    }
129    /*
130     * Prepare the code of "int 3", which stops the program's execution.
131     * x86 is a variable-length instruction set, and "int 3" is only one byte long.
132     */
133    trap = (orig & ~0xff) | 0xcc;
134    /* Write it back to replace the function code */
135    if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) {
136        perror("PTRACE_POKETEXT");
137        exit(-1);
138    }
139
140trace:
141    /* Let the paused child run */
142    if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
143        perror("PTRACE_CONT");
144        exit(-1);
145    }
146    wait(&status);
147    if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
148        /* Get current values in registers */
149        if (ptrace(PTRACE_GETREGS, pid, NULL, &h.pt_reg) < 0) {
150            perror("PTRACE_GETREGS");
151            exit(-1);
152        }
153        printf("\nExecutable %s (pid: %d) has hit breakpoint 0x%lx\n", h.exec, pid, h.symaddr);
154        printf("%%rcx: %llx\n%%rdx: %llx\n%%rbx: %llx\n"
155               "%%rax: %llx\n%%rdi: %llx\n%%rsi: %llx\n"
156               "%%r8:  %llx\n%%r9:  %llx\n%%r10: %llx\n"
157               "%%r11: %llx\n%%r12: %llx\n%%r13: %llx\n"
158               "%%r14: %llx\n%%r15: %llx\n%%rsp: %llx\n",
159               h.pt_reg.rcx, h.pt_reg.rdx, h.pt_reg.rbx,
160               h.pt_reg.rax, h.pt_reg.rdi, h.pt_reg.rsi,
161               h.pt_reg.r8,  h.pt_reg.r9,  h.pt_reg.r10,
162               h.pt_reg.r11, h.pt_reg.r12, h.pt_reg.r13,
163               h.pt_reg.r14, h.pt_reg.r15, h.pt_reg.rsp);
164        printf("\nPlease hit any key to continue: ");
165        getchar();
166        /* Recover the original function code for further execution */
167        if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, orig) < 0) {
168            perror("PTRACE_POKETEXT");
169            exit(-1);
170        }
171        /*
172         * "Take one step back", so that the function can be executed.
173         * Here 1 = length("int 3") = length(0xcc). For other arch, this number could be different.
174         */
175        h.pt_reg.rip = h.pt_reg.rip - 1;
176        /* Write all registers back to modify RIP */
177        if (ptrace(PTRACE_SETREGS, pid, NULL, &h.pt_reg) < 0) {
178            perror("PTRACE_SETREGS");
179            exit(-1);
180        }
181        /* Execute the function code that was replaced */
182        if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) {
183            perror("PTRACE_SINGLESTEP");
184            exit(-1);
185        }
186        /* Receive the SIGTRAP from PTRACE_SINGLESTEP and ignore it */
187        wait(NULL);
188        /* No practical effect, only to show how debugging could continue */
189        if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) {
190            perror("PTRACE_POKETEXT");
191            exit(-1);
192        }
193        goto trace;
194    }
195    if (WIFEXITED(status))
196        printf("Completed tracing pid: %d\n", pid);
197    exit(0);
198}
199
200Elf64_Addr lookup_symbol(handle_t *h, const char *symname)
201{
202    int i, j;
203    char *strtab;
204    Elf64_Sym *symtab;
205    for (i = 0; i < h->ehdr->e_shnum; i++){
206        if (h->shdr[i].sh_type == SHT_SYMTAB) {
207            /* Here it may presume the .strtab section always follows the .symtab section */
208            strtab = (char *)&h->mem[h->shdr[h->shdr[i].sh_link].sh_offset];
209            symtab = (Elf64_Sym *)&h->mem[h->shdr[i].sh_offset];
210            for (j = 0; j < h->shdr[i].sh_size/sizeof(Elf64_Sym); j++) {
211                if (strcmp(&strtab[symtab->st_name], symname) == 0)
212                    return (symtab->st_value);
213                symtab++;
214            }
215        }
216    }
217    return 0;
218}

备注

1h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

Map the executable file into a random (and page-aligned) segment of memory chosen by the kernel, instructed by the first argument NULL. PROT_READ makes the memory read-only, and MAP_PRIVATE indicates a private mapping, which means updates are not carried through to the underlying file.


向 Linux 内核社区提交补丁的流程
基于完整发行版搭建内核验证环境