patrace简介 使用过Linux系统多多少少会接触方便我们查看执行的程序的系统调用的strace命令或者编程时使用gdb进行程序调试。他们幕后原理工作其实就是ptrace完成的。
我们通过man ptrace
命令可以查看ptrace的使用说明。 ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
ptrace函数的定义
1 2 #include <sys/ptrace.h> long ptrace (int request, int pid, int addr, int data) ;
函数描述: request参数决定了系统调用的功能:
PTRACE_TRACEME 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA 从内存地址中读取一个字节,内存地址由addr给出。
PTRACE_PEEKUSR 从USER区域中读取一个字节,偏移量为addr。
PTRACE_POKETEXT, PTRACE_POKEDATA 往内存地址中写入一个字节。内存地址由addr给出。
PTRACE_POKEUSR 往USER区域中写入一个字节。偏移量为addr。
PTRACE_SYSCALL, PTRACE_CONT 重新运行。
PTRACE_KILL 杀掉子进程,使它退出。
PTRACE_SINGLESTEP 设置单步执行标志
PTRACE_ATTACH 跟踪指定pid 进程。
PTRACE_DETACH 结束跟踪
Intel386特有:
PTRACE_GETREGS 读取寄存器
PTRACE_SETREGS 设置寄存器
PTRACE_GETFPREGS 读取浮点寄存器
PTRACE_SETFPREGS 设置浮点寄存器
返回值
成功返回0,出错返回-1;
注意:init进程不可以使用此函数
ptrace通过如下调用进入内核的1 2 SYSCALL_DEFINE4 (ptrace, long , request, long , pid, long , addr, long , data)
小试牛刀–简单的PTRACE_ME 这里首先使用PTRACE_ME实现父进程跟踪子进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <asm/ptrace-abi.h> /*ORIG_EAX*/ int main () { pid_t child; long orig_eax; child = fork(); if (child == 0 ){ ptrace(PTRACE_TRACEME, 0 , NULL , NULL ); execl("/bin/ls" ,"ls" ,NULL ); } else { wait(NULL ); orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 *ORIG_EAX, NULL ); printf ("The child made a system call %d\n" ,orig_eax); ptrace(PTRACE_CONT, child, NULL , NULL ); } return 0 ; }
运行结果:1 2 3 ~/code/ptrace# ./ptrace1 The child mad a system call 11 ~/code/ptrace# ptrace1 ptrace1.c
上述程序首先通过fork创建了一个子进程,子进程中通过PTRACE_TRACEME来告诉内核,让其他程序来跟踪我,然后通过execl函数执行ls命令;父进程中通过wait等待来自内核的通知,收到通知后立即接管子进程,然后查看其eax的值即系统调用号,完成对子进程的操作后,通过PTRACE_CON将控制权还给子进程,子进程接着执行ls命令。
从上面的结果可知子进程在执行ls命令时,执行了11号系统调用。通过查看/usr/include/asm/unistd.h文件可知11是函数execve的系统调用号。
在出现系统调用后,内核会将eax(存放系统调用号)保存起来,所有通过使用PTRACE_PEEKUSER可以读取出这个值。
ptrace实现进程中断 我们知道gdb在调试程序的时候可以让其停留在端点处。下面我通过ptrace来实现其基本原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <sys/user.h> #include <asm/ptrace-abi.h> #include <string.h> #include <stdio.h> const int long_size = sizeof (long );void getdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u{ long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, long_size); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, j); } str[len] = ' ' ; } void putdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { memcpy (data.chars, laddr, long_size); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { memcpy (data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); } } int main (int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs, newregs; long ins; char code[] = {0xcd ,0x80 ,0xcc ,0 }; char backup[4 ]; if (argc != 2 ) { printf ("Usage: %s <pid to be traced> " , 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); getdata(traced_process, regs.eip, backup, 3 ); putdata(traced_process, regs.eip, code, 3 ); ptrace(PTRACE_CONT, traced_process, NULL , NULL ); wait(NULL ); printf ("The process stopped, putting back " "the original instructions " ); printf ("Press <enter> to continue " ); getchar(); putdata(traced_process, regs.eip, backup, 3 ); ptrace(PTRACE_SETREGS, traced_process, NULL , ®s); ptrace(PTRACE_DETACH, traced_process, NULL , NULL ); return 0 ; }
目标测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <unistd.h> #include <stdio.h> int main () { int i; printf ("pid=%d\n" ,getpid()); for (i = 0 ;i < 10 ; ++i) { printf ("My counter: %d \n" , i); sleep(2 ); } return 0 ; }
先运行目标程序1 2 3 4 5 6 7 8 9 10 11 12 13 ~/code/ptrace pid=29158 My counter: 0 My counter: 1 My counter: 2 My counter: 3 //此处中断 My counter: 4 My counter: 5 My counter: 6 My counter: 7 My counter: 8 My counter: 9
test执行中执行ptrace程序1 2 ~/code/ptrace The process stopped, putting back the original instructions Press <enter> to continue //回车后中断结束
ptrace实现进程代码注入 上面的例子我们使用了我们仅仅向内存中插入了int 3指令,是进程中断。我们当然可以将一段shellcode写入执行…
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <sys/user.h> #include <asm/ptrace-abi.h> #include <string.h> #include <stdio.h> const int long_size = sizeof (long );void getdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u{ long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, long_size); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, j); } str[len] = ' ' ; } void putdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { memcpy (data.chars, laddr, long_size); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { memcpy (data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); } } long freespaceaddr (pid_t pid) { FILE *fp; char filename[30 ]; char line[85 ]; long addr; char str[20 ]; sprintf (filename, "/proc/%d/maps" , pid); fp = fopen(filename, "r" ); if (fp == NULL ) exit (1 ); while (fgets(line, 85 , fp) != NULL ) { sscanf (line, "%lx-%*lx %*s %*s %s" , &addr, str, str, str, str); if (strcmp (str, "00:00" ) == 0 ) break ; } fclose(fp); return addr; } int main (int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs, newregs; 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]; if (argc != 2 ) { printf ("Usage: %s <pid to be traced> " , 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); getdata(traced_process, regs.eip, backup, len); putdata(traced_process, regs.eip, insertcode, len); 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 " ); putdata(traced_process, regs.eip, backup, len); ptrace(PTRACE_SETREGS, traced_process, NULL , ®s); printf ("Letting it continue with " "original flow " ); ptrace(PTRACE_DETACH, traced_process, NULL , NULL ); return 0 ; }
这里我们只是注如了一个hello world
程序进行测试。 这里总体思路与前小节一样。由于上面往内存中嵌入的只是四个字节,而当我们将把一段代码直接像之前一样简单的写入正在执行的指令流中是不现实的,所以这里我们需要将代码指令插入到进程的自由内存空间中。通过查看/proc/PID/maps
文件就可以获得可用的进程自由空间分布。
这里我们先介绍下进程的内存映射
查看进程内存映射命令
cat /proc/进程id/maps
我们运行上面的test程序然后查看其内存映射如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ~/code/ptrace 005 bf000-005 c0000 r-xp 00000000 00 :00 0 [vdso]009 a4000-00 af7000 r-xp 00000000 08 :01 134385 /lib/tls/i686/cmov/libc-2.11 .1 .so00 af7000-00 af8000 ---p 00153000 08 :01 134385 /lib/tls/i686/cmov/libc-2.11 .1 .so00 af8000-00 afa000 r--p 00153000 08 :01 134385 /lib/tls/i686/cmov/libc-2.11 .1 .so00 afa000-00 afb000 rw-p 00155000 08 :01 134385 /lib/tls/i686/cmov/libc-2.11 .1 .so00 afb000-00 afe000 rw-p 00000000 00 :00 0 00 bf8000-00 c13000 r-xp 00000000 08 :01 129989 /lib/ld-2.11 .1 .so00 c13000-00 c14000 r--p 0001 a000 08 :01 129989 /lib/ld-2.11 .1 .so00 c14000-00 c15000 rw-p 0001 b000 08 :01 129989 /lib/ld-2.11 .1 .so08048000 -08049000 r-xp 00000000 08 :01 149844 /root/code/ptrace_1/test 108049000 -0804 a000 r--p 00000000 08 :01 149844 /root/code/ptrace_1/test 10804 a000-0804 b000 rw-p 00001000 08 :01 149844 /root/code/ptrace_1/test 1b77d5000-b77d6000 rw-p 00000000 00 :00 0 b77e3000-b77e6000 rw-p 00000000 00 :00 0 bfa31000-bfa46000 rw-p 00000000 00 :00 0 [stack]
显然/proc/PID/maps
是个临时的文件,进程结束也就消失了。 查看该文件,瞅瞅进程的虚拟地址空间是如何使用的。该文件一共分为6列,每一列具体含义如下:
地址
权限
偏移量
设备
节点
路径
库在进程中的内存范围
虚拟内存的权限
虚拟内存区域在映射文件中的偏移
映射文件的主/次设备号
映射文件的节点号
映射文件路径
函数freespaceaddr的主要功能就是查找空闲的内存,具体就不分析了。
执行测试测试程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ~/code/ptrace# ./test pid=31166 My counter: 0 My counter: 1 My counter: 2 My counter: 3 My counter: 4 Hello World My counter: 5 My counter: 6 My counter: 7 My counter: 8 My counter: 9
1 2 ~/code/ptrace The process stopped, Putting back the original instructions Letting it continue with original flow
由上面的结果可以看出我们已将一段指令代码插入到了目标进程中。
注:VSDS(Virtual Dynamically-lined Shared Object),这是一个由内核提供的虚拟.so文件,它不在磁盘上,而在内核里,由内核将其映射到一个地址空间中,被所有的程序共享
玩转系统调用 第一个例子中我们可以窥视了一个进程的系统调用号。
那我们可以过滤我们希望的进程调用,然后干一些好玩的事。例如我们对某一进程的write
系统调用感兴趣。
首先,我们了解write
函数1 ssize_t write (int handle, void *buf, int nbyte) ;
handle 是文件描述符; buf 是指定的缓冲区,即指针,指向一段内存单元; nbyte 是要写入文件指定的字节数; 返回值:写入文档的字节数(成功);-1(出错)
我们在当前工作目录下,通过strace ls
命令可以得到
1 2 3 4 5 6 7 ... write(1 , "ptrace1 ptrace2 ptrace3\t p" ..., 47pt race1 ptrace2 ptrace3 ptrace4 test ) = 47 write(1 , "ptrace1.c ptrace2.c ptrace3.c " ..., 51pt race1.c ptrace2.c ptrace3.c ptrace4.c test.c ) = 51 ...
我们直接看下ls
命令的结果:
1 2 3 ~/code/ptrace# ls ptrace1 ptrace2 ptrace3 ptrace4 test ptrace1.c ptrace2.c ptrace3.c ptrace4.c test.c
然后,我们知道一个系统掉用时传参会依次存放在EBX、ECX、EDX
而系统调用号会保存在EAX寄存器中,所以我们可以先获取EAX中的值,过滤出我们感兴趣的系统调用,这里我们过滤SYS_write
系统调用,然后通过EAX、EBX、ECX
中的值来改变传入参数…
我们依然创建一个子进程执行ls命令,然后过滤ls
的write
系统调用,改变其参数。。。 测测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <asm/ptrace-abi.h> #include <sys/syscall.h> const int long_size = sizeof (long );void getdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, long_size); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4 , NULL ); memcpy (laddr, data.chars, j); } str[len] = '\0' ; } void putdata (pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0 ; j = len / long_size; laddr = str; while (i < j) { memcpy (data.chars, laddr, long_size); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); ++i; laddr += long_size; } j = len % long_size; if (j != 0 ) { memcpy (data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * 4 , data.val); } } int main () { pid_t child; child = fork(); if (child == 0 ) { ptrace(PTRACE_TRACEME, 0 , NULL , NULL ); execl("/bin/ls" , "ls" , NULL ); } else { long orig_eax; long params[3 ]; int status; char *str, *laddr; int toggle = 0 ; while (1 ) { wait(&status); if (WIFEXITED(status)) break ; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL ); if (orig_eax == SYS_write) { if (toggle == 0 ) { toggle = 1 ; params[0 ] = ptrace(PTRACE_PEEKUSER, child, 4 * EBX, NULL ); params[1 ] = ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL ); params[2 ] = ptrace(PTRACE_PEEKUSER, child, 4 * EDX, NULL ); str = (char *)malloc ((params[2 ]+1 ) * sizeof (char )); getdata(child, params[1 ], str, params[2 ]); char *hi_str = "Hi,Monkee here!\t " ; strncpy (str,hi_str,strlen (hi_str)+1 ); putdata(child, params[1 ], str,strlen (hi_str)+1 ); } else { toggle = 0 ; } } ptrace(PTRACE_SYSCALL, child, NULL , NULL ); } } return 0 ; }
运行结果:1 2 ./ptrace4 Hi ,Monkee here ! Hi ,Monkee here !
可以看到,我们用Hi,Monkee here!
替换了正常输出。
参考