
Recorre las tablas de páginas x86-64 manualmente en qemu y gdb. Descompón una dirección virtual, sigue cr3 a través de todos los niveles de memoria física y extrae una flag de bytes sin procesar.
Has leído sobre paginación. Los diagramas tienen sentido. Cuatro niveles, 9 bits cada uno, marco de página, desplazamiento. Claro. Pero luego te encuentras con un desafío que requiere caminar realmente las tablas de páginas, y te das cuenta de que no lo sabes. Sabes sobre ello. Gran diferencia.
Lo que funcionó para mí fue sentarme frente a QEMU y gdb y hacer el recorrido yo mismo: calculando cada índice, leyendo cada entrada de la memoria física, siguiendo cada puntero a mano. Una tarde de eso podría enseñar más que horas de conferencias.
Esta es una colección de mis notas de ese proceso. Si aún te falta el lado conceptual, mira la conferencia de Zardus sobre gestión de memoria del kernel primero. Esa es la teoría. Este es el laboratorio.
El objetivo: tomar una dirección virtual y perseguirla a través de la memoria física cruda hasta encontrar los datos. Sin ayudas del kernel. Sin abstracciones. Solo una máquina virtual QEMU, gdb y memoria física cruda.
Al final, la paginación no será algo sobre lo que hayas leído, será algo que sepas porque lo hiciste a mano.
Se incluyen un kernel precompilado e initramfs, ejecuté esto en Fedora, pero cualquier sistema operativo que ejecute QEMU y gdb debería funcionar. Instálalos con tu gestor de paquetes:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### El binario del desafío
El objetivo es un programa trivial en C que almacena una flag en memoria e imprime su dirección virtual:```c
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char secret[] = "FLAG{p4g3_t4bl3_w4lk3r}";
printf("secret @ %p\n", (void *)secret);
printf("pid = %d\n", getpid());
printf("Spinning. Walk the page tables to find the flag.\n");
while (1)
{
}
}
El bucle de espera activa es intencional. Originalmente usé pause(), pero eso pone el proceso a dormir en una syscall: cuando gdb detiene la VM, es probable que la CPU esté ejecutando la tarea idle con un CR3 diferente. Un bucle de giro mantiene el proceso en la CPU, por lo que detener garantiza que estés en su contexto con las tablas de páginas correctas.
Ya se incluye un initramfs preconstruido con este binario en initramfs.cpio.gz. Si necesitas reconstruirlo (solo Linux, requiere busybox y glibc-static), ejecuta make en este directorio.
./start.sh
El script arranca el kernel y el initramfs empaquetados bajo QEMU con `-s` (servidor gdb en `localhost:1234`) y `nokaslr` para que las direcciones del kernel permanezcan fijas entre ejecuciones. La VM arranca inmediatamente y se ejecuta el binario del desafío. Verás la dirección virtual de la flag impresa en la consola.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
Anota esa dirección virtual. Ese es tu objetivo.

La tecla de escape por defecto de QEMU es
Ctrl-a, pero eso choca con mi prefijo de tmux, así que el script usa-echr 0x11para reasignarlo aCtrl-q. Si usasCtrl-qpara otra cosa, cambia el valor hexadecimal enstart.shpara adaptarlo a tu configuración.
En una segunda terminal:``` gdb -ex "target remote :1234"

---
## Descomponiendo la dirección virtual
Tienes una dirección virtual. Pero ¿dónde están los datos, _en realidad_?
Las direcciones virtuales son la ficción educada del sistema operativo. Cada proceso
piensa que tiene su propia memoria privada que comienza desde cero. En realidad, los datos
se encuentran en una ubicación completamente no relacionada en la RAM física. La tabla de páginas es
el mapa entre ambas: una estructura de árbol que la CPU recorre en cada
acceso a memoria (o busca en su caché TLB).
Así que hagamos lo que hace la CPU. Manualmente. Para traducir esa dirección, necesitamos
descomponerla en los índices que la CPU utiliza en cada nivel.
Una dirección virtual x86-64 tiene 48 bits de ancho. Esos 48 bits se dividen en cinco
campos:```
63 48 47 39 38 30 29 21 20 12 11 0
┌────────┬────────┬────────┬────────┬────────┬──────────┐
│ sign │ PGD │ PUD │ PMD │ PT │ Offset │
│ extend │ index │ index │ index │ index │ │
│ (16b) │ (9b) │ (9b) │ (9b) │ (9b) │ (12b) │
└────────┴────────┴────────┴────────┴────────┴──────────┘
Cada índice de 9 bits selecciona una de las 512 entradas en una tabla de páginas en ese nivel. El desplazamiento de 12 bits selecciona un byte dentro de la página final de 4 KB (0x1000).
Para extraer los índices, desplaza y enmascara:``` PGD index = (VA >> 39) & 0x1FF PUD index = (VA >> 30) & 0x1FF PMD index = (VA >> 21) & 0x1FF PT index = (VA >> 12) & 0x1FF Offset = VA & 0xFFF
En gdb, puedes calcular estos directamente:```
(gdb) p/x (0x7ffe08985c90 >> 39) & 0x1ff
$1 = 0xff
(gdb) p/x (0x7ffe08985c90 >> 30) & 0x1ff
$2 = 0x1f8
(gdb) p/x (0x7ffe08985c90 >> 21) & 0x1ff
$3 = 0x44
(gdb) p/x (0x7ffe08985c90 >> 12) & 0x1ff
$4 = 0x185
(gdb) p/x 0x7ffe08985c90 & 0xfff
$5 = 0xc90
Anótalos. Usarás cada uno en su nivel correspondiente.
Tus valores diferirán. La dirección
0x7ffe08985c90es solo un ejemplo. Usa la dirección que haya imprimido tu binario de desafío.
Nota sobre la paginación de 5 niveles. Las CPU y kernels recientes admiten LA57, que añade un quinto nivel (PML5) por encima del PGD y extiende las direcciones virtuales a 57 bits. El recorrido sigue el mismo patrón: un índice de 9 bits más, una búsqueda en tabla más. La mayoría de los sistemas todavía usan paginación de 4 niveles. Puedes comprobarlo con:
cat /proc/cpuinfo | grep la57. Todo en este artículo asume 4 niveles.
Todo árbol tiene una raíz. Para las tablas de página, esa raíz está en el registro CR3: contiene la dirección física de la tabla de nivel superior, el PGD. Cada proceso tiene su propio valor de CR3, el kernel lo intercambia en el cambio de contexto.
Este es nuestro punto de entrada al recorrido. Lée desde gdb:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
La base de la tabla de páginas es `0x66c7000`. Los 12 bits bajos son PCID/flags (cero aquí), así que la dirección base es el valor tal cual.
Aquí es donde comienza el recorrido.
---
## El recorrido
Este es el truco: cada nivel sigue el mismo patrón. Las banderas varían ligeramente entre niveles, pero el proceso no. El patrón:
1. **Calcular la dirección de la entrada:** `base + index * 8` (cada entrada tiene 8 bytes)
2. **Leer la entrada de la memoria física** usando el comando `xp` del monitor de QEMU
3. **Decodificar las banderas** (vea la referencia a continuación). Si Present (bit 0) es 0, la página no está mapeada y el recorrido se detiene
4. **Extraer la base de la siguiente tabla:** enmascarar la entrada con `& 0x000FFFFFFFFFF000`
5. **Avanzar al siguiente nivel**