
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**
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.
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
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.
A base que extraímos da entrada PGD (0x6713000) aponta para o PUD. Mesmo processo, próximo índice: 0x1f8.```
entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0
#```
(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.
Base: 0x66ac000. Índice PMD: 0x44.```
entry = 0x66ac000 + 0x44 * 8 = 0x66ac220
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.
Base: 0x66c4000. Índice PT: 0x185.```
entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28
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.
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
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.
Combine o quadro de página física com o deslocamento do endereço virtual original:``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90
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
INPUT:```
FLAG{p4g3_t4bl3_w4lk3r}

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

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.
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}"
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
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.
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.