在Linux上的ptrace学习和代码注入
一、函数定义
long ptrace(enum __ptrace_request request,pid_t pid,void *addr,void *data);
request:决定要执行的操作;
pid:是目标进程的进程id;
addr:地址值
data:根据request值变化作用,存放数据。
头文件在:
#include <sys/ptrace.h>
request值有哪些?
0--PTRACE_TRACEME:指示此进程将由其父进程跟踪(换而言之,子进程运执行此操作就会允许别人来跟踪他自己),子进程进入阻塞态。如果进程的父进程不期望跟踪它,那么进程不应该发出此请求。使用示例:ptrace(PTRACE_TRACEME,0,0,0)。 1--PTRACE_PEEKTEXT, 2-- PTRACE_PEEKDATA:在目标进程的内存中读取地址地址处的一个字的内容,并返回该内容。 3--PTRACE_PEEKUSER:在目标进程的用户区域的偏移地址处(参数addr指定)读入一个字的内容。关于偏移大小对应的内容,更多信息可以在sys/user.h中了解。 4--PTRACE_POKETEXT, 5--PTRACE_POKEDATA:将参数data的内容复制到参数addr所指的地址处。两个相同。 6--PTRACE_POKEUSER:将参数data的内容复制到参数addr所指用户区域中的偏移。 12--PTRACE_GETREGS, 14--PTRACE_GETFPREGS:将目标进程(常指子进程)的通用寄存器或浮点寄存器的值复制到跟踪程序(常指父进程)的地址空间中(参数addr设置)。 7--PTRACE_CONT:让已阻塞的目标进程继续运行。如果数据非零,它被解释为要发送到示踪物的信号的数目;否则,没有信号交付。 24--PTRACE_SYSCALL, 9--PTRACE_SINGLESTEP: 8--PTRACE_KILL:向目标进程发送一个SIGKILL信号来终止它 16--PTRACE_ATTACH:附加到pid中指定的进程,跟踪(或调试)该进程。 17--PTRACE_DETACH:断开附加的进程。
更多可以看sys/user.h了解。
ptrace函数(系统调用)提供了一种跟踪另一个进程的方法,控制该进程执行流程的方法,并获取和更改该进程的内存和寄存器的值。其主要用于实现断点调试和系统调用跟踪。ptrace是以线程为单位,若子进程中存在多个线程,则父进程需要ptrace所有线程。
二、用法
操作系统通过系统调用的标准机制提供服务。它们为访问底层硬件和低级服务(例如文件系统)提供了标准 API。当进程想要调用系统调用时,它会将系统调用的参数放入寄存器并调用软中断 0x80。这个软中断就像是通往内核模式的大门,内核在检查完参数后会执行系统调用。
在 i386 架构上,系统调用号放在寄存器 %eax 中。系统调用的参数按顺序放入寄存器 %ebx、%ecx、%edx、%esi 和 %edi。
ptrace的调用效果什么时候在子进程触发呢?
在执行系统调用之前,内核会检查进程是否正在被跟踪。如果是,内核停止进程并将控制权交给跟踪进程,以便它可以检查和修改被跟踪进程的寄存器。我们可以使用 PTRACE_CONT 作为第一个参数使子进程继续执行。
ptrace函数读写数据是在进入和离开call时触发。
示例:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> int main() { pid_t child; long orig_eax, eax; long params[3]; int status; int insyscall = 0; struct user_regs_struct regs; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { while(1) { wait(&status); if(WIFEXITED(status)) break; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(insyscall == 0) { /* Syscall entry */ insyscall = 1; ptrace(PTRACE_GETREGS, child, NULL, ®s); printf("Write called with " "%ld, %ld, %ld\n", regs.ebx, regs.ecx, regs.edx); } else { /* Syscall exit */ eax = ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL); printf("Write returned " "with %ld\n", eax); insyscall = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
我们开始创建进程派生了一个子进程,而该子进程就是我们要跟踪的进程。首先子进程调用了ptrace函数,第一个参数为 PTRACE_TRACEME,即告诉内核该进程正在被跟踪,并且当子进程执行下一步函数调用时,它将控制权交给其父进程。父进程通过 wait函数去等待来自内核的通知,然后父进程可以检查查看子进程的寄存器使用情况。
我们在调用 ptrace(PTRACE_TRACEME, ..) 后将要跟踪的进程作为子进程运行。如果只是想查看进程如何进行系统调用并跟踪程序,这就足够了。如果要跟踪或调试已经运行的进程,则应使用 ptrace(PTRACE_ATTACH, ..)。
使用 PTRACE_PEEKUSER 作为第一个参数调用 ptrace函数,我们可以检查寄存器的内容和用户空间内存的内容。访问寄存器时,第三参数为4*EAX、4*EBX、4*ECX、4*EDX、4*ESI、4*EDI等。访问内存空间时,只要第三参数为地址就行。还可以使用 PTRACE_POKEDATA 调用 ptrace 来更改数据值。它的工作方式与 PTRACE_PEEKDATA 完全相同。
wait函数用于检查子进程是否已经退出。
使用PTRACE_GETREGS 作为第一个参数调用ptrace函数,将在进入一个call中时获取所有的寄存器的值。
使用PTRACE_SYSCALL 作为第一个参数调用ptrace函数,使内核恢复子进程执行并在下一个系统调用进入/退出时停止子进程。
我们可以使用 PTRACE_POKEDATA 作为第一个参数调用ptrace函数来更改寄存器或内存空间的数据值,它的工作方式与 PTRACE_PEEKDATA 完全相同。
ptrace提供了单步执行子进程代码的功能。使用 ptrace(PTRACE_SINGLESTEP, child,NULL, NULL) 告诉内核在每条指令处停止子进程,并让父进程控制。
三、相关函数
fork():创建子进程,返回不等于0时是在父进程代码块中,为0时是在子进程中。
wait(&status):等待子进程结束,检测到结束或成为僵尸进程(先把它杀了)就会返回。返回值为子进程pid(正常返回)或-1(没有子进程,fork前调用造成)。
四、代码注入
思路:我们先将shellcode注入到目标进程的内存空间中,再通过ptrace函数跟踪该进程,在遇到即将进入目标函数时断下进程,保存执行时的上下文信息(寄存器信息),并修改eip指向的地址为注入代码的地址。
代码如下:
int main(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct oldregs, regs; long ins; int len = 41; char insertcode[] = "\xeb\x15\x5e\xb8\x04\x00" "\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba" "\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff" "\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f" "\x72\x6c\x64\x0a\x00"; char backup[len]; long addr; if(argc != 2) { printf("Usage: %s <pid to be traced>\n", argv[0], argv[1]); exit(1); } traced_process = atoi(argv[1]); ptrace(PTRACE_ATTACH, traced_process, NULL, NULL); wait(NULL); ptrace(PTRACE_GETREGS, traced_process, NULL, ®s); addr = freespaceaddr(traced_process); getdata(traced_process, addr, backup, len); putdata(traced_process, addr, insertcode, len); memcpy(&oldregs, ®s, sizeof(regs)); regs.eip = addr; ptrace(PTRACE_SETREGS, traced_process, NULL, ®s); ptrace(PTRACE_CONT, traced_process, NULL, NULL); wait(NULL); printf("The process stopped, Putting back " "the original instructions\n"); putdata(traced_process, addr, backup, len); ptrace(PTRACE_SETREGS, traced_process, NULL, &oldregs); printf("Letting it continue with " "original flow\n"); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0; }
五、参考链接
https://www.linuxjournal.com/article/6100
https://www.linuxjournal.com/article/6210