您的位置:首页 > 其它

调试器的原理-详解ptrace函数及fork父子进程跟踪实例

2016-06-16 16:26 781 查看
最近仔细研究了一下linux调试程序的原理.gdb是linux下最为强大的调试工具,而strace可以拦截程序执行过程中的系统调用.他们的背后都隐藏了一个强悍的支持函数ptrace().调试程序过程中我们可以单步执行,逐步检查程序的输入输出,从而判断程序错误,当然我们也可以抛弃gdb,自己实现一个"外挂程序",拦截主程序中我们感兴趣的东西,比如ssh或ftp登录密码.

先看一个简单的程序:

#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获得子进程的进程号。

运行结果:



其中第一行是我们在键盘上的输入,一行为被跟踪程序的输出,还有一行为“外挂”程序的输出。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: