首页
社区
课程
招聘
[原创] Linux ptrace详细分析系列(一)
发表于: 2021-2-2 11:35 25433

[原创] Linux ptrace详细分析系列(一)

2021-2-2 11:35
25433

(该系列将深入分析Linux ptrace的方方面面,争取一次把它搞定,对后续调试或开发都有益处。不定期更新。)

备注:文章中使用的Linux内核源码版本为Linux 5.9,使用的Linux版本为Linux ubuntu 5.4.0-65-generic

ptrace系统调用提供了一个进程(tracer)可以控制另一个进程(tracee)运行的方法,并且tracer可以监控和修改tracee的内存和寄存器,主要用作实现断点调试和系统调用追踪。

tracee首先要被attach到tracer上,这里的attach以线程为对象,在多线程场景(这里的多线程场景指的使用clone CLONE_THREAD flag创建的线程组)下,每个线程可以分别被attach到tracer上。ptrace的命令总是以下面的调用格式发送到指定的tracee上:

一个进程可以通过调用fork()函数来初始化一个跟踪,并让生成的子进程执行PTRACE_TRACEME,然后执行execve(一般情况下)来启动跟踪。进程也可以使用PTRACE_ATTACHPTRACE_SEIZE进行跟踪。

当处于被跟踪状态时,tracee每收到一个信号就会stop,即使是某些时候信号是被忽略的。tracer将在下一次调用waitpid或与wait相关的系统调用之一)时收到通知。该调用会返回一个状态值,包含tracee停止的原因。tracee发生stop时,tracer可以使用各种ptrace的request来检查和修改tracee。然后,tracer使tracee继续运行,选择性地忽略所传递的信号(甚至传递一个与原来不同的信号)。

tracer结束跟踪后,发送PTRACE_DETACH信号释放traceetracee可以在常规状态下继续运行。

ptrace的原型如下:

其中request参数表明执行的行为(后续将重点介绍), pid参数标识目标进程,addr参数表明执行peekpoke操作的地址,data参数则对于poke操作,指明存放数据的地址,对于peek操作,指明获取数据的地址。

返回值,成功执行时,PTRACE_PEEK请求返回所请求的数据,其他情况时返回0,失败则返回-1。

ptrace的内核实现在kernel/ptrace.c文件中,内核接口是SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)。其代码如下,整体逻辑简单,需要注意的是对PTRACE_TRACEMEPTRACE_ATTACH进行了特殊处理(对于该函数的参数后续将进行深入解析)。

系统调用都改为了SYSCALL_DEFINE的方式。如何获得上面的定义的呢?这里需要穿插一下SYSCALL_DEFINE的定义(syscall.h):

宏定义进行展开:

__SYSCALL_DEFINEx中的x表示系统调用的参数个数,且sys_ptrace的宏定义如下:

所以对应的__SYSCALL_DEFINEx应该是SYSCALL_DEFINE4,这与上面的定义SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)一致。

仔细观察上面的代码可以发现,函数定义其实在最后一行,结尾没有分号,然后再加上花括号即形成完整的函数定义。前面的几句代码并不是函数的实现(详细的分析可以跟踪源码,出于篇幅原因此处不放出每个宏定义的跟踪)。

定义的转换过程:

而对__MAP宏和__SC_DECL宏的定义如下:

按照如上定义继续进行展开

最后调用asmlinkage long sys_ptrace(long request, long pid, unsigned long addr, unsigned long data);

为什么要将系统调用定义成宏?主要是因为2个内核漏洞CVE-2009-0029,CVE-2010-3301,Linux 2.6.28及以前版本的内核中,将系统调用中32位参数传入64位的寄存器时无法作符号扩展,可能导致系统崩溃或提权漏洞。

内核开发者通过将系统调用的所有输入参数都先转化成long类型(64位),再强制转化到相应的类型来规避这个漏洞。

首先通过一个简单的例子来熟悉一下ptrace的使用:

运行结果如下:

ptrace_demo

打印出系统调用号,并等待用户输入。查看/usr/include/x86_64-linux-gnu/asm/unistd_64.h文件(64位系统)查看59对应的系统调用:

system_call_execve

59号恰好为execve函数调用。对上面的过程进行简单总结:

父进程通过调用fork()来创建子进程,在子进程中,执行execl()之前,先运行ptrace()request参数设置为PTRACE_TRACEME来告诉kernel当前进程正在被trace。当有信号量传递到该进程,进程会stop,提醒父进程在wait()调用处继续执行。然后调用execl(),执行成功后,新程序运行前,SIGTRAP信号量被发送到该进程,子进程停止,父进程在wait()调用处收到通知,获取子进程的控制权,查看子进程内存和寄存器相关信息。

当发生系统调用时,kernel保存了rax寄存器的原始内容,其中存放的是系统调用号,我们可以使用request参数为PTRACE_PEEKUSERptrace来从子进程的USER段读取出该值。

系统调用检查结束后,子进程通过调用request参数为PTRACE_CONTptrace函数继续执行。

执行结果:

system_call_ls

在上面的程序中,跟踪的是wirte的系统调用,ls命令总计进行了三次write的调用。request参数为PTEACE_SYSCALL时的ptrace使kernel在进行系统调用进入或退出时stop子进程,这等价于执行PTRACE_CONT并在下一次系统调用进入或退出时stop。

wait系统调用中的status变量用于检查子进程是否已退出,这是用来检查子进程是否被ptrace停掉或是否退出的典型方法。而宏WIFEXITED则表示了子进程是否正常结束(例如通过调用exit或者从main返回等),正常结束时返回true

前面有介绍PTRACE_GETREGS参数,使用它来获取寄存器的值相比前面一种方法要简单很多:

执行结果:

system_call_getregs

整体输出与前面的代码无所差别,但在代码开发上使用了PTRACE_GETREGS来获取子进程的寄存器的值,简洁了很多。

[1]. https://www.linuxjournal.com/article/6100
[2]. https://blog.csdn.net/u012417380/article/details/60468697
[3]. Linux ptrace man page

 
 
 
ptrace(PTRACE_foom, pid, ...)   // pid为linux中对应的线程ID
ptrace(PTRACE_foom, pid, ...)   // pid为linux中对应的线程ID
 
 
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
 
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,unsigned long, data)
{
    struct task_struct *child;
    long ret;
 
    if (request == PTRACE_TRACEME) {
        ret = ptrace_traceme();
        if (!ret)
            arch_ptrace_attach(current);
        goto out;
    }
 
    child = find_get_task_by_vpid(pid);
    if (!child) {
        ret = -ESRCH;
        goto out;
    }
 
    if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
        ret = ptrace_attach(child, request, addr, data);
        /*
        * Some architectures need to do book-keeping after
        * a ptrace attach.
        */
        if (!ret)
            arch_ptrace_attach(child);
        goto out_put_task_struct;
    }
 
    ret = ptrace_check_attach(child, request == PTRACE_KILL ||
                request == PTRACE_INTERRUPT);
    if (ret < 0)
        goto out_put_task_struct;
 
    ret = arch_ptrace(child, request, addr, data);
    if (ret || request != PTRACE_DETACH)
        ptrace_unfreeze_traced(child);
 
out_put_task_struct:
    put_task_struct(child);
out:
    return ret;
}
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,unsigned long, data)
{
    struct task_struct *child;
    long ret;
 
    if (request == PTRACE_TRACEME) {
        ret = ptrace_traceme();
        if (!ret)
            arch_ptrace_attach(current);
        goto out;
    }
 
    child = find_get_task_by_vpid(pid);
    if (!child) {
        ret = -ESRCH;
        goto out;
    }
 
    if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
        ret = ptrace_attach(child, request, addr, data);
        /*
        * Some architectures need to do book-keeping after
        * a ptrace attach.
        */
        if (!ret)
            arch_ptrace_attach(child);
        goto out_put_task_struct;
    }
 
    ret = ptrace_check_attach(child, request == PTRACE_KILL ||
                request == PTRACE_INTERRUPT);
    if (ret < 0)
        goto out_put_task_struct;
 
    ret = arch_ptrace(child, request, addr, data);
    if (ret || request != PTRACE_DETACH)
        ptrace_unfreeze_traced(child);
 
out_put_task_struct:
    put_task_struct(child);
out:
    return ret;
}
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...)                \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)            \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
 
/*
 * The asmlinkage stub is aliased to a function named __se_sys_*() which
 * sign-extends 32-bit ints to longs whenever needed. The actual work is
 * done within __do_sys_*().
 */
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)                    \
    __diag_push();                            \
    __diag_ignore(GCC, 8, "-Wattribute-alias",            \
              "Type aliasing is used to sanitize syscall arguments");\
    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))    \
        __attribute__((alias(__stringify(__se_sys##name))));    \
    ALLOW_ERROR_INJECTION(sys##name, ERRNO);            \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
    asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));    \
    asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))    \
    {                                \
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
        __MAP(x,__SC_TEST,__VA_ARGS__);                \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));    \
        return ret;                        \
    }                                \
    __diag_pop();                            \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */
#define SYSCALL_DEFINEx(x, sname, ...)                \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)            \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
 
/*
 * The asmlinkage stub is aliased to a function named __se_sys_*() which
 * sign-extends 32-bit ints to longs whenever needed. The actual work is
 * done within __do_sys_*().
 */
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)                    \
    __diag_push();                            \
    __diag_ignore(GCC, 8, "-Wattribute-alias",            \
              "Type aliasing is used to sanitize syscall arguments");\
    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))    \
        __attribute__((alias(__stringify(__se_sys##name))));    \
    ALLOW_ERROR_INJECTION(sys##name, ERRNO);            \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
    asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));    \
    asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))    \
    {                                \
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
        __MAP(x,__SC_TEST,__VA_ARGS__);                \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));    \
        return ret;                        \
    }                                \
    __diag_pop();                            \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */
/* kernel/ptrace.c */
asmlinkage long sys_ptrace(long request, long pid, unsigned long addr,
               unsigned long data);
/* kernel/ptrace.c */
asmlinkage long sys_ptrace(long request, long pid, unsigned long addr,
               unsigned long data);
 
 
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)
--> SYSCALL_DEFINEx(4, _ptrace, __VA_ARGS__) 
    -->  __SYSCALL_DEFINEx(4, __ptrace, __VA_ARGS__)
      #define __SYSCALL_DEFINEx(x, name, ...) \
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
      --> asmlinkage long sys_ptrace(__MAP(4,__SC_DECL,__VA_ARGS__))
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)
--> SYSCALL_DEFINEx(4, _ptrace, __VA_ARGS__) 
    -->  __SYSCALL_DEFINEx(4, __ptrace, __VA_ARGS__)
      #define __SYSCALL_DEFINEx(x, name, ...) \
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
      --> asmlinkage long sys_ptrace(__MAP(4,__SC_DECL,__VA_ARGS__))
/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
 
#define __SC_DECL(t, a)    t a
/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
 
#define __SC_DECL(t, a)    t a
__MAP(4,__SC_DECL, long request, long pid, unsigned long addr,
               unsigned long data)
-->  __MAP4(__SC_DECL, long, request, long, pid, unsigned long, addr,
               unsigned long, data)
-->  __SC_DECL(long, request), __MAP3(__SC_DECL, __VA_ARGS__)
  __MAP3(__SC_DECL, long, pid, unsigned long, addr, unsigned long, data)
  --> __SC_DECL(long, pid), __MAP2(__SC_DECL, unsigned long, addr, unsigned long, data)       
          -->__SC_DECL(unsigned long, addr), __MAP1(__SC_DECL, __VA_ARGS__)
              unsigned long addr, __SC_DECL(unsigned long, data)
              --> unsigned long data
  long pid, __SC_DECL(unsigned long, addr), __MAP1(__SC_DECL, __VA_ARGS__)
  --> long pid, unsigned long addr, unsigned long data
--long request, __SC_DECL(long, pid), __MAP2(__SC_DECL, __VA_ARGS__)
--long request, long pid, unsigned long addr, unsigned long data
__MAP(4,__SC_DECL, long request, long pid, unsigned long addr,
               unsigned long data)
-->  __MAP4(__SC_DECL, long, request, long, pid, unsigned long, addr,
               unsigned long, data)
-->  __SC_DECL(long, request), __MAP3(__SC_DECL, __VA_ARGS__)
  __MAP3(__SC_DECL, long, pid, unsigned long, addr, unsigned long, data)
  --> __SC_DECL(long, pid), __MAP2(__SC_DECL, unsigned long, addr, unsigned long, data)       
          -->__SC_DECL(unsigned long, addr), __MAP1(__SC_DECL, __VA_ARGS__)
              unsigned long addr, __SC_DECL(unsigned long, data)
              --> unsigned long data
  long pid, __SC_DECL(unsigned long, addr), __MAP1(__SC_DECL, __VA_ARGS__)
  --> long pid, unsigned long addr, unsigned long data
--long request, __SC_DECL(long, pid), __MAP2(__SC_DECL, __VA_ARGS__)
--long request, long pid, unsigned long addr, unsigned long data
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-3-5 14:38 被有毒编辑 ,原因:
收藏
免费 4
支持
分享
最新回复 (14)
雪    币: 10308
活跃值: (4510)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
有雪友看的话,有本书叫《Linux 二进制分析》 可以参考,个人感觉一些东西还是挺详细的
2021-2-2 17:33
0
雪    币: 14484
活跃值: (17483)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2021-2-2 21:13
0
雪    币: 219
活跃值: (80)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
我没明白最后一个例子,没执行printf("Write returned with %ld\n", rax)  ,反倒是两次write called?
2021-2-2 23:25
0
雪    币: 15170
活跃值: (16832)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
5
byrjx 我没明白最后一个例子,没执行printf("Write returned with %ld\n", rax) ,反倒是两次write called?
你可以用strace跟踪一下看看,就能发现其中差别了
2021-2-3 08:57
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
byrjx 我没明白最后一个例子,没执行printf("Write returned with %ld\n", rax) ,反倒是两次write called?
老哥,你搞明白为啥了吗,我用strace调试了一下,没看出差别
2021-2-5 09:32
0
雪    币: 60
活跃值: (294)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
7
單純 code 寫錯了吧,x64 write syscall 要印參數應該是要印 rdi rsi rdx 之類,印 rbx rcx rdx 不知道有什麼用意,另外 insyscall == 1 所以不會進入印出 write return 那行吧
2021-2-5 15:43
0
雪    币: 15170
活跃值: (16832)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
8
返無歸一 單純 code 寫錯了吧,x64 write syscall 要印參數應該是要印 rdi rsi rdx 之類,印 rbx rcx rdx 不知道有什麼用意,另外 insyscall == 1 所以不 ...
是的  64位里打印这寄存器没有意义,只是单纯实现下功能
2021-2-5 16:16
0
雪    币: 15170
活跃值: (16832)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
9
waxsq 老哥,你搞明白为啥了吗,我用strace调试了一下,没看出差别
7楼的师傅是正解
2021-2-5 16:19
0
雪    币: 1336
活跃值: (43)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10

楼主请教一下,我的Ubuntu 18.04上,上面代码输出是2次 wrtie 系统调用的返回,但是我用strace看ls它只有一次write,实在搞不懂多出来的1次wrtie是从哪里冒出来的

最后于 2021-8-15 12:35 被zingphoy编辑 ,原因:
2021-8-15 02:04
0
雪    币: 1336
活跃值: (43)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
zingphoy 楼主请教一下,我的Ubuntu 18.04上,上面代码输出是4次 wrtie 系统调用,但是我用strace看ls它只有一次write,实在搞不懂多出来的3次wrtie是从哪里冒出来的[em_77]
附:我跑上面的代码(改了 insyscall==1 的bug了),以及手工 strace ls,都是在同一个文件夹下跑的
2021-8-15 02:05
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
最后的代码,36 行还有个错误吧,8*rax 应该是 8*RAX 吧
2021-9-15 15:23
0
雪    币: 3736
活跃值: (3867)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
13
感谢分享!
2021-9-15 15:52
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
byrjx 我没明白最后一个例子,没执行printf("Write returned with %ld\n", rax) ,反倒是两次write called?
32行 insyscall == 1; 应该改成 insyscall = 1; 实际只有一次write系统调用
另外37行 rax = ptrace(PTRACE_PEEKUSER, child, 8*rax, NULL); 获取返回值有问题,
可以换成                    ptrace(PTRACE_GETREGS, child, NULL, &regs);
                    printf("Write returned with %ld\n", regs.rax);
2022-5-15 18:53
0
雪    币: 0
活跃值: (111)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
”系统调用检查结束后,《《《子进程》》》》通过调用request参数为PTRACE_CONT的ptrace函数继续执行。"  应该是主进程吧 
2022-11-20 19:21
0
游客
登录 | 注册 方可回帖
返回
//