Files
printer-driver/docs/03-process-memory.md
T
2026-09-30 10:22:42 +02:00

201 lines
9.5 KiB
Markdown

# 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
1. La MMU no encuentra la traducción, o los permisos no lo permiten (por
ejemplo, escribir en una página de solo lectura).
2. El procesador genera una excepción de fallo de página y cede el control al
núcleo.
3. 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ódigo `0xC0000005`).
- Linux: señal `SIGSEGV` (`Segmentation fault`).
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**: `OpenProcess` con `PROCESS_VM_READ` y después
`ReadProcessMemory`. Se permite sobre procesos del mismo usuario; para
procesos del sistema hacen falta permisos de administrador
(`SeDebugPrivilege`).
- **Linux**: `ptrace`, `process_vm_readv` o `/proc/<pid>/mem`. Hace falta el
mismo usuario, y además lo limita `ptrace_scope`; con `root` o
`CAP_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:
1. **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.
2. **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.
3. **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`, `usblp` en
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**:
1. El driver confió en que los datos de fuera tenían el tamaño esperado.
2. El validador de contenido tenía su propio fallo y dio el fichero por
bueno.
3. 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:9100` y 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.