调试器的原理-详解ptrace函数及fork父子进程跟踪实例
2016-06-16 16:26
781 查看
最近仔细研究了一下linux调试程序的原理.gdb是linux下最为强大的调试工具,而strace可以拦截程序执行过程中的系统调用.他们的背后都隐藏了一个强悍的支持函数ptrace().调试程序过程中我们可以单步执行,逐步检查程序的输入输出,从而判断程序错误,当然我们也可以抛弃gdb,自己实现一个"外挂程序",拦截主程序中我们感兴趣的东西,比如ssh或ftp登录密码.
先看一个简单的程序:
我们感兴趣的是他输入了什么,使用strace输出如下(部分):
可以看到,在系统调用层面,程序使用了read函数实现了数据输入,所以"外挂"程序可以利用这一点,只针对read调用进行拦截:
需要说明一下
运行结果如下:
![](https://img-blog.csdn.net/20160616222813488?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
可以看到输入被成功捕获,用ssh试验,ssh同样不能幸免:
![](https://img-blog.csdn.net/20160616173837759?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
进一步,如果被跟踪程序使用了fork,那么我们如何跟踪其子进程或父进程呢?gdb中可以通过follow-fork-mode参数选择跟踪对象.那么具体反映到ptrace函数,该如何自己编写程序使用ptrace函数实现呢?
被跟踪程序:
关键在于ptrace(PTRACE_SETOPTIONS, pid, NULL, ptraceOption)函数,可以设置被跟踪的对象.这里需要说明一下,当设置ptraceOption后,被跟踪程序父进程收到SIGTRAP信号暂停,子进程收到SIGSTOP信号暂停.因此都需要跟踪程序显式启动它们.另外,这个程序中判断被跟踪程序的父子进程是通过与waitpid返回的进程号比较得知的,这也是strace中使用的方法。另一种方法是在被跟踪程序的父进程中调用ptrace的PTRACE_GETEVENTMSG获得子进程的进程号。
运行结果:
![](https://img-blog.csdn.net/20160616203853518?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
其中第一行是我们在键盘上的输入,一行为被跟踪程序的输出,还有一行为“外挂”程序的输出。
先看一个简单的程序:
#include<stdio.h> int main(void){ char buf[1024]; gets(buf); puts(buf); return 0; }
我们感兴趣的是他输入了什么,使用strace输出如下(部分):
fstat64(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a9000 read(0, AAAAAAAAAAAA "AAAAAAAAAAAA\n", 1024) = 13 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77a8000 write(1, "AAAAAAAAAAAA\n", 13AAAAAAAAAAAA ) = 13 exit_group(0) = ? +++ exited with 0 +++
可以看到,在系统调用层面,程序使用了read函数实现了数据输入,所以"外挂"程序可以利用这一点,只针对read调用进行拦截:
/* *该程序仅能在32位x86 linux中使用.64位系统需要修改struct user_regs_struct regs中寄存器格式. */ #include<stdio.h> #include<sys/ptrace.h> #include<unistd.h> #include<sys/wait.h> #include<errno.h> #include<string.h> #include<stdlib.h> #include<sys/syscall.h> #include<sys/user.h> #include<strings.h> void getdata(pid_t pid,long addr, char *str,int len){ int i; long data; for(i=0;i<(len+sizeof(long)-1)/sizeof(long);i++){ data=ptrace(PTRACE_PEEKDATA,pid,addr+i*sizeof(long),NULL); *(long *)(str+i*sizeof(long))=data; } *(str+len)='\0'; printf("%s",str); } int main(int argc,char **argv){ pid_t pid; int status,buf_len,i; struct user_regs_struct regs; char *buf; int insyscall=0; if (argc<2){ printf("Usage:./a.out prog <args>\n"); return 1; } if ((pid=fork())==0){ //子进程运行被跟踪程序 ptrace(PTRACE_TRACEME,0,NULL,NULL); status=execvp(argv[1],argv+1); if(status<0){ printf("execvp %s error:%s\n",argv[1],strerror(errno)); return -1; } }else if(pid>0){ //父进程拦截子进程所有系统调用 while(1){ wait(&status); if(WIFEXITED(status)) //如果子进程正常退出,则 4000 父进程退出循环 break; ptrace(PTRACE_GETREGS, pid, 0, ®s); //dump子进程寄存器 if (regs.orig_eax!=SYS_read){ //判断子进程系统调用号,非read调用继续循环 ptrace(PTRACE_SYSCALL,pid,NULL,NULL); continue; } /* if (regs.ebx!=0){ //判断输入是否来自stdin ptrace(PTRACE_SYSCALL,pid,NULL,NULL); continue; } */ if (insyscall==0){ //一次系统调用会触发两次,第一次是刚进入read调用,另一次是刚退出read调用 ptrace(PTRACE_SYSCALL,pid,NULL,NULL); insyscall=1; }else{ //这里需要的是刚进入调用时的情况,因为这里有用户刚输入的数据 buf_len=(regs.edx+sizeof(long)-1)/sizeof(long)*sizeof(long)+1; if((buf=(char *)malloc(buf_len))==NULL){ printf("malloc error!\n"); return 1; } bzero(buf,buf_len); //分配buf空间,并清空 getdata(pid,regs.ecx,buf,regs.edx); //定位到输入数据的内存地址和长度,进行dump free(buf); ptrace(PTRACE_SYSCALL,pid,NULL,NULL); insyscall=0; } } return 0; }else{ printf("fork error!\n"); return 1; } }
需要说明一下
getdata(pid,regs.ecx,buf,regs.edx);ecx保存了数据地址,edx保存了数据长度.这里可以回顾一下read系统调用格式:SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count);编译成汇编后就成了如下的指令(注意gcc参数从右向左压栈):
伪指令!!!! pushl count pushl buf pushl fd pushl read int $0x80而进入系统调用后,内核从栈内取出read调用参数:eax取到read,即系统调用号,ebx对应fd,ecx对应buf,edx对应count.有兴趣的可以参考linux系统调用过程,这里不再赘述.
运行结果如下:
可以看到输入被成功捕获,用ssh试验,ssh同样不能幸免:
进一步,如果被跟踪程序使用了fork,那么我们如何跟踪其子进程或父进程呢?gdb中可以通过follow-fork-mode参数选择跟踪对象.那么具体反映到ptrace函数,该如何自己编写程序使用ptrace函数实现呢?
被跟踪程序:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include <sys/wait.h> int main(){ int pid; if (pid=fork()){ waitpid(pid,NULL,0); return 0; }else{ char buf[1024]; scanf("%s",buf); printf("%s\n",buf); return 0; } }对应的外挂程序:
#include<stdio.h> #include<sys/ptrace.h> #include<unistd.h> #include<sys/wait.h> #include<errno.h> #include<string.h> #include<stdlib.h> #include<sys/syscall.h> #include<sys/user.h> #include<strings.h> void getdata(pid_t pid,long addr, char *str,int len){ int i; long data; for(i=0;i<(len+sizeof(long)-1)/sizeof(long);i++){ data=ptrace(PTRACE_PEEKDATA,pid,addr+i*sizeof(long),NULL); *(long *)(str+i*sizeof(long))=data; } *(str+len)='\0'; printf("%s",str); } int main(int argc,char **argv){ pid_t pid; int status,buf_len,i; struct user_regs_struct regs; char *buf; int insyscall=0; if (argc<2){ printf("Usage:./a.out prog <args>\n"); return 1; } if ((pid=fork())==0){ ptrace(PTRACE_TRACEME,0,NULL,NULL); status=execvp(argv[1],argv+1); if(status<0){ printf("execvp %s error:%s\n",argv[1],strerror(errno)); return -1; } }else if(pid>0){ wait(NULL); long ptraceOption = PTRACE_O_TRACEFORK; ptrace(PTRACE_SETOPTIONS, pid, NULL, ptraceOption); //设置ptrace属性 ptrace(PTRACE_CONT, pid, NULL, NULL); //设置好了后让被跟踪程序继续运行 while(1){ pid_t pid_waited=waitpid(-1,&status,0); //等待信号 if (pid_waited == -1) break; if(WIFEXITED(status)) continue; if (pid_waited==pid){ //如果发出信号进程的进程号跟pid一致,则说明它是被跟踪程序的父进程,否则是被跟踪程序的子进程 if(WIFSTOPPED(status)){ ptrace(PTRACE_CONT,pid_waited,NULL,NULL); continue; } } //下面是被跟踪程序的子进程,开始dump子进程的输入数据 ptrace(PTRACE_GETREGS, pid_waited, 0, ®s); if (regs.orig_eax!=SYS_read){ ptrace(PTRACE_SYSCALL,pid_waited,NULL,NULL); continue; } if (insyscall==0){ ptrace(PTRACE_SYSCALL,pid_waited,NULL,NULL); insyscall=1; }else{ buf_len=(regs.edx+sizeof(long)-1)/sizeof(long)*sizeof(long)+1; if((buf=(char *)malloc(buf_len))==NULL){ printf("malloc error!\n"); return 1; } bzero(buf,buf_len); getdata(pid_waited,regs.ecx,buf,regs.edx); free(buf); ptrace(PTRACE_SYSCALL,pid_waited,NULL,NULL); insyscall=0; } } return 0; }else{ printf("fork error!\n"); return 1; } }
关键在于ptrace(PTRACE_SETOPTIONS, pid, NULL, ptraceOption)函数,可以设置被跟踪的对象.这里需要说明一下,当设置ptraceOption后,被跟踪程序父进程收到SIGTRAP信号暂停,子进程收到SIGSTOP信号暂停.因此都需要跟踪程序显式启动它们.另外,这个程序中判断被跟踪程序的父子进程是通过与waitpid返回的进程号比较得知的,这也是strace中使用的方法。另一种方法是在被跟踪程序的父进程中调用ptrace的PTRACE_GETEVENTMSG获得子进程的进程号。
运行结果:
其中第一行是我们在键盘上的输入,一行为被跟踪程序的输出,还有一行为“外挂”程序的输出。
相关文章推荐
- window putty 生成公钥私钥 用于git 或者 ssh 等
- SQL: Java 连接 MySQL
- 快速多人游戏(2) - 客户端预测和服务器校验
- JS代码实现根据时间变换页面背景效果
- 用Matlab来恢复PNG透明度
- 关于延时操作的使用
- C++删除某个特定的进程
- 排序(2)——插入/希尔/选择/快速排序及优化
- QML Flipable、Flickable和状态与动画 下篇
- 关于Fragment你所需知道的一切
- 访谈 |《CSS揭秘》译者CSS魔法:学海无涯,而吾生有涯
- HTTP1.0和HTTP1.1的区别
- 大三迷茫的心情。
- CSS中的margin与padding
- 快速多人游戏(1) - 介绍
- css选择器
- Itex for java技巧总结
- VS2005导出函数命名规则
- C/C++ 网络编程3: 套接字基础
- scala笔记