Created
February 23, 2025 22:26
-
-
Save ben-cohen/16b6e2417ce1c542fe03abada4ac59aa to your computer and use it in GitHub Desktop.
Set breakpoints and watchpoints on the child process using ptrace on x86-64 Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ptrace_test: Set breakpoints and watchpoints on the child process using | |
| * ptrace on x86-64 Linux | |
| * | |
| * Compile using: | |
| * gcc -o ptrace_test ptrace_test.c -Wall -ggdb | |
| * | |
| * Ben Cohen, February 2025 | |
| */ | |
| #include <errno.h> | |
| #include <inttypes.h> | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <unistd.h> | |
| #include <sys/ptrace.h> | |
| #include <sys/types.h> | |
| #include <sys/user.h> | |
| #include <sys/wait.h> | |
| void *address_of_breakpoint = NULL; | |
| int variable_to_watch = 0; | |
| void ptrace_checked(const char* who, | |
| enum __ptrace_request request, | |
| pid_t pid, | |
| void *addr, | |
| void *data) | |
| { | |
| long result = ptrace(request, pid, addr, data); | |
| if (result != -1) | |
| { | |
| return; | |
| } | |
| char *request_name = NULL; | |
| switch (request) | |
| { | |
| case PTRACE_TRACEME: | |
| request_name = "PTRACE_TRACEME"; | |
| break; | |
| case PTRACE_CONT: | |
| request_name = "PTRACE_CONT"; | |
| break; | |
| case PTRACE_POKEDATA: | |
| request_name = "PTRACE_POKEDATA"; | |
| break; | |
| case PTRACE_PEEKDATA: | |
| request_name = "PTRACE_PEEKDATA"; | |
| break; | |
| case PTRACE_POKEUSER: | |
| request_name = "PTRACE_POKEUSER"; | |
| break; | |
| default: | |
| request_name = "(unknown)"; | |
| break; | |
| } | |
| fprintf(stderr, "%s: %s: %s\n", who, request_name, strerror(errno)); | |
| exit(1); | |
| } | |
| enum watchpoint_type | |
| { | |
| WP_EXECUTE = 0x0, | |
| WP_WRITE = 0x1, | |
| WP_IO_RW = 0x2, | |
| WP_DATA_RW = 0x3 | |
| }; | |
| enum watchpoint_length | |
| { | |
| WP_1 = 0x0, | |
| WP_2 = 0x1, | |
| WP_8 = 0x2, | |
| WP_4 = 0x3 | |
| }; | |
| enum watchpoint_enable | |
| { | |
| WP_NONE = 0x0, | |
| WP_LOCAL = 0x1, | |
| WP_GLOBAL = 0x2, | |
| WP_BOTH = 0x3 | |
| }; | |
| void set_watchpoint(pid_t child_pid, void* address_for_breakpoint, | |
| enum watchpoint_enable watchpoint_enable, | |
| enum watchpoint_type watchpoint_type, | |
| enum watchpoint_length watchpoint_length) | |
| { | |
| /* x86 and x86-64 have 8 debug registers DR0 to DR7, which control | |
| * up to 4 memory addresses. They are used as follows: | |
| * | |
| * DR0 Address for Breakpoint 0 | |
| * DR1 Address for Breakpoint 1 | |
| * DR2 Address for Breakpoint 2 | |
| * DR3 Address for Breakpoint 3 | |
| * DR4 (reserved) | |
| * DR5 (reserved) | |
| * DR6 Debug Status Register: state reported when an exception is | |
| * generated | |
| * DR7 Debug Control Register: enable or disable and set conditions | |
| * for the breakpoints | |
| * | |
| * References: | |
| * * The Intel® 64 and IA-32 Architectures Software Developer's Manual | |
| * Volume 3: System Programming Guide, chapter 17.2. | |
| * * https://stackoverflow.com/a/40820763/ | |
| * | |
| * Here we only use Breakpoint 0. | |
| */ | |
| uint64_t DR0 = (uintptr_t)address_for_breakpoint; | |
| uint64_t DR6 = 0x00000000FFFF0FF0; /* reserved bits */ | |
| uint64_t DR7 = 0x0000000000000700; /* reserved bits and LE/GE local and | |
| global exact breakpoint enable */ | |
| /* The following bits are for Breakpoint 0. Shift them left by 2, 4 or 6 | |
| * bits for Breakpoints 1, 2 or 3. */ | |
| /* Bits 00-01: L0, G0: 00 = both disabled | |
| * 01 = local breakpoint enable | |
| * 10 = global breakpoint enable | |
| * 11 = local and global breakpoint enable */ | |
| DR7 |= watchpoint_enable; | |
| /* Bits 16-17: R/W0: 00 = instruction execute | |
| * 01 = data write | |
| * 10 = I/O read or write | |
| * 11 = data read or write but not | |
| * instruction fetches */ | |
| DR7 |= (watchpoint_type << 16); | |
| /* Bits 18-19: LEN0: 00 = 1 byte | |
| * 01 = 2 byte | |
| * 10 = 8 byte (x86-64) | |
| * 11 = 4 byte */ | |
| DR7 |= (watchpoint_length << 18); | |
| /* Set the debug registers. */ | |
| ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid, | |
| (void*)(uintptr_t)offsetof(struct user, u_debugreg[0]), | |
| (void*)(uintptr_t)DR0); | |
| ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid, | |
| (void*)(uintptr_t)offsetof(struct user, u_debugreg[6]), | |
| (void*)(uintptr_t)DR6); | |
| ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid, | |
| (void*)(uintptr_t)offsetof(struct user, u_debugreg[7]), | |
| (void*)(uintptr_t)DR7); | |
| } | |
| void clear_watchpoint(pid_t child_pid) | |
| { | |
| set_watchpoint(child_pid, NULL, WP_NONE, 0, 0); | |
| } | |
| void wait_for_signal(pid_t child_pid, int signal) | |
| { | |
| int wstatus; | |
| waitpid(child_pid, &wstatus, 0); | |
| if (!WIFSTOPPED(wstatus)) | |
| { | |
| printf("PARENT: Child not stopped!\n"); | |
| exit(1); | |
| } | |
| else if (WSTOPSIG(wstatus) != signal) | |
| { | |
| printf("PARENT: Child stopped on unexpected signal %d!\n", | |
| WSTOPSIG(wstatus)); | |
| exit(1); | |
| } | |
| } | |
| void wait_for_exit(pid_t child_pid, uint8_t status) | |
| { | |
| int wstatus; | |
| waitpid(child_pid, &wstatus, 0); | |
| if (!WIFEXITED(wstatus)) | |
| { | |
| printf("PARENT: Child has not exited!\n"); | |
| exit(1); | |
| } | |
| else if (WEXITSTATUS(wstatus) != status) | |
| { | |
| printf("PARENT: Child exited with unexpected status %d!\n", | |
| WEXITSTATUS(wstatus)); | |
| exit(1); | |
| } | |
| } | |
| void set_breakpoint(pid_t child_pid, void* address, | |
| uint64_t* original_value) | |
| { | |
| void* aligned_address = (void*)(((uintptr_t)address) & ~7); | |
| uintptr_t remainder = ((uintptr_t)address) & 7; | |
| long result = ptrace(PTRACE_PEEKTEXT, child_pid, aligned_address, NULL); | |
| /* Note: Nasty special case to handle error return value! */ | |
| if (result == -1 && errno != 0) | |
| { | |
| printf("PARENT: ptrace: PTRACE_PEEKTEXT: %s\n", strerror(errno)); | |
| exit(1); | |
| } | |
| *original_value = result; | |
| const uint8_t ASM_INT3 = 0xCC; | |
| int shift_by = remainder * 8; | |
| uint64_t new_value = result; | |
| new_value &= ~(0xFF << shift_by); | |
| new_value |= ((uint64_t)ASM_INT3) << shift_by; | |
| ptrace_checked("PARENT", PTRACE_POKETEXT, child_pid, aligned_address, | |
| (void*)new_value); | |
| } | |
| void clear_breakpoint(pid_t child_pid, void* address, | |
| uint64_t original_value) | |
| { | |
| /* Here we simply restore the original memory. If we wanted to stop again | |
| * at the same place then we would need to single-step one instruction | |
| * and then re-set the breakpoint. */ | |
| void* aligned_address = (void*)(((uintptr_t)address) & ~7); | |
| ptrace_checked("PARENT", PTRACE_POKETEXT, child_pid, aligned_address, | |
| (void*)original_value); | |
| /* The instruction pointer is now one byte past the start of the real | |
| * instruction, so decrement it. */ | |
| struct user_regs_struct registers; | |
| ptrace_checked("PARENT", PTRACE_GETREGS, child_pid, NULL, ®isters); | |
| registers.rip --; | |
| ptrace_checked("PARENT", PTRACE_SETREGS, child_pid, NULL, ®isters); | |
| } | |
| void function_to_break_on() | |
| { | |
| /* The child should stop on the breakpoint here. */ | |
| printf(" CHILD: After breakpoint\n"); | |
| } | |
| void execute_child() | |
| { | |
| /* We use raise() here because the ptrace() call itself does not | |
| * stop the child and we are not calling execxxx(). */ | |
| ptrace_checked(" CHILD", PTRACE_TRACEME, 0, NULL, NULL); | |
| raise(SIGSTOP); | |
| printf(" CHILD: Before breakpoint\n"); | |
| function_to_break_on(); | |
| printf(" CHILD: Read from memory with 'data write only' watchpoint\n"); | |
| /* The child should not stop here. */ | |
| printf(" CHILD: variable_to_watch = %d\n", variable_to_watch); | |
| printf(" CHILD: Write to memory with 'data write only' watchpoint\n"); | |
| /* The child should stop on the watchpoint here. */ | |
| variable_to_watch = 1; | |
| printf(" CHILD: Write to memory with disabled watchpoint\n"); | |
| /* The child should not stop here. */ | |
| variable_to_watch = 1; | |
| printf(" CHILD: Exiting\n"); | |
| exit(0); | |
| } | |
| void execute_parent(pid_t child_pid) | |
| { | |
| printf("PARENT: Child has pid %" PRIdMAX ". Wait for it to stop\n", | |
| (intmax_t) child_pid); | |
| wait_for_signal(child_pid, SIGSTOP); | |
| /* PART 1: Setting breakpoints */ | |
| printf("PARENT: Set a breakpoint\n"); | |
| uint64_t original_memory = 0; | |
| set_breakpoint(child_pid, &function_to_break_on, &original_memory); | |
| printf("PARENT: Make the child continue and wait for it\n"); | |
| ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL); | |
| /* Note that we should probably check that we stopped because of the | |
| * break and not for some other reason. */ | |
| wait_for_signal(child_pid, SIGTRAP); | |
| printf("PARENT: Clear the breakpoint\n"); | |
| clear_breakpoint(child_pid, &function_to_break_on, original_memory); | |
| /* PART 2: Setting watchpoints */ | |
| printf("PARENT: Set a watchpoint on variable_to_watch\n"); | |
| set_watchpoint(child_pid, (void*)&variable_to_watch, WP_LOCAL, | |
| WP_WRITE, WP_1); | |
| printf("PARENT: Make the child continue and wait for it\n"); | |
| ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL); | |
| /* Note that we should probably check that we stopped because of the | |
| * watchpoint and not for some other reason. */ | |
| wait_for_signal(child_pid, SIGTRAP); | |
| printf("PARENT: Clear the watchpoint\n"); | |
| clear_watchpoint(child_pid); | |
| printf("PARENT: Make the child continue\n"); | |
| ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL); | |
| printf("PARENT: Waiting for child\n"); | |
| wait_for_exit(child_pid, 0); | |
| printf("PARENT: Exiting\n"); | |
| exit(0); | |
| } | |
| int main() | |
| { | |
| pid_t child_pid = fork(); | |
| if (child_pid == 0) | |
| { | |
| /* CHILD PROCESS */ | |
| execute_child(); | |
| } | |
| else if (child_pid == -1) | |
| { | |
| perror("PARENT: fork"); | |
| exit(1); | |
| } | |
| else | |
| { | |
| /* PARENT PROCESS */ | |
| execute_parent(child_pid); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment