C - Process Trace On Linux

Process Trace

操作系统对每个进程内存保护,一个进程是不能读写另一个进程的,试想一下如果进程间能够随意读写将会造成多大的安全问题,但是在某些情形下我们希望进程的内容可以被别的进程读取,例如:debugger应该能够读取/修改被调试进程的内容,当然读写别的进程内容并非任意进程间都允许的,那样的话就造成安全问题了

读写别的进程的模式

如果一个进程有权限随意读写任意进程的内容的话会造成安全问题,但是如果一个进程声明说,某某进程你可以读写我的内容,被授权的进程的读写就没有任何安全问题了,这里遵照的原则是:被读写进程拥有让受信任进程读写自身的授权权利

预备知识:系统调用

操作系统内核提供给外部使用的API又叫作系统调用,主要是调用操作系统底层的功能,例如:访问文件系统,操纵进程等功能,进程通过系统调用接口将任务传递给内核,为了识别不同的系统调用,每个系统调用都有唯一的编号。

系统调用过程有两个特殊状态称之为entry和exit,entry就是在系统调用开始时的状态,exit就是系统调用返回是的状态,在entry时寄存器%eax中存放的是系统调用号,在exit时%eax存放的是系统调用的返回值(i386机构是如此的)。

系统调用的实质过程,当进程调用了系统函数后,系统调用号写入%eax,系统调用的参数写入寄存器%ebx,%ecx,%edx,%esi,%edi,并触发软中断0x80(在linux中该中断将会导致进入内核模式,即内核开始执行系统调用函数)。

一个系统调用往往会在导致别的系统调用,其实就像自定义函数一样,系统调用也可能是嵌套的。

系统调用ptrace

读写别的进程在linux系统下是通过系统调用ptrace实现的,下面看一个例子:

    /* 代码摘自(有略微改动): http://www.linuxjournal.com/article/6100 */

    #include <sys/ptrace.h>

    #include <sys/types.h>

    #include <sys/wait.h>

    #include <unistd.h> /* system call number */

    #include <sys/reg.h>   /* For constants ORIG_EAX etc */

    int main()

    {   pid_t child;

        long orig_eax;

        child = fork();

        if(child == 0) {

            /* child process come here */

            ptrace(PTRACE_TRACEME, 0, NULL, NULL);  /* 子进程声明允许父进程trace自己 */

            execl("/bin/ls", "ls", NULL);

        }

        else {

            /* parent process come here */

            wait(NULL);  /* wait阻塞后面代码的执行,直到被唤醒 */

            /* 父进程中可以访问子进程的寄存器内容 */    

            orig_eax = ptrace(PTRACE_PEEKUSER,

                              child, 4 * ORIG_EAX,

                              NULL);

            printf("The child made a "

                   "system call %ld\n", orig_eax);

            ptrace(PTRACE_CONT, child, NULL, NULL);

        }

        return 0;

    }

下面通过执行时间的先后顺序来了解例子的运行过程:

1. 父进程处于wait状态等待被唤醒

2. 子进程调用`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`使自己允许被trace

3. 子进程进行系统调用`execl("/bin/ls", "ls", NULL);`请求

4. 内核发现子进程是被trace的,所以将子进程的控制权移交给追踪它的进程(即父进程),并且机器中存放的与子进程相关的内容信息并未变化(例:寄存器内容)

5. 父进程被唤醒,父进程可以读取寄存器%eax(该寄存器内容仍然是刚刚子进程写入的,即execl对应的系统调用号)并输出其内容

6. 父进程调用`ptrace(PTRACE_CONT, child, NULL, NULL);`,以告知内核子进程请求的系统调用继续执行(这其实相当于父进程告知内核自己放弃了对子进程的监控)

7. 内核执行子进程的execl系统调用以及相关的系统调用,最终ls当前目录

读取子进程的内容

    /* 代码摘自(略微改动): http://www.linuxjournal.com/article/6100?page=0,1 */

    #include <sys/ptrace.h>

    #include <sys/types.h>

    #include <sys/wait.h>

    #include <unistd.h>

    #include <sys/user.h>

    #include <sys/reg.h>

    #include <sys/syscall.h>   /* For SYS_write etc */

    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表示子进程状态 */

              wait(&status);

              /* 如果子进程exited,则break循环,即父进程一直循环等待直到子进程退出 */

              if(WIFEXITED(status))

                  break;

              orig_eax = ptrace(PTRACE_PEEKUSER,

                         child, 4 * ORIG_EAX, NULL);

              /* 注意: 上面子进程的execl("bin/ls", "ls", NULL);引发的第一个系统调用是SYS_execve,

                       之后还会引发一系列别的系统调用,因为我们这里只关心系统调用SYS_write,所以只对子进程

                       的SYS_write调用进行处理

                       ls命令显示结果时每输出一行内容,就是一次对stdout的写操作(即系统调用SYS_write)

              */

              if(orig_eax == SYS_write) {

                 if(insyscall == 0) {

                    /* SYS_write entry 

                        EBX, ECX, EDX存放了子进程的信息

                    */

                    insyscall = 1;

                    /* 取出子进程 USER area的内容 */

                    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);

                    printf("Write called with "

                           "%ld, %ld, %ld\n",

                           params[0], params[1],

                           params[2]);

                    /* 或者可以使用sys/user.h中定义的user_regs_struct获取

                     ptrace(PTRACE_GETREGS, child,

                            NULL, &regs);

                     printf("Write called with "

                            "%ld, %ld, %ld\n",

                            regs.ebx, regs.ecx,

                            regs.edx);

                    */

                 }

                 else { 

                    /* SYS_write exit

                       系统调用将返回值写入EAX中

                     */

                    eax = ptrace(PTRACE_PEEKUSER,

                                 child, 4 * EAX, NULL);

                        printf("Write returned "

                               "with %ld\n", eax);

                        insyscall = 0;

                 }

              }/* end of if SYS_write */

              /* 告知内核子进程继续运行直到下一个系统调用entry或exit */

              ptrace(PTRACE_SYSCALL,

                     child, NULL, NULL);

           }/* end of while */

        }

        return 0;

    }

该例子的执行过程:

1. 父进程进入循环等待直到子进程退出

2. 子进程调用`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`使得自己被trace

3. 子进程进行execl系统调用请求进而引发一系列系统调用请求

4. 父进程被子进程的系统调用请求唤醒,每次被唤醒父进程都调用`ptrace(PTRACE_SYSCALL, child, NULL, NULL);`,以告知内核子进程继续运行直到下一个系统调用entry或exit

5. 子进程的一系列系统调用请求被执行,直到子进程发起SYS_write请求

6. 父进程中对SYS_write请求做了额外的处理,即分别提取子进程刚刚进入系统调用以及系统调用返回时的信息

读写子进程的内容

    /* 代码摘自(略有改动): http://www.linuxjournal.com/article/6100?page=0,2 */

    #include <sys/ptrace.h>

    #include <sys/types.h>

    #include <sys/wait.h>

    #include <unistd.h>

    #include <sys/reg.h>

    #include <sys/user.h>

    #include <sys/syscall.h>

    const int long_size = sizeof(long);

    /* 反转字符串函数 */

    void reverse(char *str)

    {   int i, j;

        char temp;

        for(i = 0, j = strlen(str) - 2;

            i <= j; ++i, --j) {

            temp = str[i];

            str[i] = str[j];

            str[j] = temp;

        }

    }

    /* 读取子进程内容 */

    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 *)calloc(params[2]+1, sizeof(char));

                   /* 取出子进程写入内容复制到str */

                   getdata(child, params[1], str,

                           params[2]);

                   reverse(str);

                   /* 将str中的内容写入子进程的缓存中,也就是替换子进程自己写入的内容 */

                   putdata(child, params[1], str,

                           params[2]);

                }

                else {

                   toggle = 0;

                }

             }

          ptrace(PTRACE_SYSCALL, child, NULL, NULL);

          }

       }

       return 0;

    }

该例子的运行过程:

1. 子进程允许被trace以及父进程等待被唤醒和之前示例一样

2. 该例的不同之处在于,父进程对子进程SYS_write请求处理的逻辑

3. 子进程发生SYS_write请求时将内容写到子进程的缓存中

4. 父进程被唤醒,读取并修改子进程的缓存内容,再让子进程继续运行

ptrace接口

    long ptrace(enum_ptrace_request request,

                pid_t pid,

                void *addr,

                void *data);

1. 子进程允许被追踪,`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`

2. 父进程访问子进程USER area内容,读取子进程系统调用号`orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL);`,读取子进程系统调用返回值`ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL);`,读取子进程缓存地址`ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL);`,读取子进程缓存内容大小`ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL);`

3. 父进程访问子进程内存,读取子进程缓存`ptrace(PTRACE_PEEKDATA, child, addr + i * 4, data.val);`,写入子进程缓存`ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val);`

4. 父进程将控制权交回给子进程直到子进程下次系统调用entry或exit,`ptrace(PTRACE_SYSCALL, child, NULL, NULL);`

5. 父进程放弃对子进程的控制权,`ptrace(PTRACE_CONT, child, NULL, NULL);`
Xiao Wenbin
Xiao Wenbin
Natural Language Processing Engineer

My research interests include machine learning, information retrieval and natural language processing.

Related