El problema fundamental que aborda este artículo es la abstracción de la ejecución de programas en sistemas operativos modernos, particularmente Linux. Desafía la noción intuitiva de que solo los binarios compilados son 'ejecutables' y que los scripts son 'interpretados', al demostrar que esta distinción es más una cuestión de capas de interpretación. La tesis central es que el kernel de Linux actúa como un intérprete para los initrds (y, por extensión, para los binarios ELF a través de ld.so), y que esta capacidad puede ser explotada para construir sistemas recursivos o 'quines' a nivel de sistema operativo. Esto es relevante ahora debido a la creciente complejidad de las cadenas de arranque y la necesidad de entender las capas más profundas de la ejecución del sistema para optimizar, asegurar o innovar en entornos de infraestructura moderna.
Históricamente, la idea de un 'intérprete' se ha asociado con lenguajes de alto nivel como Python o Bash. Sin embargo, a medida que los sistemas operativos han evolucionado, la línea entre 'ejecución directa' y 'interpretación' se ha difuminado. Desde los primeros sistemas Unix, la capacidad de un programa para invocar a otro (como un shell invocando un script) ha sido fundamental. La introducción de mecanismos como el 'shebang' (#!), los cargadores dinámicos (ld.so) y, más recientemente, binfmt_misc, ha formalizado y extendido esta cadena de interpretación hasta el propio kernel, que es el último intérprete de las instrucciones de la CPU.
Arquitectura del Sistema
El sistema descrito se basa en varios componentes clave de Linux. Primero, kexec es fundamental; permite cargar un nuevo kernel y un initrd en memoria y ejecutarlo, reemplazando el kernel actual sin un reinicio completo del hardware. Esto es crucial para la recursión de kernels. El initrd (initial RAM disk) es un archivo CPIO que contiene un sistema de archivos raíz mínimo y un script init que se ejecuta al arrancar el kernel. En el ejemplo, este init script es el que orquesta la recursión, creando un nuevo CPIO de sí mismo y luego usando kexec para cargarlo.
La ejecución de archivos CPIO como programas se logra mediante binfmt_misc. Este módulo del kernel permite registrar manejadores para tipos de archivos arbitrarios basados en 'números mágicos' (secuencias de bytes iniciales). Al registrar un script que invoca qemu-system-x86_64 (o kexec en la versión optimizada) para archivos CPIO, se convierte efectivamente al kernel de una máquina virtual (o al siguiente kernel en la cadena de kexec) en el intérprete de esos CPIOs. El proceso de decodificación de Base64 y extracción de CPIO se realiza con base64 -d y cpio -i respectivamente. La creación del CPIO recursivo utiliza find y cpio -vo -H newc para empaquetar el sistema de archivos actual (excluyendo /proc y /r) en un nuevo archivo CPIO.
Flujo de Ejecución Inicial del 'Malware'
- 1 curl | sh Descarga y ejecuta el script shell rkx.gz descomprimido.
- 2 rkx (shell script) Verifica permisos (root) y herramientas (kexec, base64, cpio).
- 3 base64 -d > r Decodifica el payload base64 en un archivo CPIO llamado 'r'.
- 4 cpio -uidv < r 'k' > k Extrae el kernel 'k' del CPIO 'r'.
- 5 kexec --load k --initrd r Carga el nuevo kernel 'k' y el initrd 'r' en memoria.
- 6 kexec --exec Ejecuta el nuevo kernel, reemplazando el sistema operativo actual.
Flujo de Recursión del Initrd
- 1 Nuevo Kernel arranca El kernel 'k' arranca con 'r' como initrd.
- 2 /init (dentro de 'r') Monta /proc, crea un nuevo CPIO de sí mismo en /r.
- 3 find | cpio > /r Empaqueta el sistema de archivos actual (excluyendo /proc, /r) en un nuevo CPIO.
- 4 kexec --load /k --initrd /r Carga el mismo kernel 'k' y el nuevo CPIO '/r' como initrd.
- 5 kexec --exec Ejecuta el nuevo kernel, reiniciando el ciclo recursivo.
| Capa | Tecnología | Justificación |
|---|---|---|
| orchestration | kexec | Permite la carga y ejecución de un nuevo kernel y initrd sin un reinicio completo del sistema, fundamental para la recursión de kernels. --load, --initrd, --exec, --reuse-cmdline |
| storage | cpio | Formato de archivo de archivo utilizado para crear el initrd, que es el sistema de archivos raíz inicial cargado por el kernel. vs tar, squashfs -vo -H newc (para crear), -uidv (para extraer) |
| compute | binfmt_misc | Módulo del kernel que permite registrar manejadores para ejecutar archivos basados en sus 'números mágicos', extendiendo la capacidad de ejecución del kernel a formatos no nativos (como CPIO o EXE). Registro de un manejador para el magic string de CPIO (\x30\x37\x30\x37\x30\x31) |
| orchestration | QEMU | Utilizado como un intérprete de initrd virtualizado, demostrando cómo un kernel de VM puede ser el 'intérprete' de un archivo CPIO. vs VirtualBox, VMware -kernel, -initrd, -append, -nographic, -m, -no-reboot |
| compute | ld.so (Dynamic Linker/Loader) | Intérprete para binarios ELF dinámicamente enlazados, cargando bibliotecas compartidas y preparando el programa para su ejecución por el kernel. |
#!/bin/sh
set -x
if [ "$(id -u)" -ne 0 ]; then echo "Please ensure you are running as root/sudo"; exit 1; fi
if ! command -v kexec && command -v base64 && command -v cpio 2>&1 >/dev/null ; then echo "Please ensure kexec-tools, base64, and cpio are installed"; exit 1; fi
base64 -d <<912367yuiogrjklhsdijlslksdawuil234ui > r
MDcwNzAxMDAwQjI0MDkwMDAwNDE2RDAwMDBGRkZFMDAwMEZGRkUwMDAwMDAwMzAwMDAwMDAxMDAw
...
AAAAAAAAAAAAAA==
912367yuiogrjklhsdijlslksdawuil234ui
cpio -uidv < r "k" > k
kexec --load k --initrd r --reuse-cmdline
kexec --exec#!/bin/sh
mkdir -p /proc
mount -t proc proc /proc
find / | grep -v /r | grep -v /proc | cpio -vo -H newc > /r
kexec --load /k --initrd /r --reuse-cmdline
kexec --exececho ':cpio:M::\x30\x37\x30\x37\x30\x31::/path/to/my/script.sh:' \
> /proc/sys/fs/binfmt_misc/registerFundamentos Teóricos
El concepto de un programa que se auto-reproduce o se auto-referencia tiene profundas raíces en la teoría de la computación, siendo el 'quine' un ejemplo clásico. Un quine es un programa que, cuando se ejecuta, produce una copia de su propio código fuente. Este concepto fue popularizado por Douglas Hofstadter en su libro 'Gödel, Escher, Bach'. La construcción de un initrd que recursivamente se carga a sí mismo a través de kexec es una aplicación de este principio a nivel de sistema operativo, donde el 'código fuente' es el propio initrd y el 'intérprete' es el kernel de Linux.
La idea de 'tail-call optimization' (optimización de llamadas de cola) también es central. En programación funcional, una llamada de cola es una llamada a una función que es la última operación en la función llamante. La optimización permite reutilizar el marco de pila de la función llamante, evitando el crecimiento ilimitado de la pila en recursiones. Aunque kexec no es una optimización de pila en el sentido tradicional de un compilador, su efecto es análogo: reemplaza completamente el entorno de ejecución (el kernel y su initrd) en lugar de anidarlo, evitando así un 'stack overflow' de kernels. Esto se alinea con los principios de eficiencia en la ejecución de programas y la gestión de recursos, que han sido estudiados desde los primeros días de la informática.