Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
page_table_walk — qemuとgdbで手動でx86-64ページテーブルをウォークする。仮想アドレスを分解し、cr3を辿って物理メモリの全レベルを通過し、生のバイトからフラグを抽出する。 | Kitploit
ツール/GitHubGitHub/jazho76/page_table_walk
メモリフォレンジックリバースエンジニアリングデバッガCTFバイナリ解析学習と教育ラボと実践
GitHubjazho76/page_table_walk

page_table_walk

qemuとgdbで手動でx86-64ページテーブルをウォークする。仮想アドレスを分解し、cr3を辿って物理メモリの全レベルを通過し、生のバイトからフラグを抽出する。

リポジトリを見る
2815ヶ月前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

物理メモリ上のフラグキャプチャ

あなたはページングについて読んだことがあります。図表は理解できる。4レベル、各9ビット、ページフレーム、オフセット。確かに。しかし、実際にページテーブルをウォークする必要があるチャレンジに直面すると、自分はそれを_知っている_わけではないことに気づきます。_について知っている_だけです。大きな違いです。

私にとって効果的だったのは、QEMUとgdbの前に座り、自分でウォークを行うことでした。すべてのインデックスを計算し、物理メモリからすべてのエントリを読み取り、すべてのポインタを手動で追跡します。そのような半日の作業は、何時間もの講義よりも多くのことを教えてくれます。

これはそのプロセスからの私のノートのコレクションです。まだ概念的な側面が不足している場合は、まずZardusのカーネルメモリ管理に関する講義をご覧ください。それが理論です。これがラボです。

目標:仮想アドレスを取得し、生の物理メモリ内を追跡してデータを見つけるまで追跡します。カーネルヘルパーなし。抽象化なし。単なるQEMU VM、gdb、そして生の物理メモリです。

最終的には、ページングは読んだだけのものではなく、自分で手を動かして行ったことで知るものになるでしょう。


ラボのセットアップ

プリビルドされたカーネルとinitramfsが含まれています。私はFedoraで実行しましたが、QEMUとgdbを実行できるOSならどれでも動作するはずです。パッケージマネージャでインストールしてください:```

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:~
### チャレンジのバイナリ

ターゲットは、フラグをメモリに保存し、その仮想アドレスを表示する簡単なCプログラムです。```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)
    {
    }
}

ビジーループは意図的なものです。当初は pause() を使用していましたが、それではプロセスが syscall 内でスリープ状態になります。gdb が VM を停止すると、CPU はおそらく別の CR3 でアイドルタスクを実行しています。スピンループはプロセスを CPU 上に留めるため、停止することで正しいページテーブルを持つコンテキストにいることが保証されます。

このバイナリを含むビルド済み initramfs は既に initramfs.cpio.gz に含まれています。それを再ビルドする必要がある場合(Linux のみ、busybox と glibc-static が必要)、このディレクトリで make を実行してください。

QEMU の起動```

./start.sh

root@kitploit:~
このスクリプトは、バンドルされたカーネルとinitramfsをQEMU上で起動し、`-s`(gdbサーバーを`localhost:1234`で待機)と`nokaslr`オプションを指定することで、実行間でカーネルアドレスが固定されるようにします。

VMは即座に起動し、チャレンジバイナリが実行されます。コンソールにフラグの仮想アドレスが表示されます。```
secret @ 0x7ffe08985c90
pid = 1
Spinning. Walk the page tables to find the flag.

Write down that virtual address. That's your target.

QEMU terminal after boot, showing the challenge binary's output with the flag address and PID

デフォルトのQEMUエスケープキーはCtrl-aですが、それは私のtmuxプレフィックスと衝突するため、スクリプトでは-echr 0x11を使用してCtrl-qに再マッピングしています。Ctrl-qを別の用途で使用している場合は、start.shの16進数値を自分の設定に合わせて変更してください。

gdbのアタッチ```

gdb -ex "target remote :1234"

root@kitploit:~
![gdb terminal after attaching, halted and ready](https://assets.kitploit.com/production/public/readmes/12435/9fb13e082596378677ea0d42cf7da3a86706df44127860fc724b2bad0a9f138c.png)

---

## 仮想アドレスの分解

仮想アドレスはあります。しかし、データは _実際には_ どこにあるのでしょうか?

仮想アドレスは、オペレーティングシステムが提供する丁寧なフィクションです。
各プロセスは、ゼロから始まる自分専用のメモリがあると考えます。
実際には、データは物理RAMのまったく無関係な場所に存在します。
ページテーブルは、両者を結びつけるマップです。CPUがメモリアクセスを行うたびに(またはTLBキャッシュから参照するたびに)辿るツリー構造です。

それでは、CPUが行うことをやってみましょう。手動で。そのアドレスを変換するには、CPUが各レベルで使用するインデックスに分解する必要があります。

x86-64の仮想アドレスは48ビット幅です。その48ビットは5つのフィールドに分割されます。```
 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)   │
└────────┴────────┴────────┴────────┴────────┴──────────┘

各9ビットのインデックスは、そのレベルのページテーブル内の512エントリのうちの1つを選択します。12ビットのオフセットは、最終的な4KB(0x1000)ページ内のバイトを選択します。

インデックスを抽出するには、シフトとマスクを行います:``` 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:~
gdb では、これらを直接計算できます:```
(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

Write these down. You'll use each one at its corresponding level.

Your values will differ. The address 0x7ffe08985c90 is just an example. Use whatever address your challenge binary printed.

A note on 5-level paging. Recent CPUs and kernels support LA57, which adds a fifth level (PML5) above the PGD and extends virtual addresses to 57 bits. The walk is the same pattern: one more 9-bit index, one more table lookup. Most systems still run 4-level paging. You can check yours: cat /proc/cpuinfo | grep la57. Everything in this article assumes 4-level.


Finding CR3: the root of the tree

Every tree has a root. For page tables, that root is in the CR3 register: it holds the physical address of the top-level table, the PGD. Every process gets its own CR3 value, the kernel swaps it on context switch.

This is our entry point into the walk. Read it from gdb:``` (gdb) info registers cr3 cr3 0x66c7000 [ PDBR=26311 PCID=0 ]

root@kitploit:~
ページテーブルのベースは `0x66c7000` です。下位12ビットはPCID/フラグ(ここではゼロ)なので、ベースアドレスはそのままの値です。

ここからウォークが始まります。

---

## ウォーク

ここがポイントです。各レベルは同じパターンに従います。フラグはレベルによって若干異なりますが、プロセスは変わりません。そのパターンは次の通りです:

1. **エントリアドレスの計算:** `base + index * 8` (各エントリは8バイト)
2. **物理メモリからのエントリの読み取り** QEMUモニターの `xp` コマンドを使用
3. **フラグのデコード** (以下のリファレンスを参照)。Present (ビット0) が0の場合、ページはマッピングされておらず、ウォークは停止します
4. **次のテーブルのベースを抽出:** エントリを `& 0x000FFFFFFFFFF000` でマスク
5. **次のレベルに移動**

各エントリは64ビットです。一般的なフラグビット:```
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

ビット[51:12]は次のテーブル(または最終レベルのページフレーム)の物理アドレスを保持します。ビット9-11はハードウェアによって無視され、OSが使用可能です。Linuxはそれらをブックキーピング(例えばソフトダーティ追跡)に使用します。ビット52-62は予約されています。エクスプロイトの書き込みでPTEを読む際に両方に遭遇します。

進む際にこのフラグテーブルを手元に置いておいてください。

それでは始めましょう。

レベル4: PGD(ページグローバルディレクトリ)

CR3からPGDベースがあります: 0x66c7000。 PGDインデックスは 0xff です。

エントリアドレスを計算します:``` entry = 0x66c7000 + 0xff * 8 = 0x66c77f8

root@kitploit:~
QEMUの物理メモリ参照コマンドを使用してgdbから読み取ります:```
(gdb) monitor xp/1gx 0x66c77f8
000000066c77f8: 0x0000000006713067

エントリ: 0x6713067 [Present RW User Accessed Dirty].

次のベース: 0x6713067 & 0x000FFFFFFFFFF000 = 0x6713000.

レベル3: PUD (Page Upper Directory)

PGDエントリから抽出したベース (0x6713000) はPUDを指します。同じプロセスで、次のインデックス: 0x1f8.``` entry = 0x6713000 + 0x1f8 * 8 = 0x6713fc0

root@kitploit:~
No content provided.```
(gdb) monitor xp/1gx 0x6713fc0
00000006713fc0: 0x00000000066ac067

エントリ: 0x66ac067 [Present RW User Accessed Dirty]。ページサイズ (ビット7) = 0、1GB 巨大ページではありません。

次のベース: 0x66ac067 & 0x000FFFFFFFFFF000 = 0x66ac000。

レベル2: PMD (Page Middle Directory)

ベース: 0x66ac000。PMD インデックス: 0x44。``` entry = 0x66ac000 + 0x44 * 8 = 0x66ac220

root@kitploit:~
- [unlicense](https://github.com/PurpleBooth/functional-login-system-benhammondmusic) - Node.js、Express、Pug、MongoDB、Passport で書かれたログインシステム。```
(gdb) monitor xp/1gx 0x66ac220
000000066ac220: 0x00000000066c4067

エントリ: 0x66c4067 [存在 読み書き ユーザ アクセス済み ダーティ]. ページサイズ(ビット7) = 0、2MBの巨大ページではありません。

次のベース: 0x66c4067 & 0x000FFFFFFFFFF000 = 0x66c4000.

レベル1: PT (ページテーブル)

ベース: 0x66c4000. PTインデックス: 0x185.``` entry = 0x66c4000 + 0x185 * 8 = 0x66c4c28

root@kitploit:~
バッチリビジョンモードでは、スキャナーのリビジョンエンドポイントをフォアグラウンドで実行して生成テキストを分析し、`block` または `flag` の判定を返すことで、リアルタイムの API ガードレールを適用できます。これまでに見てきたすべてのクライアントコードサンプルはこのスキャン方式を使用しており、コンテンツがエンドユーザーに届く前にレビューする必要がある場合に最も役立ちます。```
(gdb) monitor xp/1gx 0x66c4c28
000000066c4c28: 0x80000000037fd867

エントリ: 0x80000000037fd867 [Present RW User Accessed Dirty NX]。これが 最終的なPTEです。

物理ページフレーム: 0x80000000037fd867 & 0x000FFFFFFFFFF000 = 0x37fd000。

Present = 0の場合はどうなるか?

ウォークがマップされていないページに当たったときに何が起こるか見てみましょう。ほぼ間違いなくマップされていないアドレス、アドレス空間の中央あたりの何かを選んでみてください:``` (gdb) p/x (0x0000414141414000 >> 39) & 0x1ff $1 = 0x82

root@kitploit:~
入力:```
(gdb) monitor xp/1gx 0x66c7000 + 0x82 * 8
00000000066c7410: 0x0000000000000000

すべてゼロです。ビット0(Present)はクリアされています。ウォークはここで停止します。PUDも PMDもPTもページフレームもありません。このアドレスは物理メモリにマッピングされていません。

CPUが通常の実行中にこれに当たった場合、page fault (割り込み14)を発生させます。カーネルのフォールトハンドラはその後、何をするかを決定します: ディスクからページをロードする(スワップ)、新しいページを割り当てる(デマンドページング)、または セグメンテーションフォールトでプロセスを強制終了します。

要点:ページテーブルは単なる変換構造ではありません。仮想メモリを _仮想的_にするメカニズムでもあります。すべてのアドレスが背後に物理メモリを 持つ必要はありません。CPUはウォーク中にこれを、一度に1レベルずつ 発見します。


明らかになること

物理ページフレームと元の仮想アドレスからのオフセットを 組み合わせます:``` Physical address = 0x37fd000 | 0xc90 = 0x37fdc90

root@kitploit:~
今すぐ読んでください:```
(gdb) monitor xp/6bx 0x37fdc90
00000000037fdc90: 0x46 0x4c 0x41 0x47 0x7b 0x70

これは F, L, A, G, {, p: フラグの始まりです。 続きを読む:``` (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:~
". Wait the user message ends with "INPUT:\n\n response". That seems like a formatting issue. The actual content to translate might be missing. Possibly the user intended to provide the markdown content but it's not present. I should check: The user says "Translate the following Kitploit tool content. This is chunk 47 of 69...". Then they give "INPUT:" and then nothing. Perhaps it's a placeholder? I need to assume there is no content to translate? That would be weird. Maybe the content is after "INPUT:" but it's blank. In that case, I should return nothing? But rules say return ONLY translated text, no preamble. If there is no input, I output nothing? That seems correct. However, I suspect the user might have forgotten to paste the actual content. But as an AI, I must follow instructions exactly. The input is empty. So I should output nothing. But to be safe, I'll output an empty response. But the instruction says "Translate the following... This is chunk 47...". Since no content is provided, I have nothing to translate. I'll output an empty string.```
FLAG{p4g3_t4bl3_w4lk3r}

gdb showing the full page table walk from CR3 to the flag

そこにある。あなたは今、CPUが毎秒何十億回も行っていることを、手動で、物理メモリから生のバイトを読みながら実行した。4段のテーブルをたどり、抽象化の背後には何も隠されていない。

以前は、ページングはスライドデッキの中の図だった。今では、頭の中で再生できる一連の読み取り操作(ベース、インデックス、シフト、マスク、追跡)となった。その違いは、カーネルエクスプロイトを前にして、PTEへの書き込みが実際に何を行うかを推論する必要があるときに重要になる。

結果は、QEMUのgva2gpa(ゲスト仮想アドレスからゲスト物理アドレスへ)モニターコマンドで検証できる。このコマンドは内部的にウォークを実行する:``` (qemu) gva2gpa 0x7ffe08985c90 gpa: 0x37fdc90

root@kitploit:~
---

## フラグとパーミッション

ウォーク中に各レベルでフラグをデコードしましたが、それらがセキュリティにとって何を意味するかについては飛ばしました。最後のPTEを見てください:```
0x80000000037fd867

注: ネットワークサービス検出エンジンはバージョン 0.43 以降非推奨となり、将来のバージョンで Pappy から削除される予定です。詳細はこちら.``` 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:~
これは理にかなっています。秘密はスタック変数です。スタックは読み取り可能、書き込み可能、そして「dirty」(書き込まれたことがある)です。また、最近のシステムではW^Xを強制しているため、実行不可(no-execute)としてマークされています。つまり、書き込み可能なページは実行可能であってはならないのです。

各レベルのフラグはハードウェアによってAND演算で結合されます。PUDエントリのUserフラグが0の場合、PTEがどうであれ、それより下の階層はユーザーからアクセスできません。最も制限の厳しい許可が優先されます。

---

## TLB:CPUがページテーブルウォークをスキップするとき

1バイトにアクセスするために4回のメモリ読み取りが必要です。これは高コストです。CPUは実際にはメモリアクセスのたびにページテーブルをウォークしているわけではありません。結果を**Translation Lookaside Buffer (TLB)** にキャッシュします。

フラグの仮想アドレスへの最初のアクセス後、CPUはマッピング `0x7ffe08985c90 -> 0x37fdc90` (おおよそ)をTLBに保存します。その後のアクセスではキャッシュにヒットし、ウォーク全体をスキップします。ページテーブルはRAM内でそのまま変更されずに残ります。

これは通常のコードには透過的です。しかし、ページテーブルエントリを**変更**した瞬間に問題が発生します。PTEに新しい物理アドレスを書き込んでも、CPUは気づきません。TLBには古いマッピングが残っているからです。明示的にフラッシュする必要があります。

カーネルはこれを `invlpg` 命令で行います。これは単一の仮想アドレスに対するTLBエントリを無効化します。ユーザー空間から `mprotect` を呼び出すと、内部ではこうした処理が行われます。カーネルがPTEのフラグを更新し、その後TLBをフラッシュすることで、CPUが新しい許可を認識するようになります。

これには直接的なセキュリティ上の影響があります。カーネルエクスプロイトにおいて、PTEに書き込むことに成功した場合(例えば、NXビットをクリアしてスタックを実行可能にするなど)、CPUがその変更を反映する前にTLBもフラッシュする必要があります。場合によっては、トリガーしたコードパスの副作用としてカーネルが自動でフラッシュしてくれることもあります。また、自分で手配する必要がある場合もあります。いずれにせよ、TLBの存在を知らなければ、エクスプロイトは理論上は機能しても実際には機能しません。

---

## 巨大ページ:ウォークが早期に終了するとき

上記のウォークスルーでは、4つのレベルすべてを経由しました。しかし、Page Sizeビット(ビット7)が設定されている場合、ウォークは早期に終了することがあります。

**レベル3(PUD):** ビット7が設定されている場合、そのエントリは1 GBのページを直接マッピングします。物理アドレスはエントリから取得され、仮想アドレスのビット[29:0]がオフセットになります(30ビット = 1 GB)。

**レベル2(PMD):** ビット7が設定されている場合、そのエントリは2 MBのページをマッピングします。仮想アドレスのビット[20:0]がオフセットになります(21ビット = 2 MB)。

カーネルマッピングでは巨大ページがよく見られます。カーネルの直接マップ領域(ほとんどの64ビットカーネルでは `0xffff888000000000` )は、TLBプレッシャーを減らすために、頻繁に2 MBまたは1 GBのページを使用します。

ウォーク中に巨大ページに遭遇した場合、計算式が変わります。```
2 MB page:  phys = (PMD_entry & 0x000FFFFFFFE00000) | (VA & 0x1FFFFF)
1 GB page:  phys = (PUD_entry & 0x000FFFFFC0000000) | (VA & 0x3FFFFFFF)

ウォークの自動化

プロセスがわかったので、それをコード化しましょう。pagewalk.py は、先ほど行ったのと同じウォークを行う gdb Python スクリプトです。コアロジックは1つの関数に収まっています:```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:~
完全なスクリプト(フラグデコードと整形出力付き)は `pagewalk.py` にあります。
それをソースとして入手し、手動の作業を検証するため、または他のアドレスを探索するために使用してください:```
(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がpagewalk.pyの出力と完全なページテーブルウォークを表示している

スクリプトがPUDとPMDレベルでhuge pageをチェックしてからウォークを続行する方法に注目してください。これはhuge pageセクションで説明したロジックと同じです。PageSize(ビット7)が設定されている場合、ウォークは早期に終了し、オフセットが拡大されます。

関数のアドレスをウォークしてみてください。NXビットがクリアされていることがわかります(コードは実行可能でなければなりません)。読み取り専用のデータセクションを試してみてください。R/Wがクリアされていることがわかります。


QEMUを使わずに物理メモリを読み取る

この演習全体を通して、monitor xpを使用して物理メモリを直接読み取りました。これはQEMUのモニターがVMの外部にあり、ゲストの物理アドレス空間にアクセスできるため機能します。実際のエクスプロイトでは、そのような贅沢はできません。

カーネルはこれをdirect-mapリージョンで自ら解決します。これは_すべての_物理RAMの連続した仮想マッピングです。x86-64では、このリージョンは慣例的に0xffff888000000000から始まりますが、KASLRが有効な場合、ベースはランダム化されます。カーネルは実際のベースをpage_offset_baseというシンボルに格納します。

nokaslrで起動したため、ベースはデフォルトです。確認してみましょう:``` (gdb) x/s 0xffff888000000000 + 0x37fdc90 0xffff888037fdc90: "FLAG{p4g3_t4bl3_w4lk3r}"

root@kitploit:~
同じ物理メモリが、カーネル仮想アドレスを通じてアクセスされる。これはカーネル自身が任意の物理メモリを読み取る方法である:`phys_to_virt()` は単に `page_offset_base + phys_addr` である。

これが、カーネルエクスプロイトが `page_offset_base` の漏洩を気にする理由でもある。KASLR が有効な場合、ダイレクトマップの開始位置が分からないため、物理アドレスをカーネル仮想アドレスに変換できない。ベースを漏洩させれば、ページテーブルエントリ自身を含む任意の物理アドレスをダイレクトマップを通じて読み書きできる。

---

## 別のプロセスのページテーブルを見つける

当セットアップでは、gdb が VM を停止したときに CR3 がチャレンジバイナリのページテーブルを指していることが保証されていた。しかし、_別の_ プロセスのページテーブルをウォークする必要がある場合はどうか?

すべてのプロセスの CR3 値は、その `task_struct` に格納されている。そのパスは次のとおり:```
task_struct -> mm_struct -> pgd -> physical page

gdbでカーネルシンボルを使用すると、initのtask_struct(PID 1)を見つけて そのページテーブルルートを抽出できます:``` (gdb) p/x init_task.mm->pgd $1 = 0xffff8880066c7000

root@kitploit:~
それはダイレクトマップにおけるカーネル仮想アドレスです。ベースを除去して物理アドレスを取得します:```
0xffff8880066c7000 - 0xffff888000000000 = 0x66c7000

That's the same CR3 we started with, which makes sense: our challenge binary is PID 1 in this minimal initramfs.

For other processes, you'd walk the task list (init_task.tasks linked list), find the target, and extract its mm->pgd the same way. Each process has its own page table tree rooted at its own CR3. The kernel swaps CR3 on every context switch, giving each process the illusion of private memory.


これが意味すること

もしあなたがここまで実際にウォークを実行してたどり着いたのであれば(読んだだけではなく)、 どんな図表も与えてくれないものを手に入れたことになります:ハードウェアレベルでメモリが実際にどのように動作するかについての直感です。 その直感が実を結ぶ場面を以下に示します。

ASLRは仮想アドレスをランダム化するのであって、ウォークそのものをランダム化するわけではありません。 ページテーブルの構造は常に同じです:4階層、各512エントリ、同じビットレイアウト。 ASLRは計算するインデックスを変えるだけで、プロセス自体は同一です。

W^Xはページテーブルで強制されます。 PTEのR/WビットとNXビットこそがmprotectを機能させるものです。 エクスプロイトがスタック上でシェルコードを実行しようとすると、CPUは変換時にNXビットをチェックし、フォールトします。

SMEPとSMAPはUserビットをチェックします。 Supervisor Mode Execution/Access Preventionは、ページテーブルの全階層にわたってUser/Supervisorビットをチェックします。 いずれかのエントリがアドレスをユーザーモードとマークし、カーネルコードがそれを実行またはアクセスしようとすると、CPUはフォールトします。 これが、現代のカーネルエクスプロイトが単にユーザースペースのシェルコードにジャンプできない理由です。

カーネルエクスプロイトはしばしばページテーブルを直接標的にします。 PTEに書き込めれば、仮想アドレスがマッピングする物理メモリを変更したり、パーミッションを変更したり、カーネルメモリをユーザーアクセス可能に再マッピングしたりできます。 ウォークを理解することは、攻撃面を理解することです。

KPTIはページテーブルを2つに分割します。 プロセスごとに1セットのページテーブルではなく、現在は2セットあります:ユーザーモード用(ほとんどのカーネルページはマップ解除された状態)とカーネルモード用(すべてがマップされた状態)。 カーネルはシステムコールのエントリとexitのたびにCR3をスワップします。 これを観察できます:ユーザースペースでVMを停止しCR3を読み取り、システムコールエントリにブレークポイントを設定してもう一度CR3を読み取ってみてください。それらは異なります。 ユーザーモードのページテーブルにはカーネルメモリのエントリが単純に含まれていないため、ウォークが完了してもリークするものがありません。

次にカーネルエクスプロイトの記事で「ページテーブルの再マッピング」という言葉が出てきたとき、それは抽象的なものではなくなります。 あなたは彼らがどのバイトについて話しているかを正確に知っているでしょう。なぜなら自分でそれを読んだからです。


参考文献

  • Understanding Paging: このチュートリアルのインスピレーションとなった記事です。この記事はもう少し先まで探求しようと試みていますが、私にとってはここが始まりでした。
  • Intel SDM, Volume 3A, Chapter 4: "Paging": 公式リファレンス(一度手動でウォークを行うと、驚くほど読みやすくなります)。
  • pwn.college: Kernel Security: 私がこの分野を実際に探求するきっかけとなったチャレンジ。強くお勧めします。
ツールをダウンロード