Linux ‘PTRACE_TRACEME’提权漏洞(CVE-2019-13272)分析

 

漏洞背景

ptrace是linux的进程调试syscall,我们可以使用它在一个进程(tracer)中追踪调试另一个进程(tracee)。

PTRACE_TRACEME被tracee使用,作用是让自己的父进程成为自己的tracer。例如我们执行fork之后,在child中执行PTRACE_TRACEME,这样parent就成为了tracer,child成为tracee。

 

漏洞成因

我们从补丁看起:

这个补丁修复了两个bug,我们关心的是__task_cred(new_parent)current_cred()的变动。

补丁之前的ptrace_link()是这样处理的:

这样,我们的child就可以获取new_parent的credentials。

如果一个child执行了PTRACE_TRACEME,最终会调用ptrace_link()。它的parent会成为tracer,它本身加入parent的ptraced列表,且parent的credentials会被child获取,存储于child->ptracer_cred

一个普通权限的用户可以利用这一点创建一个privileged的ptrace关系,并用其完成提权。大致思路是,当ptracer是privileged进程时,它的tracee(也就是child),使用execve执行一个suid程序,会是正常的suid模式,也就是获得suid程序owner的权限,其EUID改变。

execve()的作用是用新的程序替换掉当前进程的image,不涉及进程创建,进程属性也大多保持不变。
当执行的程序具有SUID时,进程的euid会变为程序文件owner的uid。但有例外:

我们的使用情景符合第三个情况,于是execve执行的suid程序是不会set-UID的,那么tracee执行suid程序到底有没有set-UID,就靠ptrace机制自己决定。
然后ptrace的机制就是,如果tracer是priviliged,那么tracee就可以set-UID。

我们回想一下PTRACE_TRACEME的过程,当parent没有trace child的时候,它可能是privileged进程,这时我们的child使用PTRACE_TRACEME,强制parent成为自己的tracer,那么child在ptrace看来就是由一个priviliged进程所trace的,它执行suid程序也就可以实现set-UID了。

再解释个可能的疑问。既然execve执行suid程序正常情况下可以获得set-UID,那我们直接执行suid程序然后replace为我们自己的程序可以么?听起来好像可以,但execve执行成功之后永远不会return到你调用它的函数里,因为原进程已经被replace了,你无法继续往它里面注入自己的程序,换言之,suid程序的执行不受你的控制。但ptrace机制保证了你可以对tracee进行完全控制,你完全可以在tracee做了set-UID之后,replace它的程序为自己的程序。

然后还有一点,你也不能替换掉tracer进程的程序,因为当它执行suid程序时,是无法被你trace的,你也就无法借用上文所述的方法去注入你的程序。这个叫做dumpable:

如何使用ptrace操作tracee,使其程序被替换为我们想要的程序,这本身也是比较有技术含量的。

如图,作者先wait目标tracee的pid让其正常exit,然后从fd参数执行想要的程序(execveat(),使用syscall实现的)替换掉原程序,并detach,等待它的exit。

执行syscall的时候,它也改变了程序的argv*arg0,然后该程序(也就是poc本身)的main会判断argv来执行不同stage。

注:在linux的文档中,credentials是指process identifiers,它们包括了PID信息,以及user/group identifiers等等,后者也就是UID/GID,具体分如下情况(我们通常只关心前三个):

其中saved set-user-ID用于保存程序执行前的effective UID,例如一个普通权限的UID 1000。这个机制对具有set-user-ID的程序起作用,它们可以drop掉自己的privileged身份(切换到saved set-user-ID),也可以重新claim权限(切到real user-ID)。

 

漏洞利用

漏洞作者 jannh@google.com 的利用思路相当巧妙,涉及两个ptrace和三个stage。

具体解释如下:

  1. task A: fork()一个child, task B
  2. task B: fork()一个child, task C
  3. task B: execve(/some/special/suid/binary),这里是pkexec

    pkexec是具有set-UID的binary程序,它执行时是其owner也就是root的身份,但我们的A无法attach到这个privileged进程,于是我们接着就需要drop privilege,也就是前文所述的原理,这样才能被A PTRACE_ATTACH并控制。
    这里pkexec指定了--user为当前用户,其execve的helper也就最终变成了dumpable的进程,可以被A所PTRACE_ATTACH
  4. task C: 使用PTRACE_TRACEME (建立 privileged ptrace relationship),结合3来看,我们的C在pkexec运行时,可以得到一个root身份的tracer:
  5. task C: execve(/usr/bin/passwd),此时C的进程就是运行passwd了,这是以root身份运行的set-UID程序。
    由于PTRACE_TRACEME的使用,C在execve()这里收到SIGTRAP,挂起以供其tracer追踪。

  1. task B: drop privileges (setresuid(getuid(), getuid(), getuid())),这里poc是使用pkexec--user选项完成了这些工作。
  2. task B: become dumpable again (e.g. execve(/some/other/binary)),B进程的程序替换为helper,然后重新成为dumpable的程序(因为SUID程序非dumpable)。
  3. task A: 此时A可以PTRACE_ATTACH到B进程,成为其tracer。
  4. task A: use ptrace to take control of task B
  5. task B: use ptrace to take control of task C

stage 1是A进程起始,直到attach到B进程之后,在B中执行stage 2。

stage 2中,B使用waitpid()去获取child pid (C),并在C执行stage 3,也就是spawn_shell(),通过setresgid()setresuid(),把自己的uid和gid全部设为0,并执行bash,弹出root shell。

C在执行stage 3的时候,是有CAP_SETUID权限的,因为它在此之前通过其tracer的privileged身份,执行的passwd是以正常的SUID执行(passwd的owner是root),也就是说C进程成为了root身份(passwd的owner)。

最后,poc的测试结果如下图。

本poc使用的pkexec只可以在本地session运行,否则需要二次认证,因此无法在ssh session中使用。

 

参考资料

https://bugs.chromium.org/p/project-zero/issues/detail?id=1903
http://man7.org/linux/man-pages/man2/ptrace.2.html
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6994eefb0053799d2e07cd140df6c2ea106c41ee
http://man7.org/linux/man-pages/man7/capabilities.7.html

(完)