说一说ptrace不可忽略的技术细节 - 游望之

admin 2021年12月31日14:41:49评论264 views字数 4903阅读16分20秒阅读模式

说一说ptrace不可忽略的技术细节

ptrace的API使用起来简单明了,好像上手很容易,但实际开发使用中存在很多细节,容易产生各种各样的错误。本文内容来源于本人使用ptrace开发时遇到问题及解决之后的经验总结。

细节一:多线程处理

ptrace操作的最小单位是线程,这是很多人都忽略的一个点。可以肯定的讲,成熟商用的软件几乎没有一个不是多线程运行的。在LINUX中,线程其实就是进程,进程号就是当前进程的主线程号。
比如使用top -Hp 92964命令查看tomcat进程,得到如下结果

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                        
 92964 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 java                                           
 92965 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.57 java                                           
 92966 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.04 GC Thread#0                                    
 92967 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 CMS Thread#0                                   
 92968 root      20   0 6070636 198540  37784 S  0.0  2.4   0:02.61 CMS Main Thread                                
 92969 root      20   0 6070636 198540  37784 S  0.0  2.4   0:07.37 VM Thread                                      
 92970 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 Reference Handl                                
 92971 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 Finalizer                                      
 92972 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 Signal Dispatch                                
 92973 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 Service Thread                                 
 92974 root      20   0 6070636 198540  37784 S  0.0  2.4   0:02.60 C2 CompilerThre                                
 92975 root      20   0 6070636 198540  37784 S  0.0  2.4   0:01.30 C1 CompilerThre                                
 92976 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 Sweeper thread                                 
 92977 root      20   0 6070636 198540  37784 S  0.0  2.4   0:58.99 VM Periodic Tas                                
 92978 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.08 Common-Cleaner                                 
 92979 root      20   0 6070636 198540  37784 S  0.0  2.4   0:03.75 AsyncFileHandle                                
 92982 root      20   0 6070636 198540  37784 S  0.0  2.4   0:03.54 NioBlockingSele                                
 92985 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.04 GC Thread#1                                    
 92986 root      20   0 6070636 198540  37784 S  0.0  2.4   0:02.50 ContainerBackgr                                
 92987 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.04 http-nio-8080-e                                
 92988 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92989 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92990 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.01 http-nio-8080-e                                
 92991 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92992 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92993 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92994 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92995 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92996 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.00 http-nio-8080-e                                
 92997 root      20   0 6070636 198540  37784 S  0.0  2.4   0:03.50 http-nio-8080-C                                
 92998 root      20   0 6070636 198540  37784 S  0.0  2.4   0:03.31 http-nio-8080-C                                
 92999 root      20   0 6070636 198540  37784 S  0.0  2.4   0:00.01 http-nio-8080-A                                
 93000 root      20   0 6070636 198540  37784 S  0.0  2.4   0:04.03 http-nio-8080-A

清楚的列出各个线程的ID、CPU占用、内存占用、运行状态(有R、S、D、T、Z、X几种,这里是S)。
所以在做多线程进程进行ptrace操作时,在attach时就要把每一个线程都要attach一遍。这样才能确保整个进程都处于完全停止状态,如果只对进程id(即主线程)进行attach,那其余的线程还是会继续运行。
如何获取进程有哪些线程?还以进程92964为例:

#ls  /proc/92964/task/
92964  92967  92970  92973  92976  92979  92986  92989  92992  92995  92998
92965  92968  92971  92974  92977  92982  92987  92990  92993  92996  92999
92966  92969  92972  92975  92978  92985  92988  92991  92994  92997  93000
#ls  /proc/92964/task/92967/
attr        cmdline  exe      limits     mounts     oom_score      projid_map  setgroups     statm
auxv        comm     fd       loginuid   net        oom_score_adj  root        smaps         status
cgroup      cpuset   fdinfo   maps       ns         pagemap        sched       smaps_rollup  syscall
children    cwd      gid_map  mem        numa_maps  patch_state    schedstat   stack         uid_map
clear_refs  environ  io       mountinfo  oom_adj    personality    sessionid   stat          wchan

细节二:ptrace_attach

ptrace的调用者(tracer)与被调试线程(tracee)是通过信号进行交互的。tracee在被attach之后,会进入STOP状态同时会给tracer发送SIGSTOP信号。很多开发者都会去在attach之后,只是写一个循环调用waitpid等待SIGSTOP,代码如下:

int attach(int tid)
{
    if(ptrace(PTRACE_ATTACH, tid, 0, 0) == -1)
    {
        printf("attach pid %d fail\n", tid);
        return -1;
    }

    int status = 0;
    waitpid(tid, &status, __WALL);

    int sig = WSTOPSIG(status);
    while(!WIFSTOPPED(status) || sig != SIGSTOP)
    {
        waitpid(tid, &status, __WALL);
    }

    return 0;
}

这是一个错误的做法,tracee在发送SIGSTOP之前还可能发送其他信号,上面的代码直接把这个信号丢弃了。正确做法是,如果不是SIGSTOP,应该把信号发送回去。

int Debugger::attach(int tid)
{
    if(ptrace(PTRACE_ATTACH, tid, 0, 0) == -1)
    {
        printf("attach pid %d fail\n", tid);
        return -1;
    }

    int status = 0;
    waitpid(tid, &status, __WALL);

    int sig = WSTOPSIG(status);
    while(!WIFSTOPPED(status) || sig != SIGSTOP)
    {
        //如果在SIGSTOP之前收到其他信号,那么在接收之后要发送回去
        tkill(tid, sig);
        waitpid(tid, &status, __WALL);
    }

    return 0;
}

细节三:读写tracee的/proc/pid/mem

我们都知道在对线程自己的寄存器、栈读写或代码段读写时,线程要处于STOP状态。什么事件会使用线程处于STOP状态?如ptrace-attach,ptrace-singlestep,tracee在触发SIGSEGV、SIGTRAP、SIGILL等场景,这里不一一列举。
/proc/pid/mem映射的是整个进程的内存,可以通过操作它来实现读写修改代码段、栈空间、数据段等等功能,它要求主线程处理STOP状态。
从LINUX内核3.2开始提供process_vm_readv和process_vm_writev系统调用,用来实现这一功能,是一种新的IPC机制。

细节四:断点

使用ptrace实现断点的原理就是将某一代码段地址的指令修改为TRAP指令,比如x86的int3。各线程的寄存器、栈帧是独有的,但代码是共享的。
也就是说,通过线程A去下的断点,线程B、线程C都可能会执行到,如果线程B或线程C没有被ptrace attach,执行到此TRAP指令时产生的SIGTRAP就不会被捕捉到,导致进程异常退出。
因此,对于多线程进程的断点实现,要及时跟踪进程中是否有新线程产生并ptrace-attach。

细节五:代码注入

使用ptrace可以修改tracee进程的任意寄存器,如PC、SP,也几乎可以修改任意内存地址,包括映射为只读的代码段,注入代码并执行完全不是难事。
通常注入代码有shellcode和so两种形式,在操作上大同小异,一般都会借用libc中如mmap、libc_dlopen_mode等函数完成。那么如何使用ptrace进行函数调用?一般流程如下:

1.保存tracee当前PC上下文(即所有寄存器)
2.构造参数,修改tracee的PC指针为自定义代码
3.在自定义代码末尾放置TRAP指令
4.ptrace-cont
5.tracer捕获SIGTRAP
6.恢复tracee之前PC上下文
7.ptrace-dettach,结束

那么步骤2、步骤3的实现方式可以有很多种,有用__mmap/malloc申请可执行内存的,有修改代码段放置TRAP指令、执行完再恢复的,其中有一个方法我觉得最简洁,不需要调用函数去申请内存、也不需要修改已有代码段。

分配空间使用栈,而放置TRAP指令替换为把函数返回地址替换为0

比如想要调用system("echo 123"),以x86_64为例,有如下步骤

1.修改sp寄存器,抬高8字节,用来存放字串echo 123\0
2.修改pc寄存器为system地址
3.修改rdi为第1步的栈顶地址
4.将0压栈
5.ptrace-cont

这样在执行完system之后,RET指令使RIP变为0会触发SIGSEGV,此时tracer捕获后恢复PC上下文即可。

细节六:glibc的栈对齐

x86_64在调用libc的函数时,一定要保证SP指针是16字节对齐的,否则在运行到movaps [rbp-0x470],xmm0这种指令时会SIGSEGV。这个问题也经常出现在汇编混合调用C的场景中。

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日14:41:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   说一说ptrace不可忽略的技术细节 - 游望之http://cn-sec.com/archives/709324.html

发表评论

匿名网友 填写信息