距离我上一篇研究ptrace的随笔http://www.cnblogs.com/zealotrouge/p/3544147.html已经过去半年了,最近不忙的时候抽空继续研究了下。同样,参考了Pradeep Padala的博文http://www.linuxjournal.com/article/6210,对其中断点部分比较感兴趣。因为从开始学习编程之日起,调试就是我们不可或缺的重要工具,而调试的基础就在于断点,那么,断点是如何让一个运行中的程序暂停的呢?背后的机制又是什么?为了探寻这个问题,我研究了上面的文章,并在自己的机器上(环境为Ubuntu12.04 + Intel x86_64 i5)动手实现了一下x64版本。
下面是整个问题研究的思路,按这个思路来给大家展示代码。首先,我们有一个简单的源程序tracedProcess.c,简单到仅仅是每隔1s输出一行"I'm running",这种简单的程序比较适合初学者分析汇编代码的语义,并找到容易设置断点的地方:
1 /* 2 tracedProcess.c 3 author: pengyiming 4 */ 5 6 #include <stdio.h> 7 8 void main() 9 { 10 while(1) 11 { 12 printf("I'm running\n"); 13 14 sleep(1); 15 } 16 }
gcc编译如上代码:
gcc -o tracedProcess.o tracedProcess.c
objdump分析目标文件tracedProcess.o,得到如下输出(截取main部分):
objdump -d tracedProcess.o 0000000000400544 <main>: 400544: 55 push %rbp 400545: 48 89 e5 mov %rsp,%rbp 400548: bf 5c 06 40 00 mov $0x40065c,%edi 40054d: e8 de fe ff ff callq 400430 <puts@plt> 400552: bf 01 00 00 00 mov $0x1,%edi 400557: b8 00 00 00 00 mov $0x0,%eax 40055c: e8 ef fe ff ff callq 400450 <sleep@plt> 400561: eb e5 jmp 400548 <main+0x4>
简单分析下:
0x400544 这个虚地址是main函数的入口
0x400544~0x400547 是所有函数的默认动作,新建一个函数栈
0x400548~0x40054c 将0x40065c传给edi寄存器,edi是字符串操作寄存器,存储的是字符串地址,0x40065c是只读区地址,用后面的getData()函数可以打出来,发现就是"I'm running"这个字符串
0x40054d~0x400551 callq执行一个函数调用,0x400430是此函数的入口,不难看出就是printf()函数链接到此目标文件的地址
0x400552~0x400556 清空edi寄存器
0x400557~0x40055b printf()无返回值,无需传递返回值地址给eax
0x40055c~0x400560 callq执行一个函数调用,sleep()
0x400561 跳转到0x400548进入下一个循环
分析完后,可以分析出,如果想把断点加在printf("I'm running\n");这条语句上,我们可以把0x400548作为断点。
合适打断点的地址找到后,我们就要想想如何让程序暂停,继续参考上一篇博客中提到的Intel处理器开发手册,发现可以使用Trap指令使程序暂停运行。具体是使用int 0x80进入内核态,然后调用Trap指令——int3,只要CPU执行了这个指令,即可让程序暂停并处于一直等待状态,所以我们需要用ptrace在tracedProcess.o运行时,操作CPU寄存器和注入int 0x80 int3指令,一旦程序执行完int3,即可断点成功;当然,在断点后,我们希望程序能恢复运行,我们还需要备份CPU寄存器和原来代码段中的指令,以便之后的恢复。
小结一下,断点+恢复需要两次注入来实现:
一、断点注入步骤
(1)PTRACE_ATTACH附着被注入进程(会暂停被注入进程),备份当前的寄存器值
(2)备份注入地址处指令
(3)替换注入地址处指令为Trap指令
(4)PTRACE_CONT使被注入进程继续执行,直到执行完Trap指令
二、恢复注入步骤
(1)恢复之前的寄存器值(注:为了方便,所有的寄存器都备份了,实质上是为了恢复栈指针rsp&rbp和指令指针rip)
(2)恢复注入地址处指令
(3)PTRACE_DETACH使被注入进程继续执行,并脱离被注入进程
代码如下:
1 /* 2 ptrace4.c 3 author: pengyiming 4 description: 5 1, attach a test process, insert a break point 6 2, sleep for 5s then continue it 7 */ 8 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <sys/ptrace.h> 13 #include <sys/types.h> 14 #include <sys/wait.h> 15 #include <sys/reg.h> 16 #include <sys/user.h> 17 #include <sys/syscall.h> 18 #include <unistd.h> 19 20 #define WORD_SIZE sizeof(long) 21 22 static unsigned long injectAddress = 0x400548; 23 24 // converter long to char[] 25 union 26 { 27 long rawData; 28 char strData[WORD_SIZE]; 29 } converter; 30 31 void getData(pid_t pid, unsigned long dataAddr, unsigned long dataLen, char * const p_data) 32 { 33 // PEEKDATA counter 34 int counter = 0; 35 // PEEKDATA max count 36 int maxCount = dataLen / WORD_SIZE; 37 if (dataLen % WORD_SIZE != 0) 38 { 39 maxCount++; 40 } 41 // moving pointer 42 void * p_moving = p_data; 43 44 while (counter < maxCount) 45 { 46 memset(&converter, 0, WORD_SIZE); 47 converter.rawData = ptrace(PTRACE_PEEKDATA, pid, dataAddr + counter * WORD_SIZE, NULL); 48 49 memcpy(p_moving, converter.strData, WORD_SIZE); 50 p_moving += WORD_SIZE; 51 counter++; 52 } 53 p_data[dataLen] = '\0'; 54 } 55 56 void setData(pid_t pid, unsigned long dataAddr, unsigned long dataLen, char * const p_data) 57 { 58 // POKEDATA counter 59 int counter = 0; 60 // POKEDATA max count 61 int maxCount = dataLen / WORD_SIZE; 62 // data left length (prevent out of range in memory when written) 63 int dataLeftLen = dataLen % WORD_SIZE; 64 // moving pointer 65 void * p_moving = p_data; 66 67 // write part of data which align to WORD_SIZE 68 int ret; 69 while (counter < maxCount) 70 { 71 memset(&converter, 0, WORD_SIZE); 72 memcpy(converter.strData, p_moving, WORD_SIZE); 73 ret = ptrace(PTRACE_POKEDATA, pid, dataAddr + counter * WORD_SIZE, converter.rawData); 74 75 p_moving += WORD_SIZE; 76 counter++; 77 } 78 79 // write data left 80 if (dataLeftLen != 0) 81 { 82 memset(&converter, 0, WORD_SIZE); 83 memcpy(converter.strData, p_moving, dataLeftLen); 84 ret = ptrace(PTRACE_POKEDATA, pid, dataAddr + counter * WORD_SIZE, converter.rawData); 85 } 86 } 87 88 void debugRegs(char * pTag, pid_t pid) 89 { 90 struct user_regs_struct regs; 91 memset(®s, 0, sizeof(struct user_regs_struct)); 92 ptrace(PTRACE_GETREGS, pid, NULL, ®s); 93 94 printf("----%s -----\n", pTag); 95 printf("regs.cs = 0x%lx\n", regs.cs); 96 printf("regs.rip = 0x%lx\n", regs.rip); 97 printf("regs.rsp = 0x%lx\n", regs.rsp); 98 printf("regs.rbp = 0x%lx\n", regs.rbp); 99 100 printf("regs.rax = 0x%lx\n", regs.rax); 101 printf("regs.rbx = 0x%lx\n", regs.rbx); 102 printf("regs.rcx = 0x%lx\n", regs.rcx); 103 printf("regs.rdx = 0x%lx\n", regs.rdx); 104 printf("regs.rsi = 0x%lx\n", regs.rsi); 105 printf("regs.rdi = 0x%lx\n", regs.rdi); 106 printf("regs.orig_rax = 0x%lx\n", regs.orig_rax); 107 printf("regs.eflags = 0x%lx\n", regs.eflags); 108 printf("regs.ds = 0x%lx\n", regs.ds); 109 printf("regs.es = 0x%lx\n", regs.es); 110 printf("regs.fs = 0x%lx\n", regs.fs); 111 printf("regs.gs = 0x%lx\n", regs.gs); 112 printf("regs.fs_base = 0x%lx\n", regs.fs_base); 113 printf("regs.gs_base = 0x%lx\n", regs.gs_base); 114 printf("----%s -----\n", pTag); 115 } 116 117 void debugInstructionByAddr(char * pTag, pid_t pid, unsigned long addr) 118 { 119 char instruction[WORD_SIZE]; 120 memset(instruction, 0, WORD_SIZE); 121 getData(pid, addr, WORD_SIZE, instruction); 122 123 printf("0x%lx, %s instruction =", addr, pTag); 124 int index; 125 for (index = 0; index < WORD_SIZE; index++) 126 { 127 printf(" 0x%02x ", (unsigned char) instruction[index]); 128 } 129 printf("\n"); 130 } 131 132 int main() 133 { 134 pid_t pid = 0; 135 int waitStatus = -1; 136 137 // enter the pid of the process you want attach 138 printf("enter pid that you want attach : "); 139 scanf("%d", &pid); 140 141 int ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL); 142 if (ret == -1) 143 { 144 printf("attach error\n"); 145 return 1; 146 } 147 printf("attach success\n"); 148 149 // wait 150 waitpid(pid, &waitStatus, 0); 151 152 /* pause the process */ 153 // 1, backup the orignal regs 154 struct user_regs_struct backupRegs; 155 memset(&backupRegs, 0, sizeof(struct user_regs_struct)); 156 ptrace(PTRACE_GETREGS, pid, NULL, &backupRegs); 157 // 2, backup the instruction 158 char backupInstruction[WORD_SIZE]; 159 memset(backupInstruction, 0, WORD_SIZE); 160 getData(pid, injectAddress, WORD_SIZE, backupInstruction); 161 // 3, insert break instruction —— "int 0x80, int3" 162 char breakInstruction[WORD_SIZE] = { 0xcd, 0x80, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0 }; 163 setData(pid, injectAddress, WORD_SIZE, breakInstruction); 164 // 4, PTRACE_CONT for execute "int 0x80, int3" instruction 165 ret = ptrace(PTRACE_CONT, pid, NULL, NULL); 166 if (ret == -1) 167 { 168 printf("continue error\n"); 169 return 1; 170 } 171 printf("continue success\n"); 172 173 // wait 174 waitpid(pid, &waitStatus, 0); 175 /* pause the process */ 176 177 // wait for 5 seconds 178 printf("the process is paused for 5s...\n"); 179 sleep(5); 180 181 /* continue the process */ 182 // 1, restore regs to the orignal address 183 ptrace(PTRACE_SETREGS, pid, NULL, &backupRegs); 184 // 2, restore instruction 185 setData(pid, injectAddress, WORD_SIZE, backupInstruction); 186 // 3, PTRACE_DETACH for execute the orignal code 187 ret = ptrace(PTRACE_DETACH, pid, NULL, NULL); 188 if (ret == -1) 189 { 190 printf("detach error\n"); 191 return 1; 192 } 193 printf("detach success\n"); 194 /* continue the process */ 195 196 return 0; 197 }
代码执行结果:被注入进程输出"I'm running",ptrace4进程注入后,被注入进程暂停输出"I'm running",5s后恢复。
最后说下调试过程中遇到的一个问题,在恢复被注入进程的运行时,总是会导致被注入进程segment fault,代码中加入了大量的debug函数用于分析问题,最后利用dmesg工具发现原因是恢复时写入注入地址指令有误,根本原因是之前认为breakInstruction只有3bytes,所以备份指令也只需要3bytes,于是便存储在char backupInstruction[3]中,而ptrace的读写单位都是word,在x64下是8bytes,所以在恢复时写入的指令为了3bytes的正确指令+5bytes的0x00空指令所致~