参考文献:
http://man7.org/linux/man-pages/man2/ptrace.2.html
https://www.linuxjournal.com/article/6100?page=0,0
《程序员的自我修养》
目标:
1)对ptrace 有正确理解, 代码实现
2)ptrace的用处
3)ptrace代码注入
4)如何防止反调试
一. ptrace 介绍
Ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号
man手册介绍:http://man7.org/linux/man-pages/man2/ptrace.2.html
二. ptrace 的函数详解:
2.1 函数声明
1 long ptrace(enum __ptrace_request request, 2 pid_t pid, 3 void *addr, 4 void *data);
.参数request:请求ptrace执行的操作
.参数pid:目标进程的ID
.参数addr:目标进程的地址值
.参数data:作用则根据request的不同而变化,如果需要向目标进程中写入数据,data存放的是需要写入的数据;如果从目标进程中读数据,data将存放返回的数据
request参数决定了CODE的行为以及后续的参数是如何被使用的,参数request的常用的值如下:
三. 代码示例理解
在 i386 平台下(本文所有代码都基于 i386), 系统调用的编号会被放在寄存器 %eax 中,而系统调用的参数会被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,比如说,对于下面的系统调用:
write(2, "Hello", 5)
汇编代码:
1 movl $4, %eax 2 movl $2, %ebx 3 movl $hello,%ecx 4 movl $5, %edx 5 int $0x80
看完上面简单的例子,现在我们来看看 ptrace 又是怎样执行的:
参考的文献中的代码
1 #include <sys/ptrace.h> 2 #include <sys/types.h> 3 #include <sys/wait.h> 4 #include <unistd.h> 5 #include <linux/user.h> /* For constants 6 ORIG_EAX etc */ 7 int main() 8 { pid_t child; 9 long orig_eax; 10 child = fork(); 11 if(child == 0) { 12 ptrace(PTRACE_TRACEME, 0, NULL, NULL); 13 execl("/bin/ls", "ls", NULL); 14 } 15 else { 16 wait(NULL); 17 orig_eax = ptrace(PTRACE_PEEKUSER, 18 child, 4 * ORIG_EAX, 19 NULL); 20 printf("The child made a " 21 "system call %ldn", orig_eax); 22 ptrace(PTRACE_CONT, child, NULL, NULL); 23 } 24 return 0; 25 }
gcc -o ptrace ptrace.c
报错:
ptrace.c:6:24: error: linux/user.h: No such file or directory ptrace.c: In function ‘main’: ptrace.c:18: error: ‘ORIG_EAX’ undeclared (first use in this function) ptrace.c:18: error: (Each undeclared identifier is reported only once ptrace.c:18: error: for each function it appears in.) ptrace.c:20: warning: incompatible implicit declaration of built-in function ‘printf’
由于我的环境是64 位系统
这里有两个地方有问题
两个修改方案:
1) linux/user.h 改成 sys/reg.h
long original_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
地址从“4*orig-eax”更改为“8*orig-rax”,因为它是要在用户区域中读取的地址,而user-regs-struct中的orig-rax成员是第15个成员(从0开始)。文件‘sys/reg.h‘中的orig_rax定义指定其位置:定义orig_rax 15
因为其他成员在64位机器上的大小是8,所以地址是:8*orig_rax
我们看一下头文件结构体
1 struct user_regs_struct 2 { 3 unsigned long int r15; 4 unsigned long int r14; 5 unsigned long int r13; 6 unsigned long int r12; 7 unsigned long int rbp; 8 unsigned long int rbx; 9 unsigned long int r11; 10 unsigned long int r10; 11 unsigned long int r9; 12 unsigned long int r8; 13 unsigned long int rax; 14 unsigned long int rcx; 15 unsigned long int rdx; 16 unsigned long int rsi; 17 unsigned long int rdi; 18 unsigned long int orig_rax; 19 unsigned long int rip; 20 unsigned long int cs; 21 unsigned long int eflags; 22 unsigned long int rsp; 23 unsigned long int ss; 24 unsigned long int fs_base; 25 unsigned long int gs_base; 26 unsigned long int ds; 27 unsigned long int es; 28 unsigned long int fs; 29 unsigned long int gs; 30 }; 31 32 struct user 33 { 34 struct user_regs_struct regs; 35 int u_fpvalid; 36 struct user_fpregs_struct i387; 37 unsigned long int u_tsize; 38 unsigned long int u_dsize; 39 unsigned long int u_ssize; 40 unsigned long int start_code; 41 unsigned long int start_stack; 42 long int signal; 43 int reserved; 44 struct user_regs_struct* u_ar0; 45 struct user_fpregs_struct* u_fpstate; 46 unsigned long int magic; 47 char u_comm [32]; 48 unsigned long int u_debugreg [8]; 49 };
2)修改‘linux/user.h’ 为 ‘sys/user.h’
1 struct user_regs_struct regs; 2 ptrace(PTRACE_GETREGS, child, NULL, ®s); 3 printf("The child made a system call %ldn", regs.orig_rax);
第二个更简单,因为它不需要计算位置,但它读取的数据比第一个多
我认为,如果我们直接使用orig_x字段的地址,会更清楚、更容易理解:
1 struct user* user_space = (struct user*)0; 2 long original_rax = ptrace(PTRACE_PEEKUSER, child, &user_space->regs.orig_rax, NULL);
我们现在可以编译和运行它了,但是我们得到了:“子系统调用59”,这与原来的“11”不同,有什么问题吗?
在文件sys/syscall.h中,它包含文件‘asm/unistd.h‘,注释中指出该文件列出了系统调用:
1 /* This file should list the numbers of the system calls the system knows. 2 But instead of duplicating this we use the information available 3 from the kernel sources. */ 4 #include <asm/unistd.h>
是由于头文件asm/unistd.h 包含了不同的文件,根据 __i386__ 和_ILP32__:
1 # ifdef __i386__ 2 # include <asm/unistd_32.h> 3 # elif defined(__ILP32__) 4 # include <asm/unistd_x32.h> 5 # else 6 # include <asm/unistd_64.h> 7 # endif
从头文件里面asm/unistd_64.h 我们可以看到64位系统调用:
#define __NR_execve 59
第一个程序搞定, 让我们继续往下看吧, 蹩脚的英语确实让人头疼
第二个示例:读取系统调用的参数
通过调用ptrace并传入PTRACE_PEEKUSER作为第一个参数,我们可以检查子进程中,保存了该进程的寄存器的内容(及其它一些内容)的用户态内存区域(USER area)。内核把寄存器的内容保存到这块区域,就是为了能够让父进程通过ptrace来读取,下面举一个例子来说明一下:
1 #include <sys/ptrace.h> 2 #include <sys/types.h> 3 #include <sys/wait.h> 4 #include <unistd.h> 5 #include <linux/user.h> 6 #include <sys/syscall.h> /* For SYS_write etc */ 7 int main() 8 { pid_t child; 9 long orig_eax, eax; 10 long params[3]; 11 int status; 12 int insyscall = 0; 13 child = fork(); 14 if(child == 0) { 15 ptrace(PTRACE_TRACEME, 0, NULL, NULL); 16 execl("/bin/ls", "ls", NULL); 17 } 18 else { 19 while(1) { 20 wait(&status); 21 if(WIFEXITED(status)) 22 break; 23 orig_eax = ptrace(PTRACE_PEEKUSER, 24 child, 4 * ORIG_EAX, NULL); 25 if(orig_eax == SYS_write) { 26 if(insyscall == 0) { 27 /* Syscall entry */ 28 insyscall = 1; 29 params[0] = ptrace(PTRACE_PEEKUSER, 30 child, 4 * EBX, 31 NULL); 32 params[1] = ptrace(PTRACE_PEEKUSER, 33 child, 4 * ECX, 34 NULL); 35 params[2] = ptrace(PTRACE_PEEKUSER, 36 child, 4 * EDX, 37 NULL); 38 printf("Write called with " 39 "%ld, %ld, %ld\n", 40 params[0], params[1], 41 params[2]); 42 } 43 else { /* Syscall exit */ 44 eax = ptrace(PTRACE_PEEKUSER, 45 child, 4 * EAX, NULL); 46 printf("Write returned " 47 "with %ld\n", eax); 48 insyscall = 0; 49 } 50 } 51 ptrace(PTRACE_SYSCALL, 52 child, NULL, NULL); 53 } 54 } 55 return 0; 56 }
待续 ...
原文:https://www.cnblogs.com/mysky007/p/11047943.html