
Navegue manualmente pelas tabelas de páginas x86-64 no qemu e gdb. Decomponha um endereço virtual, siga o cr3 por todos os níveis da memória física e extraia uma flag de bytes brutos.
Você já leu sobre paginação. Os diagramas fazem sentido. Quatro níveis, 9 bits cada, quadro de página, deslocamento. Claro. Mas então você encontra um desafio que exige realmente percorrer tabelas de páginas, e você percebe que não sabe isso. Você sabe sobre isso. Grande diferença.
O que funcionou para mim foi sentar na frente do QEMU e do gdb e fazer a caminhada eu mesmo: calcular cada índice, ler cada entrada da memória física, seguir cada ponteiro manualmente. Uma tarde disso pode ensinar mais do que horas de palestras.
Esta é uma coleção das minhas anotações desse processo. Se você ainda não tem o lado conceitual, assista à palestra de Zardus sobre gerenciamento de memória do kernel primeiro. Essa é a teoria. Este é o laboratório.
O objetivo: pegar um endereço virtual e persegui-lo através da memória física bruta até encontrarmos os dados. Sem assistentes do kernel. Sem abstrações. Apenas uma VM QEMU, gdb e memória física bruta.
Ao final, paginação não será algo sobre o qual você leu, será algo que você conhece porque fez manualmente.
Um kernel e initramfs pré-construídos estão incluídos, eu executei isso no Fedora, mas qualquer SO que execute QEMU e gdb deve funcionar. Instale-os com seu gerenciador de pacotes:```
sudo apt install qemu-system-x86 gdb
sudo dnf install qemu-system-x86 gdb
brew install qemu gdb
### O binário do desafio
O alvo é um programa C trivial que armazena uma flag na memória e imprime seu endereço 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)
{
}
}
O loop ocupado é intencional. Originalmente eu usei pause(), mas isso coloca o processo para dormir em uma chamada de sistema: quando o gdb para a VM, a CPU provavelmente está executando a tarefa ociosa com um CR3 diferente. Um loop giratório mantém o processo na CPU, então a parada garante que você está em seu contexto com as tabelas de páginas corretas.
Um initramfs pré-construído com este binário já está incluído em initramfs.cpio.gz. Se você precisar reconstruí-lo (somente Linux, requer busybox e glibc-static), execute make neste diretório.
./start.sh
O script inicializa o kernel e o initramfs empacotados sob QEMU com `-s`
(servidor gdb em `localhost:1234`) e `nokaslr` para que os endereços do kernel permaneçam
fixos entre execuções.
A VM inicializa imediatamente e o binário do desafio é executado. Você verá o endereço virtual
da flag impresso no console.```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.
Anote esse endereço virtual. Esse é o seu alvo.

A tecla de escape padrão do QEMU é
Ctrl-a, mas isso colide com meu prefixo do tmux, então o script usa-echr 0x11para remapeá-la paraCtrl-q. Se você usaCtrl-qpara outra coisa, altere o valor hexadecimal emstart.shpara adequar à sua configuração.
Em um segundo terminal:``` gdb -ex "target remote :1234"

---
## Decompondo o endereço virtual
Você tem um endereço virtual. Mas onde estão os dados, _realmente_?
Os endereços virtuais são a ficção educada do sistema operacional. Cada processo
pensa que possui sua própria memória privada começando do zero. Na realidade, os dados
vivem em algum local completamente não relacionado na RAM física. A tabela de páginas é
o mapa entre os dois: uma estrutura em árvore que a CPU percorre em cada
acesso à memória (ou consulta em seu cache TLB).
Então, vamos fazer o que a CPU faz. Manualmente. Para traduzir esse endereço, precisamos
decompô-lo nos índices que a CPU usa em cada nível.
Um endereço virtual x86-64 tem 48 bits de largura. Esses 48 bits são divididos em 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 seleciona uma das 512 entradas em uma tabela de páginas nesse nível. O deslocamento de 12 bits seleciona um byte dentro da página final de 4 KB (0x1000).
Para extrair os índices, desloque e mascare:``` PGD index = (VA >> 39) & 0x1FF PUD index = (VA >> 30) & 0x1FF PMD index = (VA >> 21) & 0x1FF PT index = (VA >> 12) & 0x1FF Offset = VA & 0xFFF
Em gdb, você pode calcular estes diretamente:```
(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
Anote estes. Você usará cada um no seu nível correspondente.
Seus valores serão diferentes. O endereço
0x7ffe08985c90é apenas um exemplo. Use o endereço que seu binário do desafio imprimir.
Uma nota sobre paginação de 5 níveis. CPUs e kernels recentes suportam LA57, que adiciona um quinto nível (PML5) acima do PGD e estende os endereços virtuais para 57 bits. A navegação segue o mesmo padrão: mais um índice de 9 bits, mais uma consulta de tabela. A maioria dos sistemas ainda usa paginação de 4 níveis. Você pode verificar o seu:
cat /proc/cpuinfo | grep la57. Tudo neste artigo assume 4 níveis.
Toda árvore tem uma raiz. Para tabelas de página, essa raiz está no registrador CR3: ele armazena o endereço físico da tabela de nível superior, o PGD. Cada processo tem seu próprio valor de CR3; o kernel o troca na troca de contexto.
Este é nosso ponto de entrada na navegação. Leia-o a partir do gdb:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]
A base da tabela de páginas é `0x66c7000`. Os 12 bits baixos são PCID/flags (zero aqui),
então o endereço base é o valor como está.
Aqui é onde a caminhada começa.
---
## A caminhada
Aqui está o truque: cada nível
segue o mesmo padrão. As flags variam ligeiramente entre os níveis, mas o
processo não. O padrão:
1. **Calcular o endereço da entrada:** `base + index * 8` (cada entrada tem 8 bytes)
2. **Ler a entrada da memória física** usando o comando `xp` do monitor QEMU
3. **Decodificar as flags** (veja a referência abaixo). Se Presente (bit 0) for 0, a página não está mapeada e a caminhada para
4. **Extrair a base da próxima tabela:** mascarar a entrada com `& 0x000FFFFFFFFFF000`
5. **Ir para o próximo nível**