HackTheBox - Cyberpsychosis
CHALLENGE DESCRIPTION
Malicious actors have infiltrated our systems and we believe they've implanted a custom rootkit. Can you disarm the rootkit and find the hidden data?
Archivos iniciales:
diamorphine.ko
Análisis inicial
Al ejecutar file sobre el archivo, vemos lo siguiente:
$ file diamorphine.ko
diamorphine.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=e6a635e5bd8219ae93d2bc26574fff42dc4e1105, with debug_info, not strippedELF 64-bitSe trata de un objeto binario / ejecutablewith debug_info, not stripped: Si ejecutamos strings o lo abrimos con ghidra, es probable que tengamos más facilidad para identificar qué hace cada función o variable.
Al principio, no sé qué es un .ko. así que busco en Internet y encuentro esto:
A .ko file (Kernel Object) is a loadable kernel module in Linux, encapsulating code and data that can be dynamically inserted into or removed from the running kernel.
Se trata de un módulo del kernel de Linux, diseñado para cargarse dinámicamente. Tiene varias secciones:
.text: Instrucciones en assembly.rodata: Datos de solo lectura.data: Variables inicializadas.bss: Variables sin inicializar.init.text: Código que se ejecuta una sola vez al iniciar el módulo.exit.text: Código que se ejecuta al parar el módulo.modinfo: Metadatos del autor, licencia, etc....
Antes de intentar descompilarlo, buscamos más información sobre el nombre del archivo, vemos que diamorfina es otro nombre para la heroína y, además, es el nombre de un rootkit de Linux real. Según la página de GitHub del rootkit, hace lo siguiente:
When loaded, the module starts invisible;
Hide/unhide any process by sending a signal 31;
Sending a signal 63(to any pid) makes the module become (in)visible;
Sending a signal 64(to any pid) makes the given user become root;
Files or directories starting with the MAGIC_PREFIX become invisible;
Así que, por defecto, varias de las funciones que encontremos harán cosas como:
Ocultar el binario al ejecutarlo
Mostrar y ocultar procesos al recibir señal
31Mostrar y ocultar usuarios al recibir señal
63Hacer root a un usuario al recibir señal
64Filtrar archivos si su nombre empieza por el MAGIC_PREFIX
Según veo, este rootkit no es tanto uno como los que suelen instalarse cuando un dispositivo es infectado por malware, sino uno que por ejemplo podría usar un pentester para conseguir persistencia, permitiendo conseguir privilegios y ocultarse de un blue team fácilmente. Además, el nombre diamorphine (y el del propio challenge: Cyberpsychosis) parece ser simbólico y relacionado con la droga directamente: Se inyecta en el kernel (o en vena directamente), oculta procesos y manipula syscalls (hace de analgésico) y permite conseguir root instantáneamente (tiene efecto casi inmediato).
Descompilando
Por motivos obvios, no vamos a ejecutar el rootkit localmente, aunque técnicamente podemos desinstalarlo fácilmente e incluso en el repositorio se incluye una guía para ello. Lo descompilaremos con Ghidra, aunque, teniendo el código fuente disponible, sería una oportunidad desaprovechada no ir comparándolo con el pseudocódigo de Ghidra 1 a 1.
Según vayamos descompilando funciones iremos mirando si se ha modificado algo del rootkit o si todo queda igual, para ver si el flag está hardcodeado el algún sitio o hay algo relevante. Si todo queda igual, simplemente desactivaremos el rootkit del servidor y buscaremos el flag.
diamorphine_init() y diamorphine_cleanup() -> Funcionamiento del rootkit
diamorphine_init() y diamorphine_cleanup() -> Funcionamiento del rootkitEsta es la función principal que se ejecuta cuando se conecta el módulo al kernel del sistema. Ghidra nos da lo siguiente (de forma simplificada):
En el kernel la sys_call_table es simplemente un array gigante de punteros a subrutinas, y cada syscall específica tiene un índice asignado. En este archivo del kernel de Linux se puede ver que:
sys_kill(Matar un proceso) tiene el 62 (0x3e)sys_getdents(Mostrar contenidos de un directorio) tiene el 78 (0x4e)sys_getdents64(Como el anterior, pero para 64bit) tiene el 217 (0xd9)
Entonces lo que se hace es lo siguiente (con algunas funciones copiadas del código fuente original porque Ghidra las había detectado erróneamente):
Con diamorphine_cleanup() vemos que se hace la operación contraria:
Ambas funciones son iguales al código fuente, así que no hay nada interesante.
find_task() -> Nada relevante
find_task() -> Nada relevanteGhidra nos devuelve lo siguiente:
Mientras que el código en C original se entiende mucho mejor:
El sistema lee todos los procesos (for_each_process(p)), y , cuando encuentra uno con el PID especificado, devuelve todo su struct entero (toda su info). Si no encuentra uno, devuelve NULL. La diferencia entre el código en C y el de Ghidra es que, como for_each_process() es un macro del sistema y Ghidra solo entiende la lógica del programa, lo que nos muestra es el macro "deshecho". Los procesos en Linux se guardan como una linked-list circular, así que hay que llevar la cuenta de cuál es el primer elemento que se ha comprobado para no repetir la vuelta, por eso Ghidra muestra esto:
Se guarda en process_ptr el struct del proceso init (el primero que se ejecuta al arrancar el sistema), y se hace lo siguiente mientras no se encuente el PID buscado:
Se guarda el puntero al siguiente proceso en
nextPTR(ubicado a mitad del struct, por eso se usa base+offset)Se va al siguiente elemento y se guarda el puntero a su struct en
process_ptrSe compara su PID (ubicado en
Dir.Base_Struct + 0x108) con el buscado.Si no es, se vuelve al inicio del ciclo. Si se encuentra, se devuelve su struct
Si se llega al elemento inicial (init) de nuevo (
nextPTR == &DAT_001028f0, dato hardcodeado que apunta a la cabeza de la linked-list), se devuelve NULL
Esta función es exactamente igual, así que no hay nada que podamos encontrar interesante.
give_root() -> Nada relevante
give_root() -> Nada relevanteEsta subrutina corresponde a la funcionalidad de hacer root al usuario, así que en algún otro lado habrá una comprobación en la que se mira si un usuario manda una señal 64 a algún PID, ejecutando esta función si es así.
Vemos que usa prepare_creds() para guardar un struct de credenciales (struct cred, aunque Ghidra lo detecta como Long), luego cambia todos los UID/RID a 0 (para hacer root al usuario), y finalmente hace commit_creds() para guardar los cambios. No hay nada más, así que no es la función que buscamos.
hacked_kill() -> Señal custom
hacked_kill() -> Señal customEsta es una de las syscalls cuya dirección de salto el rootkit ha modificado para que se ejecuten instrucciones diferentes. En el código original vemos que se trata de dos funciones, y se usa una u otra en función de la versión del kernel.
Gracias a que tienen headers diferentes, podemos ver que la que se nos muestra en Ghidra es int hacked_kill(pt_regs *pt_regs), es decir, la primera, así que nos quedamos con esa:
Si miramos la función hacked_kill() de Ghidra, vemos que hace exactamente lo mismo y no hay datos escondidos por ahí, aunque hay algo relevante: la señal usada para mostrar/ocultar el módulo del kernel no es la que venía por defecto (63), sino que es la 46.
En
hacked_getdents() -> Magic Prefix Custom
hacked_getdents() -> Magic Prefix CustomSabemos que el rootkit hacía dos cosas en esta función:
Si se lista
/proc, se ocultan los procesos marcados como invisiblesSi no, se ocultan elementos (archivos/directorios) que empiecen por
MAGIC_PREFIX
Pero cuál es el MAGIC_PREFIX? Si buscamos en Ghidra:
Aquí vemos una serie de datos (recordemos que en Little-Endian): 0x69736f6863797370 y 's'. Esto, pasado a ASCII, es:
s+isohcysp=psychosis
Conexión al servidor
Así que sabemos que el rootkit, cuando está activo, oculta todos los archivos que empiecen por psychosis. Además, sabemos cómo desactivarlo, pues con la señal 46 se muestra/oculta el malware:
Para ello, primero tendremos que hacernos root, usando la propia funcionalidad del rootkit:
Ahora hacemos visible el módulo usando la señal custom y lo desactivamos
Finalmente buscamos archivos que empiecen por psychosis:
Y tenemos el flag.
Última actualización