linux ptrace II
Posted mmmmar的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux ptrace II相关的知识,希望对你有一定的参考价值。
第一篇 linux ptrace I
在之前的文章中我们用ptrace函数实现了查看系统调用参数的功能。在这篇文章中,我们会用ptrace函数实现设置断点,跟代码注入功能。
参考资料
Playing with ptrace, Part I
Playing with ptrace, Part II
英文好的推荐直接看老外的文章。但是其代码是运行在x86系统上的,本文中将其移植到了x86_64系统。
进程附加
在之前的文章中,我们都是trace自己程序fork出来的子进程,现在我们来看一下如何trace一个正在运行的进程。
trace一个正在运行的进程称为进程附加(attach)。使用的是ptrace函数的PTRACE_ATTACH参数。当一个进程成功附加到一个正在运行的进程时,此进程会成为被附加进程的父进程,同时向被附加的进程发送一个SIGSTOP信号,让其停止,这时我们就可以对其进行操纵。当我们完成对tracee的操作后就可以使用ptrace的PTRACE_DETACH参数停止附加。
我们用一个循环来模拟一个正在运行的进程,下边称此程序为hello
int main() { int i; for(i = 0;i < 10; ++i) { printf("My counter: %d\n", i); sleep(2); } return 0; }
本文之后所有的程序都以此程序当作被附加的进程。在其运行之后我们可以使用 ps -h 命令查看其进程号(pid),以便我们通过进程号对其附加。
接下来看一个简单的进程附加的例子
#include <sys/types.h> #include <sys/reg.h> #include <sys/user.h> #include <sys/wait.h> #include <sys/ptrace.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs; long ins; if(argc != 2) { puts("no pid input"); exit(1); } traced_process = atoi(argv[1]); printf("try to trace pid :%u\n",traced_process); if(ptrace(PTRACE_ATTACH,traced_process,NULL,NULL)==-1) { perror("trace error:"); } wait(NULL); if(ptrace(PTRACE_GETREGS,traced_process,NULL,®s)==-1) { perror("trace error:"); } ins = ptrace(PTRACE_PEEKTEXT,traced_process,regs.rip,NULL); if(ins == -1) { perror("trace error:"); } printf("EIP:%llx Instruction executed: %lx\n",regs.rip,ins); if(ptrace(PTRACE_DETACH,traced_process,NULL,NULL)==-1) { perror("trace error:"); } return 0; }
上边的程序对hello进行了附加,等其停下来以后,读取hello要运行的下一条指令的内容(地址存在rip中)。读取之后停止附加,让hello继续运行。
设置断点
下面要实现的是许多调试器都拥有的设置断点功能。
设置断点的原理:假如要在A地址处设置断点,可以把A地址处的指令替换为一条trap指令,就是说当tracee运行完这条被替换的指令后会自动停止,然后告知tracer自己已停止。
代码如下:
#include <sys/ptrace.h> #include <sys/reg.h> #include <sys/user.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define LONG_SIZE 8 void getdata(pid_t child, long addr,char *str,int len) { char *laddr = str; int i = 0,j = len/LONG_SIZE; union u{ long val; char chars[LONG_SIZE]; } word; while(i<j) { word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL); if(word.val == -1) perror("trace error"); memcpy(laddr,word.chars,LONG_SIZE); ++i; laddr += LONG_SIZE; } j = len %LONG_SIZE; if(j!=0) { word.val == ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL); if(word.val == -1) perror("trace error"); } str[len] = ‘\0‘; } void putdata(pid_t child,long addr,char *str,int len) { char *laddr = str; int i = 0, j = len/LONG_SIZE; union u{ long val; char chars[LONG_SIZE]; }word; while(i<j) { memcpy(word.chars,laddr,LONG_SIZE); if(ptrace(PTRACE_POKEDATA,child,addr+i*LONG_SIZE,word.val) == -1) perror("trace error"); ++i; laddr += LONG_SIZE; } j = len % LONG_SIZE; if(j != 0) { word.val = 0; memcpy(word.chars,laddr,j); if(ptrace(PTRACE_POKEDATA,child,addr+i*LONG_SIZE,word.val) == -1) perror("trace error"); } } void printBytes(const char* tip,char* codes,int len) { int i; printf("%s :",tip); for(i = 0;i<len;++i) { printf("%02x ",(unsigned char)codes[i]); } puts(""); } #define CODE_SIZE 8 int main(int argc ,char *argv[]) { if(argc != 2) { puts("no pid input"); exit(1); } pid_t traced_process; struct user_regs_struct regs; long ins; char code[LONG_SIZE] = {0xcc}; char backup[LONG_SIZE]; traced_process = atoi(argv[1]); printf("try to attach pid:%u\n",traced_process); if(ptrace(PTRACE_ATTACH,traced_process,NULL,NULL) == -1) { perror("trace attach error"); } wait(NULL); if(ptrace(PTRACE_GETREGS,traced_process,NULL,®s) == -1) { perror("trace get regs error"); } //copy instructions into backup variable getdata(traced_process,regs.rip,backup,CODE_SIZE); printBytes("get tracee instuction",backup,LONG_SIZE); puts("try to set breakpoint"); printBytes("set breakpoint instruction",code,LONG_SIZE); putdata(traced_process,regs.rip,code,CODE_SIZE); if(ptrace(PTRACE_CONT,traced_process,NULL,NULL) == -1) { perror("trace continue error"); } wait(NULL); puts("the process stopped Press <Enter> to continue"); getchar(); printBytes("place breakpoint instruction with tracee instruction",backup,LONG_SIZE); putdata(traced_process,regs.rip,backup,CODE_SIZE); ptrace(PTRACE_SETREGS,traced_process,NULL,®s); ptrace(PTRACE_DETACH,traced_process,NULL,NULL); return 0;
}
这里在hello程序停下来后将rip指向的指令备份在了backup数组中,同时把rip指向的指令替换为code数组中的指令。
code数组中0xcc对应为汇编指令的
int3 ;软中断指令,执行后程序会陷入中断停止,同时发送signal,可被tracer接收
当tracer接收到软中断指令发送的signal时,从等待(wait)中被唤醒,把被替换的指令还原回去,同时也将寄存器还原,最后停止附加,让hello继续执行。
示意图如下
代码注入
接下来我们实现最后一个功能,进行代码注入,让hello程序打印出一行 “hello world"
要进行代码注入,首先我们要知道被注入代码的机器指令是什么。
先写出要进行注入代码的汇编代码
section .text global main main: jmp forward backward: pop rsi mov rax, 1 mov rdi, 1 mov rdx, 13 syscall int3 forward: call backward db "Hello world",0xa
注意,这里为什么要跳来跳去呢,因为我们要定位字符串的地址,在执行 call backward 这条指令时,会把其下一条指令,也就是字符串的地址压入栈中。然后 pop rsi 将其取出。
使用nasm工具对其进行汇编
nasm -f elf64 inject.asm
再使用objdump工具查看对应的机器码
objdump -d inject.o
现在就可以将其注入到hello的进程里了。方法同设置断点一样,这里只贴出主函数代码
#define CODE_SIZE 48
int main(int argc ,char *argv[]) { if(argc<2) { puts("no pid input"); exit(1); } pid_t tracee = atoi(argv[1]); char code_inject[CODE_SIZE] ={0xeb,0x13,0x5e,0xb8,0x01,0x00,0x00,0x00,0xbf,0x01,0x00,0x00,0x00,0xba,0x0d,0x00,0x00,0x00,0x0f,0x05,0xcc,0xe8,0xe8,0xff,0xff,0xff,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x77,0x6f,0x72,0x6c,0x64,0xa}; char code_backup[CODE_SIZE]; struct user_regs_struct oldregs,regs; long ins; if(ptrace(PTRACE_ATTACH,tracee,NULL,NULL) == -1) { perror("attach error"); } wait(NULL); puts("attach success"); ptrace(PTRACE_GETREGS,tracee,NULL,®s); long addr = freeSpaceAddr(tracee); // long addr = regs.rip; printf("find free addr %lx\n",addr); getdata(tracee,addr,code_backup,CODE_SIZE); putdata(tracee,addr,code_inject,CODE_SIZE); memcpy(&oldregs,®s,sizeof(regs)); regs.rip = addr; printf("new rip :%llx\n",regs.rip); if(ptrace(PTRACE_SETREGS,tracee,NULL,®s) == -1) { perror("set regs error"); } puts("replace instructions success, continue tracee"); if(ptrace(PTRACE_CONT,tracee,NULL,NULL) == -1) { perror("continue tracee error"); } wait(NULL); ptrace(PTRACE_GETREGS,tracee,NULL,®s); printf("tracee end at rip: %llx\n",regs.rip); puts("tracee has stopped, putting back original instructions"); putdata(tracee,addr,code_backup,CODE_SIZE); if(ptrace(PTRACE_SETREGS,tracee,NULL,&oldregs) == -1) { perror("put original instuctions error"); } ptrace(PTRACE_DETACH,tracee,NULL,NULL); return 0; }
以上我们完成了设置断点和代码注入的功能。在老外文章 playing with ptrace 2 中还有一小节。是讲我们之前的代码注入是将代码注入到tracee的代码空间里,这样做不是很好。可以通过 搜索/proc/pid/maps文件找到一块空闲空间进行代码注入。不过老外没有讲具体的搜索空闲内存空间的原理,按照他的代码我没有成功。有成功的朋友欢迎补充。
以上是关于linux ptrace II的主要内容,如果未能解决你的问题,请参考以下文章
ptrace PTRACE_ATTACH 失败 - 用户拥有的进程的 Linux 权限
Linux C++ ptrace——映射子进程内存的所有单元
如何使用 Amazon Linux 2、单容器、无 Docker Compose 在 AWS Elastic Beanstalk 上获取 SYS_PTRACE