Saltar al contenido
Open Security
Linux RealIntermedio· 30 min

Del comando al kernel: capas, syscalls y strace

Qué pasa entre que escribís ls y aparece la lista. User space, kernel space y las syscalls que cruzan la frontera, vistas con strace en vez de imaginadas.

#linux#kernel#syscalls#strace

Antes de empezar necesitás

  • Una terminal Linux con strace, o el entorno con Podman de este lab
  • Haber hecho el lab de permisos, usuarios y procesos

Al terminar vas a poder

  • Ubicar las capas de un sistema Linux: programas, glibc, syscalls, kernel, drivers y hardware
  • Explicar por qué un segfault mata un proceso y un bug en el kernel tira el sistema
  • Leer una traza de strace y reconocer openat, read, write, clone, execve y wait4
  • Ver el patrón fork + exec con el que la shell lanza cada comando

Para ver y leer

Entorno listo con Podman

Debian con strace para trazar ls y la shell sin tocar tu sistema. lab-check revisa tus trazas, el resumen de syscalls y el segfault que no tira nada.

# Una vez: cloná el repo y construí la imagen
git clone https://github.com/ValentinTorassa/Open-Security-Labs.git
cd Open-Security-Labs
podman build -t osl/del-comando-al-kernel -f entornos/linux-real/del-comando-al-kernel/Containerfile entornos

# Abrí el entorno (se borra al salir)
podman run --rm -it --name osl-del-comando-al-kernel --hostname labs osl/del-comando-al-kernel

# Al terminar, adentro del contenedor
lab-check
  • lab-check revisa lo que hiciste y guarda el resultado en /home/vt/evidencia. Desde otra terminal, en la carpeta del repo, npm run lab:check -- linux-real/del-comando-al-kernel corre el mismo chequeo y copia tu evidencia a evidencia/del-comando-al-kernel/.
  • ¿Usás Docker? Cambiá podman por docker en los comandos y funciona igual.
  • Qué trae, qué revisa y sus límites: entornos/linux-real/del-comando-al-kernel/README.md.

Escribís ls y aparece una lista. En el medio, ls nunca tocó el disco: le pidió al kernel que abra un directorio, que le pase las entradas y que escriba el resultado en tu terminal. Este lab es sobre ese pedido, que tiene nombre (syscall) y se puede ver entero con strace.

Las capas

graph TD
A["Programas<br/>bash · ls · tu código"]
B["Biblioteca de C (glibc)<br/>envuelve las syscalls en funciones"]
C["Interfaz de syscalls<br/>openat · read · write · clone · execve · mmap"]
D["Kernel<br/>procesos · memoria · VFS · red"]
E["Drivers<br/>hablan con cada dispositivo"]
F["Hardware<br/>CPU · RAM · disco · placa de red"]
A --> B --> C --> D --> E --> F
La línea que importa es la interfaz de syscalls: arriba, user space; abajo, kernel space.

El código de user space corre en el ring 3 de la CPU. Las instrucciones que tocan hardware necesitan ring 0, donde corre el kernel. Si un programa intenta leer memoria que no es suya o hablarle directo a un dispositivo, la CPU genera una falla antes de que pase nada y el kernel mata al proceso. No es una política escrita en un archivo: lo hace el silicio.

La frontera: user space y kernel space

Cada proceso tiene su propio espacio de direcciones virtual. Las tablas de páginas que arma el kernel traducen esas direcciones a memoria física, y un proceso no tiene ninguna entrada que apunte a las páginas de otro. No hay un camino que bloquear: el camino no existe.

Eso explica la diferencia entre dos fallas que suenan parecidas:

segfault en user space   el proceso muere, el kernel limpia y el sistema sigue
bug en el kernel         no hay nadie más arriba que lo ataje: kernel panic

Probalo con un proceso que se manda a sí mismo SIGSEGV:

vt@labs:~
{ bash -c 'kill -SEGV $$'; echo "salida: $?"; }
# bash: line 1: 22 Segmentation fault (core dumped) bash -c 'kill -SEGV $$'
# salida: 139

139 es 128 + 11, y 11 es el número de SIGSEGV. El hijo murió; tu shell siguió como si nada. Por eso un módulo del kernel de origen dudoso es un riesgo distinto a un programa con bugs: los módulos corren en ring 0, con todos los privilegios, y un error ahí no queda contenido.

Syscalls: la única puerta

Para cruzar de forma legítima, el programa pone el número de syscall en un registro (rax en x86-64), los argumentos en otros y ejecuta la instrucción syscall. La CPU pasa a ring 0, el kernel busca el handler en una tabla por ese número, hace el trabajo y devuelve el resultado. El programa nunca estuvo en ring 0: esperó.

Syscall Qué hace
openat Abre un archivo y devuelve un file descriptor
read / write Lee o escribe bytes sobre un file descriptor
close Libera el file descriptor
clone Crea un proceso hijo (lo que glibc usa para fork())
execve Reemplaza la imagen del proceso por otro programa
mmap Mapea memoria; es lo que termina llamando malloc
wait4 Espera a que un hijo termine
exit_group Termina el proceso

Casi nunca las llamás directo: glibc las envuelve. fopen() termina en openat, malloc() en mmap o brk, y los runtimes de Python, Go o Node se apoyan en la misma interfaz.

strace: lo que el programa hace de verdad

strace muestra cada syscall de un proceso con sus argumentos y su resultado. Es honesto: no muestra lo que el código dice que hace, muestra lo que le pide al kernel.

vt@labs:~
# Solo las aperturas de archivos
strace -e trace=openat ls /tmp

# Un resumen: cuántas veces se llamó cada syscall
strace -c ls /tmp

La primera línea de la traza de ls casi seguro es esta:

openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

Es el linker dinámico buscando dónde están las bibliotecas compartidas, antes de que main() de ls empiece a correr. Después vienen libc.so.6, libselinux y compañía. Un “programa corriendo” es bastante más que el código que escribió su autor.

fork + exec: cómo la shell lanza un comando

Cada comando que escribís sigue el mismo patrón: la shell se clona, el hijo se reemplaza con el programa pedido y el padre espera.

vt@labs:~
strace -f -e trace=process bash -c 'ls >/dev/null; echo listo'
execve("/usr/bin/bash", ["bash", "-c", "ls >/dev/null; echo listo"], ...) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 21
[pid    20] wait4(-1,  <unfinished ...>
[pid    21] execve("/usr/bin/ls", ["ls"], ...) = 0
[pid    21] exit_group(0)               = ?
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 21

-f hace que strace siga a los hijos. El ; echo listo está a propósito: si le das a bash -c un solo comando, bash se ahorra el clone y hace execve directo, y la traza no muestra el patrón.

Root también es un número

El kernel nunca escuchó hablar de vt. Conoce el UID 1000, y todas sus decisiones de permisos se toman con números. Cada proceso lleva varios:

vt@labs:~
grep -E '^(Uid|Gid)' /proc/$$/status
# Uid:	1000	1000	1000	1000   real, efectivo, guardado y de filesystem

El que el kernel mira para decidir es el efectivo. Root es el UID 0 y nada más: el kernel tiene condiciones del tipo “si el UID efectivo es 0, saltá este chequeo”. sudo es un binario setuid: al ejecutarlo, su UID efectivo pasa a ser 0, revisa /etc/sudoers y deja un registro por comando. Trabajar siempre como root no deja ese registro y hace que cada typo corra con todos los privilegios.

Quiz

Comprobá lo que entendiste

0 / 3 correctas

  1. 1. Un programa hace un segfault. ¿Qué pasa con el resto del sistema?

  2. 2. ¿Cuál es el primer archivo que abre ls, antes de su main()?

  3. 3. Con strace -f, la shell lanza ls. ¿En qué orden aparecen las syscalls?

Lo que practicás en este lab

Llevátelo a tu repo si querés, pero no es obligatorio: es tu aprendizaje.

  • Resumen de strace -c sobre ls con las syscalls que más se repiten
  • El primer archivo que abre ls antes de llegar a main(), con la línea de strace que lo muestra
  • Traza de bash -c lanzando un comando, con el clone, el execve y el wait4 marcados
  • Writeup de tres líneas: dónde termina user space y por qué importa

Reto

Elegí un comando que uses todos los días (cat, grep, curl). Corré strace -c y strace -e trace=openat sobre él. Explicá en tres líneas qué archivos abrió que no esperabas y qué syscalls dominan.

Resolvelo y escribí dos líneas explicando qué pasó. Con eso lo fijás.

¿Hiciste el lab?

Si querés, guardá lo que hiciste (comandos, notas, un repo) para volver después. Y si encontrás un error o querés mejorar este lab,contribuí al repo. El progreso se guarda solo en tu navegador.