9.5 KiB
La memoria de un proceso y el sistema operativo
Memoria virtual
Cada proceso tiene su propio espacio de direcciones virtual. Las direcciones que guarda un puntero no son direcciones de la RAM física: son virtuales.
- La MMU, un componente del procesador, traduce cada dirección virtual a una física en cada acceso, usando las tablas de páginas del proceso.
- Esas tablas las crea y mantiene el núcleo (kernel), y un proceso no puede modificarlas.
- La memoria se gestiona en páginas de 4 KB. Cada página tiene permisos: lectura, escritura, ejecución, y si es accesible desde modo usuario o solo desde el núcleo.
Consecuencia: la misma dirección, por ejemplo 0x7ff6a000, en dos procesos
distintos apunta a sitios físicos diferentes. Un proceso no puede ni
siquiera nombrar la memoria de otro.
Qué pasa al acceder a una dirección no válida
- La MMU no encuentra la traducción, o los permisos no lo permiten (por ejemplo, escribir en una página de solo lectura).
- El procesador genera una excepción de fallo de página y cede el control al núcleo.
- El núcleo decide: o era legítimo (una página que tenía en disco, por
ejemplo), o es un error y mata el proceso.
- Windows:
Access violation(código0xC0000005). - Linux: señal
SIGSEGV(Segmentation fault).
- Windows:
Así fallaría escribir en "1.0.0": está en una página de solo lectura.
Modo usuario y modo núcleo
- Tu programa se ejecuta en modo usuario (ring 3). Hay instrucciones que no puede ejecutar, y no puede acceder a las páginas del núcleo ni al hardware directamente.
- El núcleo se ejecuta en modo núcleo (ring 0), con acceso a todo.
- Para cualquier cosa fuera de su memoria (ficheros, red, USB, otros procesos), el programa hace una llamada al sistema (syscall): pide al núcleo que lo haga por él, y el núcleo comprueba permisos.
Por eso los transportes del proyecto (Winsock, spooler, WinUSB, sockets POSIX) son todos APIs del sistema: ningún programa en modo usuario habla directamente con la tarjeta de red ni con el USB.
Leer la memoria de otro proceso
Solo a través del núcleo y con permisos:
- Windows:
OpenProcessconPROCESS_VM_READy despuésReadProcessMemory. Se permite sobre procesos del mismo usuario; para procesos del sistema hacen falta permisos de administrador (SeDebugPrivilege). - Linux:
ptrace,process_vm_readvo/proc/<pid>/mem. Hace falta el mismo usuario, y además lo limitaptrace_scope; conrootoCAP_SYS_PTRACE, de cualquiera.
Así funcionan los depuradores como gdb.
Leer la memoria física o la del núcleo
Solo desde código que se ejecute en modo núcleo, es decir, un driver. En
Windows, los drivers tienen que estar firmados. En Linux, /dev/mem está muy
restringido. Los fallos de CPU como Meltdown (2018) fueron graves justo porque
permitían saltarse esa barrera.
El peligro real en C: tu propia memoria
C no impide que un puntero lea cualquier parte de tu propio proceso. Salirse de un array no provoca un fallo si la página de al lado está mapeada: lee o machaca en silencio otros datos tuyos. Es comportamiento indefinido.
- Heartbleed (OpenSSL, 2014) era exactamente esto: una lectura fuera de los límites que devolvía al atacante memoria del propio servidor, con claves y contraseñas incluidas.
Cómo actúa el software malicioso
Casi nunca rompe el aislamiento de memoria. Las vías habituales son:
- Usar permisos legítimos. Un programa que ejecutas se ejecuta con tus permisos: puede leer, cifrar o enviar tus ficheros con las APIs normales. El ransomware no necesita ningún truco de memoria. El sistema operativo aísla unos procesos de otros y al núcleo de los usuarios, pero no protege tus ficheros de un programa que tú mismo has lanzado.
- Explotar fallos de memoria en programas legítimos. Un servidor escrito en C recibe datos preparados por el atacante que desbordan un buffer, y con eso toma el control de ese proceso. Aquí es donde la falta de comprobaciones de C importa. Según Microsoft y Google, en torno al 70 % de sus vulnerabilidades graves son fallos de seguridad de memoria.
- Escalar privilegios. Aprovechar un fallo en el núcleo o en un driver para pasar de usuario a administrador o a modo núcleo.
Defensas que usan el procesador y el sistema operativo:
- DEP/NX: las páginas de datos no son ejecutables.
- ASLR: el sistema coloca el código y los datos en direcciones aleatorias en cada ejecución.
- Canarios de pila (
-fstack-protector): el compilador detecta si alguien ha sobrescrito la pila.
Estas defensas dificultan el ataque, pero no corrigen el fallo de fondo.
Para la librería: tratará datos que no controla, como las respuestas de la
impresora (DLE EOT, GS I) o el texto que llegue desde Go. Toda longitud
que venga de fuera se comprueba antes de copiar nada.
Drivers del núcleo frente a la librería del proyecto
"Driver" es una palabra con varios significados.
- Driver del núcleo: un módulo que se carga dentro del núcleo, en modo
núcleo. Habla con el hardware directamente (registros, interrupciones, DMA)
y ofrece el dispositivo a los programas de usuario de forma controlada.
Ejemplos: el de la tarjeta de red,
usbprint.sys,winusb.sys,usblpen Linux. - La librería del proyecto: código en modo usuario que conoce el protocolo de la impresora (ESC/POS) y genera y envía los bytes a través de lo que ofrecen los drivers del núcleo.
La pila completa al imprimir:
programa en Go
└─ librería escpos (modo usuario) ← lo que construimos: sabe ESC/POS
├─ LAN: Winsock / sockets ──▶ syscall ──▶ pila TCP/IP ──▶ driver de red
└─ USB: spooler / WinUSB / libusb ──▶ syscall ──▶ usbprint/winusb ──▶ controlador USB
└──── modo núcleo ────┘
Los drivers del núcleo solo saben mover bytes y no entienden ESC/POS. La librería entiende ESC/POS, pero no puede tocar el hardware.
No es un "driver de mentira": es otra capa. Los drivers de impresora de Windows que instalan los fabricantes también son, en su mayor parte, código en modo usuario que convierte el documento al lenguaje de la impresora; debajo usan los mismos drivers del núcleo. Microsoft tiene incluso un marco para escribir drivers en modo usuario (UMDF).
En Go: es como net/http. No implementa TCP, que es cosa del núcleo:
habla HTTP sobre los sockets que le da el sistema. La librería hace lo mismo
con ESC/POS.
Caso real: CrowdStrike (19 de julio de 2024)
- Qué es: Falcon, el antivirus/EDR de CrowdStrike, tiene en Windows un driver en modo núcleo que se carga al arrancar el sistema. Está en el núcleo para poder vigilar todo el sistema y para que un malware no pueda desactivarlo.
- Qué pasó: una actualización de contenido (un fichero de datos, el "Channel File 291", no código nuevo) definía 21 campos de entrada. El código del driver que lo procesaba solo proporcionaba 20. Al acceder al campo 21 hizo una lectura fuera de límites, en modo núcleo.
- Por qué fue tan grave: en modo usuario, el núcleo mata el proceso y el resto del sistema sigue funcionando. En modo núcleo no hay nadie por encima que pueda recoger el error, así que Windows se detiene entero (pantalla azul). Como el driver se carga al arrancar, el equipo caía en cada reinicio. Microsoft estimó unos 8,5 millones de equipos afectados: aerolíneas, hospitales, bancos.
- Arreglo: manual, equipo por equipo: arrancar en modo seguro y borrar el fichero. Con BitLocker hacía falta además la clave de recuperación de cada disco.
- Fallos encadenados:
- El driver confió en que los datos de fuera tenían el tamaño esperado.
- El validador de contenido tenía su propio fallo y dio el fichero por bueno.
- La actualización se envió a todos los clientes a la vez, sin despliegue progresivo.
- Consecuencia: Microsoft anunció después planes para que los antivirus puedan funcionar fuera del núcleo.
En Go, ese acceso habría sido un panic: index out of range en un
proceso. En C dentro del núcleo, millones de pantallas azules.
Para el proyecto: todo lo que haremos (Winsock, spooler, WinUSB, libusb) se ejecuta en modo usuario. Un fallo en la librería tumba el programa que la usa, no la máquina.
cgo: Go y C comparten proceso
Con cgo, la librería en C se ejecuta dentro del mismo proceso que el programa en Go, en el mismo espacio de direcciones. Las comprobaciones de Go solo protegen el código Go: el código C puede escribir en cualquier parte del proceso, incluidas la memoria de Go y las estructuras de su runtime.
- Un desbordamiento en la librería puede corromper datos de Go y provocar un fallo mucho más tarde, en código Go correcto, lejos de la causa.
- Por LAN, cualquier equipo de la red puede hacerse pasar por la impresora en
IP:9100y enviar respuestas preparadas. Si la librería las copia sin comprobar la longitud, ese equipo puede tomar el control del programa en Go.
La garantía de seguridad de memoria de Go termina en la frontera de cgo.
En Go: el modelo de procesos es el mismo, porque lo impone el sistema
operativo y no el lenguaje. La diferencia es que Go comprueba los límites de
cada slice y cada puntero nil, y provoca un panic antes del acceso
indebido. C no comprueba nada; para detectar estos errores están
AddressSanitizer y valgrind.