漏洞背景
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。
具体解释如下:
- task A:
fork()
一个child, task B - task B:
fork()
一个child, task C - task B:
execve(/some/special/suid/binary)
,这里是pkexec
:pkexec
是具有set-UID的binary程序,它执行时是其owner也就是root的身份,但我们的A无法attach到这个privileged进程,于是我们接着就需要drop privilege,也就是前文所述的原理,这样才能被APTRACE_ATTACH
并控制。
这里pkexec
指定了--user
为当前用户,其execve
的helper也就最终变成了dumpable的进程,可以被A所PTRACE_ATTACH
。 - task C: 使用
PTRACE_TRACEME
(建立 privileged ptrace relationship),结合3来看,我们的C在pkexec
运行时,可以得到一个root身份的tracer:
- task C:
execve(/usr/bin/passwd)
,此时C的进程就是运行passwd
了,这是以root身份运行的set-UID程序。
由于PTRACE_TRACEME
的使用,C在execve()
这里收到SIGTRAP
,挂起以供其tracer追踪。
- task B: drop privileges (
setresuid(getuid(), getuid(), getuid()))
,这里poc是使用pkexec
的--user
选项完成了这些工作。 - task B: become dumpable again (e.g.
execve(/some/other/binary)
),B进程的程序替换为helper,然后重新成为dumpable的程序(因为SUID程序非dumpable)。 - task A: 此时A可以
PTRACE_ATTACH
到B进程,成为其tracer。 - task A: use
ptrace
to take control of task B - 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