
Desencadenando y analizando la vulnerabilidad del kernel de Android CVE-2019-2215
En noviembre de 2017, el sistema syzkaller detectó un error use-after-free en el kernel de Linux. En febrero de 2018, esto fue parcheado en algunos kernels de Linux y versiones de Android.
Esta corrección nunca se incluyó en los boletines mensuales de seguridad de Android, por lo que no fue parcheada en muchos dispositivos recién lanzados, como Pixel y Pixel2.
En septiembre de 2019, Project Zero informó a Android sobre las implicaciones de seguridad de este error. Luego, Android asignó CVE-2019-2215 a esta vulnerabilidad para hacerla más formal y conocida.
CVE-2019-2215 es un use-after-free en binder.c que permite la escalada de privilegios (obtener acceso root) desde una aplicación de Android. No es necesario la interacción del usuario para explotar esta vulnerabilidad. Solo requiere la instalación de una aplicación local maliciosa.
Aquí vamos a presentar esta vulnerabilidad del kernel de Android con más detalle y usaremos esta vulnerabilidad para obtener acceso root (escalada de privilegios) de todo el dispositivo Android.
Usaremos esta Prueba de Concepto (PoC):
https://github.com/cloudfuzz/android-kernel-exploitation
Primero te mostraremos una forma de desencadenar esta vulnerabilidad en un emulador de Android y provocar un fallo del kernel. Luego, para ver lo peligroso que sería, continuamos usando la PoC para obtener acceso root en el dispositivo Android simulado. Luego analizaremos el código del kernel para ver cuál es la razón (análisis estático y análisis dinámico). Después del análisis, veremos cómo obtuvimos el acceso root. Por último, veremos cómo se mitiga esta vulnerabilidad mediante parches.
Para provocar un fallo del kernel desencadenando esta vulnerabilidad, seguimos estos pasos:
Mira el siguiente video:
[video]
En esta (y la siguiente) sección entenderemos por qué ocurre el fallo mediante análisis estático y dinámico.
Aquí analizaremos el código del kernel (análisis estático) para entender el problema. En crash_report.txt está el informe de KASan que indica que este es un error de use-after-free. Significa que se asignó un objeto en el montón (y tenemos una referencia a él), luego liberamos el objeto del montón y luego lo invocamos erróneamente mediante una referencia. En este informe se imprime la traza de pila de estas tres etapas.
Si recuerdas de lo anterior, usamos 'trigger.cpp' en la PoC.
Aquí está el código principal de trigger.cpp:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }
Veamos qué está haciendo 'trigger.cpp'.
En Android (como en otros sistemas operativos basados en Unix) tenemos algunos procesos. Cada programa que ejecutamos crea uno (o más) procesos y estos procesos son manejados por el SO. El SO puede cambiar entre ellos (multitarea) o terminar un proceso, etc. Por razones de seguridad, los procesos están aislados entre sí por defecto.
En algunos casos, un proceso puede necesitar intercambiar datos con otro proceso. Esto se llama Comunicación entre Procesos (IPC). Hay varias formas en que los procesos pueden comunicarse en linux. Android introdujo un mecanismo IPC específico llamado **'Binder'**. Binder es un controlador del kernel para facilitar la comunicación entre procesos.
En Android, la IPC se puede realizar llamando directamente a algunos métodos del kernel (la mayoría de ellos están en drivers/binder.c) o usando implementaciones de alto nivel (por ejemplo en java).

Para usar **binder**, debemos abrir el módulo binder del kernel. Esto se hace usando la línea 3 de trigger.cpp. Luego tenemos un puntero de descriptor de archivo. Usando este fd, se pueden identificar el iniciador del kernel y los destinatarios de la IPC.
Todas las interacciones con el controlador se realizarán a través de un pequeño conjunto de comandos **'ioctl'** (BINDER_THREAD_EXIT, BINDER_WRITE_READ, ...).
Más sobre binder: [link1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf), [link2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)
En linux, tenemos un concepto llamado '**event polling**'. La API '**epoll**' se usa cuando queremos monitorear múltiples descriptores de archivo (los descriptores de archivo son lo que tenemos al abrir un controlador o trabajar con E/S, etc.).
**epoll** es una estructura del kernel que tiene dos campos importantes.
* lista de interés = lista de descriptores de archivo que queremos monitorear.
* lista lista = lista de descriptores de archivo que están listos para E/S.
Para usar event polling, primero creamos un epoll (línea 4), luego agregamos o eliminamos (EPOLL_CTL_ADD) un evento (&event) asociado con un descriptor de archivo (**fd**) a nuestro epoll creado (**epfd**) llamando al método **epoll_ctl** del kernel.
=> `epoll_event event` es un evento que se activa cuando el archivo asociado (fd) está disponible para una operación de lectura.
Ahora entendemos qué está haciendo **trigger.cpp** (¡no es necesario profundizar!). Abre el módulo **binder**, crea un **epoll** para escucharlo cuando esté listo. Luego, en la línea 6, salimos del binder que iniciamos en la línea 3.
#### Asignar:
Al llamar a open(), en realidad llamamos a **open_binder()** (implementación de open() en binder.c). En open_binder(), se creará una nueva estructura **'binder_proc'** y:
` fd->private_data = binder_proc`
Al llamar a **epoll_create()**, se creará una nueva estructura epoll y se agregará a una estructura de cola.

Al llamar a **epoll_ctrl(epdf, ADD, fd, event)**, se crea un nuevo **ep_item**, se asocia **fd** (descriptor de archivo a escuchar) con este **ep_item** y se inserta en el **árbol rojo-negro** de event_poll (una estructura de datos en ep para guardar ep_items). También llama a **ep_item_poll()**, este método maneja la asociación de la función callback con ep_item.
Crea una nueva estructura **binder_thread** (**la asignación ocurre aquí**), la enlaza al **binder_proc** (creado anteriormente). Luego se crea una estructura **epoll_entry**, que tiene dos listas: **epoll_entry->wait** y **epoll_entry->whead**. Ambas listas tienen un puntero al **binder_thread** creado anteriormente.
Luego, **epoll_entry** se enlaza a ep_item (**ep_item->pwqlist** es una lista que contiene este epoll_entry).

#### Liberar:
Al llamar a **ioctl(fd, ...)**, se accede a **binder_proc** a través de fd->private_data, luego la estructura **binder_thread** será **liberada** de la memoria.
#### Usar:
Cuando nuestro proceso actual sale, se llamará a **epoll_ctl(epfd, DEL, fd, event)**.
Llama a **ep_remove(event_poll, ep_item)**. Este método obtiene el **epoll_entry** de **ep_item->pwqlist**, luego obtiene la lista de espera de **ep_item (ep_item->wait)**, que es una lista enlazada. Quiere eliminar uno de los elementos de esta lista de espera.
Usa el siguiente código (se usa pseudocódigo):```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;
Aquí wait->entry es un puntero a binder_thread que fue eliminado de la memoria! Por lo tanto, es un use after free y provoca un error!!
Creamos un event_poll que tiene un red_black_tree, cada nodo es un ep_item que tiene un campo que es una lista de epoll_entry, cada epoll_entry tiene dos punteros a la estructura binder_thread (wait, whead).
Al llamar a ioctl() liberamos el binder_thread de la memoria, luego al salir, se accede a esta estructura a través de un puntero que aún estaba disponible!
El análisis dinámico es la prueba y evaluación de un programa mediante la ejecución de datos en tiempo real; para encontrar errores en un programa mientras se ejecuta.
pasos:
Construir el kernel de Android sin KASan
Iniciar el emulador con el kernel recién construido
Lanzar el emulador
Construir el desencadenante de la vulnerabilidad y enviarlo al dispositivo virtual
Poner un punto de interrupción en GDB
Cargar el script personalizado de python (dynamic-analysis.py en el repositorio):
Para rastrear llamadas a funciones y volcar el fragmento de la estructura binder_thread antes y después de que se libere. También volcar la misma estructura binder_thread antes y después de que se haya realizado la operación de unlink.
En este archivo primero eliminamos todos los puntos de interrupción y luego colocamos 2 puntos de interrupción (BP); El primer símbolo es “binder_free_thread” (rastreará la función binder_free_thread) antes de que binder_thread sea liberado, se llamará a la función stop; por lo que los parámetros y el símbolo se mostrarán con (gb.write(....) ) y luego se llamará al método de devolución de llamada (lo configuramos set_dump_binder_thread ); En esta función, binder_thread_address se establecerá en nuestra variable global y gdb.execute enviará cualquier salida producida por el comando a la salida estándar de GDB.
El segundo símbolo es “remove_wait_queue”(rastreará la función remove_wait_queue) Los parámetros que queremos observar son "wq_head", "wq_entry" y para la salida se establecerá un punto de interrupción en wait.c:52. Su función de devolución de llamada es dump_binder_thread . Estos puntos de interrupción mostrarán lo que sucede antes y después de la operación de unlink.
resultado:
binder_free_thread(thread=0xffff88800c18f200)(enter) 0xffff88800c18f200: 0xffff88806793c000 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88805c05cae0 0xffff88800c18f2b0: 0xffff88805c05cae0 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
en nuestro código python teníamos los siguientes códigos:``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )
y es la función binder_free_thread, por lo que el parámetro para esta función es un puntero a binder_thread:```
static void binder_free_thread(struct binder_thread *thread)
{
[...]
kfree(thread);
}
en el resultado tuvimos(binder_free_thread(thread=0xffff88800c18f200)(enter)).
Las líneas siguientes muestran el resultado de la ejecución , con el siguiente comando obtenemos el desplazamiento de binder_thread.wait:``` p offsetof(struct binder_thread, wait)
el resultado es 0xa0 y si ponemos wait.head en lugar de wait en el comando , el resultado será 0xa8 y contiene `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) 0xffff88800c18f200: 0xffff88800c18f600 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88805c05cae0 0xffff88800c18f2b0: 0xffff88805c05cae0 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
En remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) , wq_head es la dirección de binder_thread.wait y wq_entry son datos de wait.head.
después de esto ocurrirá una operación de desenlace (unlink).
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53. remove_wait_queue_wait.c:52(exit) 0xffff88800c18f200: 0xffff88800c18f600 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88800c18f2a8 0xffff88800c18f2b0: 0xffff88800c18f2a8 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
La parte resaltada se debe a esto. El resultado es después de la operación de desenlace y podemos ver que para desenlazar se escribe la dirección (0xffff88800c18f2a0 + 0x8) en next y previous.
significa:
(puntero a binder_thread->wait.head) = binder_thread->wait.head.next = binder_thread->wait.head.prev
En esta parte mostraremos cómo podemos usar este error para obtener acceso root.
Recuerda de arriba, teníamos una estructura binder_thread, fue liberada y usada nuevamente mediante un puntero. Este es el código de la estructura binder_thread:``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };
Uno de los campos es el puntero **task_struct**, esta estructura tiene un campo llamado **addr_limit**.
Cuando queremos acceder a una dirección en un proceso, se verificará si la dirección está en **user-space** o no, si estaba en **kernel-space**, este acceso debería bloquearse. Esta verificación se realiza comparando nuestra dirección con **addr_limit**, si nuestra dirección es menor que **addr_limit**, tenemos acceso válido.
**addr_limit** está separando realmente el espacio de usuario del espacio de kernel, por lo que al cambiar este campo en **task_struct** tenemos acceso completo al espacio de kernel y ¡podemos hacer cualquier cosa!
Aquí tenemos dos pasos para explotar:
1. encontrar la dirección de task_struct en el espacio de kernel
2. cambiar el addr_limit en task_struct
#### Encontrar la dirección de task_struct
En el kernel, podemos realizar E/S vectorizada, lo que significa que podemos escribir o leer más de un bloque de datos desde o hacia un descriptor de archivo (archivo, socket, etc.).
La E/S vectorizada se realiza mediante los métodos **writev**, **readv**, **recvmsg** y la estructura **iovec**.```
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};
Al usar E/S vectorizada, realizamos operaciones de E/S en un arreglo de búferes (iovec). Cada iovec tiene un puntero a un búfer (iov_base) y el tamaño del búfer (iov_len).
Por ejemplo, si queremos escribir un arreglo de búferes en un archivo (fd), llamamos a
writev(fd, iovecStack, count)
Este método (como readv y recvmsg) primero copia el arreglo iovec (iovecStack) al espacio del kernel, luego lee de estos búferes y escribe en fd.
Podemos escribir y leer búferes usando pipe; pipe es una estructura que nos da dos descriptores de archivo, uno para lectura y otro para escritura. Pipe tiene una longitud en bytes; cuando un proceso escribe en un pipe más de su longitud, el pipe bloquea a ese proceso y espera a que otro proceso lea de ese pipe (usando el descriptor de archivo de lectura de ese pipe).
El kernel intenta asignar memoria (para una estructura) de acuerdo a su tamaño. Por ejemplo, cuando liberamos la estructura binder_thread de la memoria (ver análisis estático), y después de eso, si tenemos una estructura con un tamaño similar a binder_thread, tiene una alta probabilidad de ser asignada en el mismo lugar que el binder_thread liberado.
Primero creamos un iovecStack (arreglo de estructuras iovec) con un tamaño similar a la estructura binder_thread.
Luego liberamos binder_thread de la memoria (ver la parte de 'free' en el análisis estático)
Luego llamamos a writev() en este iovecStack. La primera parte de este método copia iovecStack en el espacio del kernel.
Es muy probable que nuestro iovecStack se asigne en el mismo lugar que el binder_thread liberado.
Si tenemos suficientes iovec en iovecStack, la memoria en la ubicación de binder_thread se ve así:

Puedes ver que iovecStack[10].iov_base y iovecStack[10].iov_len y iovecStack[11].iov_base estarán en el mismo lugar que los campos wait.lock, wait.head.next y wait.head.prev de binder_thread.
En la parte 'use' del análisis estático vimos que el fallo ocurría debido a un acceso a la parte wait de binder_thread durante el proceso de desenlace (eliminar un elemento de una lista enlazada).
Durante el proceso de desenlace, wait.head se desenlaza y sus campos next y prev apuntarían al campo wait de binder_thread (desenlazando).

Antes de que ocurra la segunda parte de writev (escribir desde los iovecs al archivo), si ejecutamos el proceso de desenlace, iovecStack[10].iov_len y iovecStack[11].iov_base serán sobrescritos por una dirección del kernel. Luego, al ejecutar el resto de writev, cuando quiera procesar iovecStack[11], leerá desde iovecStack[11].iov_base (=dirección de wait en binder_thread) una longitud de iovecStack[11].iov_len de datos.
Si iovecStack[11].iov_len es suficiente, leemos desde el campo wait hasta el campo task_struct del binder_thread, por lo que obtendríamos el puntero a task_struct.
Entonces, para obtener el puntero a task_struct hacemos lo siguiente:
ver exploit.cpp en el repositorio.
Ahora tenemos la dirección de task_struct en el espacio del kernel (task_ptr).
Aquí usamos socket_pair en lugar de pipe. Y usamos recvmsg() para leer desde el socket y escribir en los iovecs.
Pasos:
Después de escribir, **recvmsg**() comienza a leer desde el socket y escribir en **iovecStack**. Debido a basura, ha escrito hasta **iovecStack**[10].
Por lo tanto, comienza a escribir **finalSocketData** en **iovecStack**[12], obtiene la dirección de **iovecStack[12].iov_base**, que es la dirección de **wait en binder_thread** debido a la operación de desvinculación, **iovecStack[12].iov_len** se establece en 4 bytes, por lo que escribe:
* 0x1 en iovecStack[10].iov_len
* 0x41414141 en iovecStack[11].iov_base
* 0x8 + 0x8 + 0x8 + 0x8 en iovecStack[11].iov_len
* **pointer_to_addr_limit** en iovecStack[12].iov_base
ahora se han escrito 4 bytes en **iovecStack[11]**, por lo que **recvmsg**() va a **iovecStack[12]** para escribir el resto de **finalSocketData**:
escribe **0xFFFFFFFFFFFFFFFE **en la dirección en** iovecStack[12].iov_base** que está establecida a **pointer_to_addr_limit**, esto significa que recvmsg() cambia addr_limit a 0xFFFFFFFFFFFFFFFE (no 0xFFFFFFFFFFFFFFFF debido a algunos problemas con arm64).
Ahora el espacio de usuario se expande a aproximadamente todo el espacio del kernel! (¡y puede hacer cualquier cosa!!)
# Parche
binder_poll() pasa la cola de espera thread->wait en la que se puede dormir para esperar trabajo. Cuando un hilo que usa epoll sale explícitamente usando BINDER_THREAD_EXIT, la cola de espera se libera, pero <span style="text-decoration:underline;">nunca se elimina de la estructura de datos epoll correspondiente</span>. Cuando el proceso posteriormente sale, el código de limpieza de epoll intenta acceder a la lista de espera, lo que resulta en un use-after-free.
Prevenir esto usando POLLFREE cuando el hilo sale.
teníamos este código:```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
.
.
.
int active_transactions = 0;
.
.
.
binder_thread_dec_tmpref(thread);
return active_transactions;
}
Estas líneas han sido añadidas al código fuente:
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
.
.
.
/*
* If this thread used poll, make sure we remove the waitqueue
* from any epoll data structures holding it with POLLFREE.
* waitqueue_active() is safe to use here because we're holding
* the inner lock.
*/
if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
}
binder_inner_proc_unlock(thread->proc);
/*
* This is needed to avoid races between wake_up_poll() above and
* and ep_remove_waitqueue() called for other reasons (eg the epoll file
* descriptor being closed); ep_remove_waitqueue() holds an RCU read
* lock, so we can be sure it's done after calling synchronize_rcu().
*/
if (thread->looper & BINDER_LOOPER_STATE_POLL)
synchronize_rcu();
.
.
.
}
ver el código completo aquí
el sistema operativo es una capa de software responsable de hacer que todo el hardware funcione más eficientemente y construir una infraestructura sobre la cual pueden funcionar las aplicaciones que usas; su núcleo es el kernel.
La idea detrás de la explotación es simple: el software tiene errores, y los errores hacen que el software se comporte incorrectamente o realice incorrectamente una tarea para la cual fue diseñado para realizar correctamente; explotar un error significa convertir este mal comportamiento en una ventaja para los atacantes.
Los errores que son explotables se denominan vulnerabilidades.
Un usuario o proceso privilegiado es aquel que tiene acceso completo al dispositivo.
La mayoría de las arquitecturas de conjuntos de instrucciones proporcionan al menos dos modos de ejecución:
privilegiado: todas las instrucciones a nivel de máquina son accesibles.
no privilegiado: solo un subconjunto de las instrucciones son accesibles.
Las vulnerabilidades de Use-After-Free son un tipo de falla de corrupción de memoria que puede ser aprovechada por hackers para ejecutar código arbitrario.
Use-After-Free se refiere específicamente al intento de acceder a memoria después de que ha sido liberada, lo que puede hacer que un programa se bloquee o, en el caso de una falla de Use-After-Free, puede resultar potencialmente en la ejecución de código arbitrario o incluso habilitar capacidades completas de ejecución remota de código.
Es un use-after-free en Binder en el kernel de Android. El error es una vulnerabilidad de escalada de privilegios local que permite un compromiso completo de un dispositivo vulnerable. Si se encadena con un exploit del renderizador del navegador, este error podría comprometer completamente un dispositivo a través de un sitio web malicioso. Es alcanzable desde dentro del sandbox de Chrome.
Nota: Funciona en Pixel 1 y 2, pero no en Pixel 3 y 3a. ver este ataque.
Los boletines de seguridad de Android son una lista publicada por Google (mensualmente). Esta lista contiene vulnerabilidades de seguridad corregidas que afectan al framework de Android, al kernel de Linux, etc.
Syzkaller es un fuzzer de kernel. El fuzzing es una técnica de prueba en la que un programa automatizado produce entradas semi-aleatorias para un programa objetivo para verificar si se desencadena algún error. El fuzzing es especialmente útil para encontrar errores de corrupción de memoria en programas C o C++.
Project Zero es un equipo de analistas de seguridad empleados por Google encargado de encontrar vulnerabilidades de día cero. Una vulnerabilidad de día cero es una vulnerabilidad que desconocen aquellos que deberían solucionarla.
GDB significa GNU Project Debugger, que es el depurador más popular para sistemas UNIX para depurar programas C y C++. GDB permite ejecutar el programa hasta cierto punto, luego detenerse e imprimir los valores de ciertas variables en ese punto, o recorrer el programa línea por línea e imprimir los valores de cada variable después de ejecutar cada línea.
Puedes conectar tu emulador de Android con un gdbserver para depurar.
Kernel Address SANitizer (KASAN) es un detector de errores de memoria dinámico diseñado para encontrar errores de fuera de límites y use-after-free. KASAN utiliza instrumentación en tiempo de compilación para insertar comprobaciones de validez antes de cada acceso a memoria, y por lo tanto requiere una versión del compilador que lo soporte. Los accesos a la memoria del kernel pueden verificarse contra el mapa sombra para ver si son válidos.
QEMU (Quick Emulator) es un emulador gratuito y de código abierto que realiza virtualización de hardware. El Emulador de Android es descendiente del emulador QEMU; añade soporte para arrancar dispositivos Android, emula hardware típico de Android (OpenGL, GPS, GSM, Sensores) y una interfaz gráfica. El emulador de Android extiende qemu de varias maneras.
En el análisis estático, usamos el código fuente de un programa para encontrar un error o cualquier problema. No ejecutamos el programa.
En el análisis dinámico, analizamos el comportamiento de un programa mientras se está ejecutando. Por ejemplo, proporcionando entradas especiales.
Android NDK es un conjunto de herramientas que permite ejecutar código nativo como C y C++ en un dispositivo Android.
El kernel goldfish de Android se utiliza para ejecutar código del kernel en un emulador de Android. Se puede clonar y modificar, y luego construir para usarlo en un emulador.
ADB es una herramienta de línea de comandos. Ayuda a comunicarse con un dispositivo Android en ejecución y obtener un shell del mismo. Se puede utilizar para fines de depuración.
https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html
https://nvd.nist.gov/vuln/detail/CVE-2019-2215
https://www.tutorialspoint.com/gnu_debugger
https://www.kernel.org/doc/html/latest/dev-tools/kasan.html
https://android.googlesource.com/kernel/goldfish/
https://man7.org/linux/man-pages/man7/epoll.7.html
https://www.scaler.com/topics/c/debugging-c-program/
...
Iniciar adb shell y ejecutar el PoC desencadenante