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);
有两种方法可以建立跟踪关系(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)
与执行权限相关的操作主要位于 `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 }
看看上面代码的以下两行代码:
- 第 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 函数运行。