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

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

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を辿って物理メモリの全レベルを通過し、生のバイトからフラグを抽出する。

リポジトリを見る
281126ヶ月前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

### チャレンジのバイナリ

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

このスクリプトは、バンドルされたカーネルと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"

![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

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 ]

ページテーブルのベースは `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

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

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

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

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

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

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

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

ツールをダウンロード