Cómo inyectar código en el kernel de Linux sin código fuente ni símbolos de depuración
Imagina este escenario: tienes un binario del kernel de Linux ya listo para un dispositivo móvil o una placa ARM64 embebida. No hay código fuente disponible, la información de depuración fue eliminada durante la compilación, y necesitas interceptar una llamada al sistema o agregar tu propia lógica directamente a nivel del kernel.
El enfoque habitual en estos casos es largo y tedioso. Tienes que hacer ingeniería inversa del binario en IDA Pro o Ghidra, encontrar manualmente los offsets de las funciones y escribir parches frágiles. Un desarrollador con el sobrenombre bmax121 decidió simplificar este proceso y publicó el proyecto KernelPatch.
Por qué se necesita KernelPatch
KernelPatch es un framework para parchear y enganchar el kernel de Linux cuando todo lo que tienes para trabajar es una imagen del kernel despojada como vmlinux o boot.img.
Cuando un kernel se compila sin información de depuración, las herramientas tradicionales de depuración como kprobes se niegan a funcionar sin soluciones alternativas adicionales. KernelPatch resuelve este problema por sí mismo. Recupera la tabla de símbolos directamente del binario del kernel y proporciona una API conveniente para modificar código ejecutable.
El proyecto es útil para ingenieros reversos, investigadores de seguridad de Android y desarrolladores de software de bajo nivel para sistemas embebidos.
Qué puede hacer el framework
El desarrollador incorporó varias capacidades importantes en el proyecto para trabajar con el kernel:
- Búsqueda de símbolos sin código fuente. La herramienta analiza estructuras internas de
kallsymsdentro de la imagen ya lista. No necesita archivos externos deSystem.mapni código fuente. Un enfoque similar se utiliza en la utilidadvmlinux-to-elf, de la cual el autor tomó prestadas algunas ideas. - Dos modos de inyección de código. Puedes hacer un parche estático a la imagen (por ejemplo, sobrescribir
boot.imgen disco) o cargar código dinámicamente en tiempo de ejecución. - Intercepción de funciones mediante inline hook. Para la arquitectura ARM64, la interceptación de llamadas se basa en el trabajo de
android-inline-hook. El framework maneja correctamente las instrucciones de direccionamiento relativo (b,bl,adrp) para no romper la ejecución del código original. - Hooks de llamadas al sistema. Reemplazar direcciones en la tabla de
sys_call_tablesolo toma un par de comandos. - Módulo SU integrado. Para dispositivos Android, el proyecto incluye su propia implementación de superusuario.
Cómo funciona internamente
Para cargar módulos de usuario (KPM — Kernel Patch Modules) sobre la marcha, el framework necesita memoria dentro del kernel. Para esto, KernelPatch incluye un asignador TLSF compacto. Ayuda a asignar páginas de memoria ejecutable (ROX) directamente en el espacio de direcciones del kernel.
Al realizar un inline hook, el framework lee las primeras instrucciones de la función objetivo, forma un llamado trampolín a partir de ellas y escribe un salto hacia tu manejador. Cuando tu código termina de ejecutarse, el control regresa a la función original a través del trampolín.
+-------------------+ +---------------------+ +-------------------+
| Целевая функция | ---> | Ваш обработчик KPM | ---> | Трамплин (Orig) |
| (инструкция JMP) | | (пользовательский) | | (продолжение) |
+-------------------+ +---------------------+ +-------------------+
Requisitos del sistema y limitaciones
El proyecto tiene límites claros de aplicabilidad que es importante conocer de antemano:
- Solo la arquitectura ARM64 es compatible. Aún no hay versiones para x86_64 o RISC-V.
- Rango de versiones del kernel — desde Linux 3.18 hasta 6.6.
- El requisito principal es que el kernel debe estar compilado con el indicador
CONFIG_KALLSYMS=y.
Si el proveedor del firmware eliminó completamente CONFIG_KALLSYMS en la etapa de compilación, KernelPatch no podrá analizar automáticamente las direcciones de las funciones. Afortunadamente, este indicador se deja habilitado en la mayoría de los firmwares de dispositivos Android y ARM.
Ejemplo de uso práctico
Digamos que necesitas rastrear todos los lanzamientos de procesos a través de la llamada sys_execve en un dispositivo Android de prueba, pero no quieres recompilar el kernel desde el código fuente del proveedor.
Antes, habrías tenido que construir un módulo cargable de kernel completo (LKM), lidiar con problemas de verificación de firma digital y la ausencia de un cargador de módulos en el kernel.
Con KernelPatch, el proceso se ve diferente:
- Escribes un pequeño código en C con la lógica de interceptación.
- Lo compilas en formato KPM.
- Alimentas la utilidad KernelPatch con tu
boot.imgy el módulo KPM compilado. - Obtienes un
boot.imgmodificado y lo flasheas a través de fastboot.
Después de que el dispositivo arranca, cada llamada a execve pasa primero por tu manejador.
El README del proyecto es bastante lacónico, y toda la documentación detallada tendrá que estudiarse a partir del código fuente y los archivos en la carpeta doc/. Sin embargo, el proyecto se está desarrollando activamente y ya ha recopilado más de 14,000 estrellas en GitHub.
Por cierto, si no necesitas el framework en sí para experimentos, sino más bien un root listo para usar para Android basado en él, el autor recomienda buscar su proyecto adyacente — APatch. Esta es una alternativa de más alto nivel a Magisk y KernelSU, utilizando KernelPatch como base.
Para investigadores de seguridad y desarrolladores de ARM64, KernelPatch es un gran hallazgo, que ahorra docenas de horas al analizar kernels de código cerrado.
Proyectos relacionados