Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
page_table_walk — 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. | Kitploit
Ferramentas/GitHubGitHub/jazho76/page_table_walk
Forensia de MemóriaEngenharia ReversaDepuradoresCTFAnálise de BináriosAprendizado e EducaçãoLabs e Prática
GitHubjazho76/page_table_walk

page_table_walk

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.

Ver Repositório
281há 5 mesesRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Capture the Flag na Memória Física

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.


Configurando o laboratório

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:```

Ubuntu/Debian

sudo apt install qemu-system-x86 gdb

Fedora/RHEL

sudo dnf install qemu-system-x86 gdb

macOS

brew install qemu gdb

root@kitploit:~
### 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.

Iniciando o QEMU```

./start.sh

root@kitploit:~
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.

Terminal do QEMU após a inicialização, mostrando a saída do binário do desafio com o endereço da flag e PID

A tecla de escape padrão do QEMU é Ctrl-a, mas isso colide com meu prefixo do tmux, então o script usa -echr 0x11 para remapeá-la para Ctrl-q. Se você usa Ctrl-q para outra coisa, altere o valor hexadecimal em start.sh para adequar à sua configuração.

Anexando o gdb

Em um segundo terminal:``` gdb -ex "target remote :1234"

root@kitploit:~
![gdb terminal após attach, interrompido e pronto](https://assets.kitploit.com/production/public/readmes/12435/9fb13e082596378677ea0d42cf7da3a86706df44127860fc724b2bad0a9f138c.png)

---

## 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

root@kitploit:~
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.


Encontrando CR3: a raiz da árvore

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 ]

root@kitploit:~
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**

Cada entrada tem 64 bits. Os bits de flag comuns:```
Bit   Name              Meaning when set
 0    Present           Page/table is mapped
 1    Read/Write        Writable
 2    User/Supervisor   Accessible from userspace
 3    Write-Through     Write-through caching
 4    Cache Disable     Caching disabled
 5    Accessed          CPU has read this entry
 6    Dirty             CPU has written to the page (final level only)
 7    Page Size         1 GB page (PUD) or 2 MB page (PMD)
63    NX                No-execute

Bits [51:12] contêm o endereço físico da próxima tabela (ou o quadro de página no nível final). Bits 9-11 são ignorados pelo hardware e disponíveis para uso do SO. O Linux os usa para contabilidade (rastreamento soft-dirty, por exemplo). Bits 52-62 são reservados. Você encontrará ambos ao ler PTEs em writeups de exploits.

Mantenha esta tabela de flags à mão enquanto caminha.

Vamos.

Nível 4: PGD (Page Global Directory)

Temos a base do PGD a partir de CR3: 0x66c7000. Nosso índice PGD é 0xff.

Calcule o endereço da entrada:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8

root@kitploit:~
Leia-o do gdb usando o comando de exame de memória física do QEMU:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067

Entrada: 0x6713067 [Presente RW Usuário Acessado Sujo].

Próxima base: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.

Nível 3: PUD (Diretório Superior de Páginas)

A base que extraímos da entrada PGD (0x6713000) aponta para o PUD. Mesmo processo, próximo índice: 0x1f8.``` entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0

root@kitploit:~
#```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067

Entrada: 0x66ac067 [Present RW User Accessed Dirty]. Page Size (bit 7) = 0, not a 1 GB huge page.

Next base: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000.

Nível 2: PMD (Page Middle Directory)

Base: 0x66ac000. Índice PMD: 0x44.``` entry = 0x66ac000 + 0x44 * 8 = 0x66ac220

root@kitploit:~
ENTRADA:```
(gdb) monitor xp/1gx 0x66ac220
000000066ac220: 0x00000000066c4067

Entrada: 0x66c4067 [Present RW User Accessed Dirty]. Tamanho da página (bit 7) = 0, não é uma página enorme de 2 MB.

Próxima base: 0x66c4067 & 0x000FFFFFFFFFF000 = 0x66c4000.

Nível 1: PT (Tabela de Páginas)

Base: 0x66c4000. Índice PT: 0x185.``` entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28

root@kitploit:~
Após análise, não há texto natural para traduzir. O conteúdo fornecido consiste apenas no rótulo "INPUT:" sem qualquer texto adicional. Portanto, não há saída.```
(gdb) monitor xp/1gx 0x66c4c28
000000066c4c28: 0x80000000037fd867

Entry: 0x80000000037fd867 [Present RW User Accessed Dirty NX]. This is the final PTE.

Physical page frame: 0x80000000037fd867 & 0x000FFFFFFFFFF000 = 0x37fd000.

E se Present = 0?

Vamos ver o que acontece quando a walk encontra uma página não mapeada. Escolha um endereço que quase certamente não está mapeado, algo no meio do espaço de endereçamento:``` (gdb) p/x (0x0000414141414000 >> 39) & 0x1ff $1 = 0x82

root@kitploit:~
Restrinja o acesso ao manipulador de webhook (por exemplo, por endereço IP ou usando tokens de autenticação).```
(gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8
00000000066c7410: 0x0000000000000000

Todos zeros. O bit 0 (Presente) está limpo. A caminhada para aqui. Não há PUD, nem PMD, nem PT, nem quadro de página. Este endereço não mapeia para a memória física.

Se a CPU encontrasse isso durante a execução normal, levantaria uma falha de página (interrupção 14). O manipulador de falhas do kernel então decidiria o que fazer: carregar a página do disco (swap), alocar uma nova página (paginação por demanda) ou matar o processo com um segfault.

O ponto: a tabela de páginas não é apenas uma estrutura de tradução. É também o mecanismo que torna a memória virtual virtual. Nem todo endereço precisa ter memória física por trás. A CPU descobre isso durante a caminhada, um nível de cada vez.


A revelação

Combine o quadro de página física com o deslocamento do endereço virtual original:``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90

root@kitploit:~
Agora leia:```
(gdb) monitor xp/6bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70

Isso é F, L, A, G, {, p: o início da nossa flag. Leia mais:``` (gdb) monitor xp/24bx 0x37fdc90 00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70 0x34 0x67 00000000037fdc98: 0x33 0x5f 0x74 0x34 0x62 0x6c 0x33 0x5f 00000000037fdca0: 0x77 0x34 0x6c 0x6b 0x33 0x72 0x7d 0x00

root@kitploit:~
INPUT:```
FLAG{p4g3_t4bl3_w4lk3r}

gdb mostrando a caminhada completa da tabela de páginas desde CR3 até a flag

Lá está. Você acabou de fazer o que a CPU faz bilhões de vezes por segundo, mas você fez manualmente, lendo bytes brutos da memória física. Quatro tabelas de profundidade, nada oculto por trás de uma abstração.

Antes, paginação era um diagrama em um slide. Agora é uma sequência de leituras que você pode repetir mentalmente: base, índice, deslocamento, máscara, seguir. Essa diferença importa quando você está analisando uma exploração de kernel e precisa raciocinar sobre o que uma gravação em um PTE realmente faz.

Você pode verificar seu resultado com o comando gva2gpa (endereço virtual convidado para endereço físico convidado) do monitor QEMU, que faz a caminhada internamente:``` (qemu) gva2gpa 0x7ffe08985c90 gpa: 0x37fdc90

root@kitploit:~
## Flags e permissões

Decodificamos flags em todos os níveis durante a caminhada, mas ignoramos o que elas significam para a segurança. Veja o PTE final:```
0x80000000037fd867

Podemos ver que o cookie foi agora eliminado.``` Bit 0 (Present) = 1 Page is in physical memory Bit 1 (Read/Write) = 1 Page is writable Bit 2 (User/Supervisor)= 1 Accessible from user mode Bit 3 (Write-Through) = 0 Write-back caching Bit 4 (Cache Disable) = 0 Caching enabled Bit 5 (Accessed) = 1 CPU has read this page Bit 6 (Dirty) = 1 CPU has written to this page Bit 7 (Page Size) = 0 4 KB page (not huge) Bit 63 (NX) = 1 No-Execute: cannot run code from this page

root@kitploit:~
Isso faz sentido: o segredo é uma variável de pilha. A pilha é legível, gravável e suja (foi escrita). Está marcada como não executável porque sistemas modernos impõem W^X: uma página que é gravável não deve ser executável.

As flags em cada nível são combinadas com AND pelo hardware. Se a entrada PUD tiver User=0, nada abaixo dela é acessível pelo usuário, independentemente do que a PTE disser. A permissão mais restritiva vence.

---

## A TLB: quando a CPU pula a caminhada

Quatro leituras de memória apenas para acessar um byte. Isso é caro. A CPU não caminha pela tabela de páginas em todo acesso à memória. Ela armazena em cache o resultado em um **Translation Lookaside Buffer (TLB)**.

Após o primeiro acesso ao endereço virtual da nossa flag, a CPU armazena o mapeamento `0x7ffe08985c90 -> 0x37fdc90` (aproximadamente) na TLB. Acessos subsequentes acertam o cache e pulam a caminhada completamente. A tabela de páginas permanece intocada na RAM.

Isso é transparente para código normal. Mas importa no momento em que você _modifica_ uma entrada da tabela de páginas. Se você escrever um novo endereço físico em uma PTE, a CPU não percebe: a TLB ainda mantém o mapeamento antigo. Você tem que liberá-la explicitamente.

O kernel faz isso com a instrução `invlpg`, que invalida a entrada da TLB para um único endereço virtual. Chame `mprotect` do espaço do usuário e é isso que acontece nos bastidores: o kernel atualiza as flags da PTE, depois libera a TLB para que a CPU pegue as novas permissões.

Isso tem implicações diretas de segurança. Em um exploit de kernel, se você conseguir escrever em uma PTE (por exemplo, limpando o bit NX para tornar a pilha executável), você também precisa que a TLB seja liberada antes que a CPU honre a mudança. Às vezes o kernel faz isso para você como efeito colateral do caminho de código que você acionou. Às vezes você precisa organizar isso você mesmo. De qualquer forma, você precisa saber que a TLB existe, ou seu exploit funciona na teoria mas não na prática.

---

## Grandes páginas: quando a caminhada termina cedo

No passo a passo acima, passamos por todos os quatro níveis. Mas a caminhada pode terminar cedo se um bit de Page Size (bit 7) estiver definido.

**No Nível 3 (PUD):** Se o bit 7 estiver definido, a entrada mapeia uma página de 1 GB diretamente. O endereço físico é obtido da entrada, e os bits [29:0] do endereço virtual se tornam o deslocamento (30 bits = 1 GB).

**No Nível 2 (PMD):** Se o bit 7 estiver definido, a entrada mapeia uma página de 2 MB. Os bits [20:0] do endereço virtual se tornam o deslocamento (21 bits = 2 MB).

Você verá frequentemente grandes páginas em mapeamentos do kernel. A região de mapeamento direto do kernel (`0xffff888000000000` na maioria dos kernels de 64 bits) usa frequentemente páginas de 2 MB ou 1 GB para reduzir a pressão na TLB.

Se você encontrar uma grande página durante sua caminhada, a fórmula muda:```
2 MB page:  phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF)
1 GB page:  phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)

Automatizando a caminhada

Agora que conhecemos o processo, vamos codificá-lo. pagewalk.py é um script Python para gdb que faz a mesma caminhada que acabamos de fazer. A lógica principal cabe em uma função:```python ADDR_MASK = 0x000FFFFFFFFFF000

def read_phys(addr): """Read a 64-bit value from guest physical memory via QEMU monitor.""" result = gdb.execute(f"monitor xp/1gx {addr:#x}", to_string=True) return int(result.strip().split(":")[1].strip(), 16)

def pagewalk(va): cr3 = int(gdb.parse_and_eval("$cr3")) pgd_base = cr3 & ADDR_MASK

root@kitploit:~
# Decompose the virtual address
pgd_idx = (va >> 39) & 0x1FF
pud_idx = (va >> 30) & 0x1FF
pmd_idx = (va >> 21) & 0x1FF
pt_idx  = (va >> 12) & 0x1FF
offset  =  va        & 0xFFF

# Walk: each level is the same pattern
pgd_entry = read_phys(pgd_base + pgd_idx * 8)
if not (pgd_entry & 1): return None       # Not present
pud_base = pgd_entry & ADDR_MASK

pud_entry = read_phys(pud_base + pud_idx * 8)
if not (pud_entry & 1): return None
if pud_entry & (1 << 7):                   # 1 GB huge page
    return (pud_entry & 0x000FFFFFC0000000) | (va & 0x3FFFFFFF)
pmd_base = pud_entry & ADDR_MASK

pmd_entry = read_phys(pmd_base + pmd_idx * 8)
if not (pmd_entry & 1): return None
if pmd_entry & (1 << 7):                   # 2 MB huge page
    return (pmd_entry & 0x000FFFFFFFE00000) | (va & 0x1FFFFF)
pt_base = pmd_entry & ADDR_MASK

pt_entry = read_phys(pt_base + pt_idx * 8)
if not (pt_entry & 1): return None

return (pt_entry & ADDR_MASK) | offset
root@kitploit:~
O script completo (com decodificação de flag e saída formatada) está em `pagewalk.py`.
Utilize-o para verificar seu trabalho manual, ou para explorar outros endereços:```
(gdb) source ./pagewalk.py
Page walk command loaded. Usage: pagewalk <virtual-address>
(gdb) pagewalk 0x7ffe08985c90
Decoded Virtual Address:
  PGD=0x0ff
  PUD=0x1f8
  PMD=0x044
  PT=0x185
  Offset=0xc90

CR3: 0x00000000066c7000
PGD[0x0ff]:  0x0000000006713067  [Present RW User Accessed Dirty]
PUD[0x1f8]:  0x00000000066ac067  [Present RW User Accessed Dirty]
PMD[0x044]:  0x00000000066c4067  [Present RW User Accessed Dirty]
PT[0x185]:   0x80000000037fd867  [Present RW User Accessed Dirty NX]

Physical address: 0x00000000037fdc90

gdb mostrando a saída do pagewalk.py com a caminhada completa da tabela de páginas

Observe como o script verifica páginas enormes nos níveis PUD e PMD antes de continuar a caminhada. Essa é a mesma lógica que discutimos na seção de páginas enormes: se PageSize (bit 7) estiver definido, a caminhada termina mais cedo e o deslocamento é mais amplo.

Tente caminhar pelo endereço de uma função: você verá que o bit NX está limpo (o código deve ser executável). Tente uma seção de dados somente leitura: você verá que R/W está limpo.


Lendo memória física sem QEMU

Ao longo deste exercício, usamos monitor xp para ler memória física diretamente. Isso funciona porque o monitor do QEMU está fora da VM e pode acessar o espaço de endereço físico do convidado. Em um exploit real, você não tem esse luxo.

O kernel resolve isso para si mesmo com a região de mapeamento direto: um mapeamento virtual contíguo de toda a RAM física. No x86-64, esta região convencionalmente começa em 0xffff888000000000, mas com KASLR habilitado a base é aleatória. O kernel armazena a base real em um símbolo chamado page_offset_base.

Inicializamos com nokaslr, então a base está em seu padrão. Vamos confirmar:``` (gdb) x/s 0xffff888000000000 + 0x37fdc90 0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"

root@kitploit:~
Mesma memória física, acessada através de um endereço virtual do kernel. É assim que o próprio kernel lê memória física arbitrária: `phys_to_virt()` é apenas `page_offset_base + phys_addr`.

Esta é também a razão pela qual explorações do kernel se preocupam em vazar `page_offset_base`. Se o KASLR estiver habilitado, você não sabe onde o mapa direto começa, então não pode converter endereços físicos para endereços virtuais do kernel. Vaze a base, e você pode ler ou escrever qualquer endereço físico através do mapa direto, incluindo as próprias entradas da tabela de páginas.

---

## Encontrando as tabelas de página de outro processo

Nossa configuração garantiu que CR3 apontava para as tabelas de página do binário desafio quando o gdb pausou a VM. Mas e se você precisar percorrer as tabelas de página de um processo _diferente_?

O valor de CR3 de cada processo é armazenado em seu `task_struct`. O caminho é:```
task_struct -> mm_struct -> pgd -> physical page

Em gdb com símbolos do kernel, você pode encontrar o task_struct do init (PID 1) e extrair sua raiz da tabela de páginas:``` (gdb) p/x init_task.mm->pgd $1 = 0xffff8880066c7000

root@kitploit:~
Essa é um endereço virtual do kernel no mapa direto. Remova a base para obter o endereço físico:```
0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000

Esse é o mesmo CR3 com o qual começamos, o que faz sentido: nosso binário de desafio é o PID 1 neste initramfs mínimo.

Para outros processos, você percorreria a lista de tarefas (lista ligada init_task.tasks), encontraria o alvo e extrairia seu mm->pgd da mesma forma. Cada processo tem sua própria árvore de tabelas de páginas enraizada em seu próprio CR3. O kernel troca o CR3 a cada troca de contexto, dando a cada processo a ilusão de memória privada.


O que isso significa

Se você chegou até aqui fazendo a caminhada de verdade (não apenas lendo), agora tem algo que nenhum diagrama pode dar: intuição de como a memória realmente funciona no nível de hardware. É aqui que essa intuição compensa:

ASLR randomiza o endereço virtual, não a caminhada. A estrutura da tabela de páginas é sempre a mesma: quatro níveis, 512 entradas cada, mesmo layout de bits. ASLR muda quais índices você calculará, mas o processo é idêntico.

W^X é imposto na tabela de páginas. O bit R/W e o bit NX na PTE são o que fazem mprotect funcionar. Quando um exploit tenta executar shellcode na pilha, a CPU verifica o bit NX durante a tradução e gera uma falha.

SMEP e SMAP verificam o bit de Usuário. A Prevenção de Execução/Acesso em Modo Supervisor verifica o bit Usuário/Supervisor em todos os níveis da tabela de páginas. Se alguma entrada marca o endereço como modo usuário e o código do kernel tenta executá-lo ou acessá-lo, a CPU gera uma falha. É por isso que exploits modernos do kernel não podem simplesmente pular para shellcode no espaço do usuário.

Exploits do kernel frequentemente miram diretamente nas tabelas de páginas. Se você pode escrever em uma PTE, pode alterar a que memória física um endereço virtual mapeia, alterar permissões ou remapear memória do kernel como acessível pelo usuário. Entender a caminhada é entender a superfície de ataque.

KPTI divide a tabela de páginas em duas. Em vez de um conjunto de tabelas de páginas por processo, agora há dois: um para modo usuário (com quase todas as páginas do kernel desmapeadas) e um para modo kernel (com tudo). O kernel troca o CR3 em cada entrada e saída de syscall. Você pode observar isso: pare a VM enquanto está no espaço do usuário e leia o CR3, depois defina um breakpoint em uma entrada de syscall e leia o CR3 novamente. Eles serão diferentes. A tabela de páginas do modo usuário simplesmente não contém entradas para a memória do kernel, então não há nada a vazar mesmo que a caminhada seja concluída.

Na próxima vez que um artigo sobre exploit do kernel mencionar "remapeamento de tabelas de páginas", não será abstrato. Você saberá exatamente quais bytes eles estão falando, porque você mesmo os leu.


Leitura adicional

  • Understanding Paging: o tutorial que inspirou este. Este artigo tenta levar a exploração um pouco mais longe, mas foi por onde comecei.
  • Intel SDM, Volume 3A, Capítulo 4: "Paging": a referência autoritativa (surpreendentemente legível depois de fazer uma caminhada manual).
  • pwn.college: Segurança do Kernel: os desafios que me fizeram realmente explorar essas coisas, altamente recomendado.
Baixar ferramenta