はじめに
貸し切り状態の Windows Advent カレンダーですが、三部作の最終編です。第一部、第二部のリンクを一応貼っておきます。
NT カーネルのページング基礎 - Qiita
https://qiita.com/msmania/items/e169025b6c4a7efe138f
共有メモリのページング - Qiita
https://qiita.com/msmania/items/5d546f2619e937a0aa18
今回の記事は、役に立たない度で言うと三部作の中で最も役に立たないと思います。強いて言えば、ソースコードが公開されていない Windows でも頑張ればいろいろと遊べることが伝わるとよいかも・・・?お金を出してせっかく Windows を買ったのなら、WSL もいいですがたまには NT カーネルで遊んでみて下さい。
今回のネタはイメージベース アドレスです。EXE や DLL といったモジュール ファイルのデータは、Copy-on-Write 属性の共有メモリとしてロードされます。したがって、データの内容を書き換えない限りはプロセス間で物理アドレスは共通です。前回の記事で触れたのは、g.exe がマップされた仮想アドレスであるイメージベース アドレスも、親プロセスと子プロセスとで等しくなっていたことです。繰り返しプロセスを実行してもこの動作は変わらず、イメージベースは必ず等しくなる、ように見えます。ASLR はどこへ行った。
結論から言うと、イメージベースの値がプロセス間で必ず共通になる、というのは意図された動作です。といっても、全条件を完全に網羅したわけではないので見落としがあるのかもしれませんが。以下本記事では、その調査内容とプロセス間でイメージベースを変化させる方法について紹介します。
このイメージベースが共通になる現象を利用して、親プロセスから子プロセスに WriteProcessMemory を使ってデータを転送するテクニックが Chromium で使われています。有名な方法かもしれません。
target_process.cc - Chromium Code Search
https://source.chromium.org/chromium/chromium/src/+/master:sandbox/win/src/target_process.cc;l=232;drc=665b8382259e810d6d4fdfaf54cc4c5192782a13
もしイメージベースが共通であるという前提が崩れると、Chrome/Chromium は起動しなくなります。Edge が Chromium ベースになったことを加味すると、これは Windows の仕様と言っても構わないほど動かない事実な気がするのですが、意外にググっても情報が出てきません。ついでに言うと Firefox も起動しなくなります。実はそれを対応できるようにしようかと考えてこの調査を始めたのですが、起きない現象にわざわざ対応する意味ないよなぁ・・・。
調べるにあたって、イメージベースをいちいちデバッガーで確かめるのは面倒くさいので、第一部の記事で使った t.exe にコードを付け足してコンソール上に出力されるようにしました。以下の出力例だと、00007FF6B8380000 がイメージベースです。
msmania/mmdemo: NT Kernel Memory Manager demo
https://github.com/msmania/mmdemo/
C:\MSWORK> t.exe
1410: 00007FF6B8380000
1GB Page: 0000024500000000
2MB Page: 00000244ED800000
1164: 00007FF6B8380000
C:\MSWORK> t.exe
104c: 00007FF6B8380000
1GB Page: 00000197C0000000
2MB Page: 000001979C800000
10dc: 00007FF6B8380000
今回デバッグするのは 20H2 に 2020.12 のロールアップを適用した環境です。デバッガーは 20H2 の WDK がまだ公開されていないので、その 20H1 のものです。
- OS: Windows 10, version 20H2 + KB4592438 (OS Build: 19042.685)
- Debugger: 10.0.19041.1 (included in Windows 10 2004 WDK)
プロセスが実行されるまで
プロセスが作成されるときのカーネル側のエントリポイントは NtCreateUserProcess です。NtCreateUserProcess 内部の処理で、本記事に関連するものを以下に抜粋して記載しました。この情報は他の用途でデバッグするときにも役に立つかもしれません。
有難いことに、関数の名前から大体の流れが掴めるようになっています。大まかには、1) ファイルを開いて、2) 開いたファイルのセクション オブジェクトを作成し、3) そのセクション オブジェクトをマップ、という作業が順番に行われるのが分かります。
nt!NtCreateUserProcess
nt!IoCreateFileEx --> create a new nt!_FILE_OBJECT
nt!MmCreateSpecialImageSection
nt!MiCreateSection
nt!MiCreateImageOrDataSection
nt!MiReferenceControlArea
--> (1) or (2)
(1)
nt!MiShareExistingControlArea
nt!MiValidateExistingImage
nt!MiRelocateImageAgain
nt!MiSelectImageBase
nt!ExGenRandom --> the case of ImageBaseOkToReuse
nt!MiSwitchBaseAddress
(2)
nt!MiCreateNewSection
nt!MiRelocateImage
nt!MiSelectImageBase
nt!ExGenRandom ---> the case of a new executable file
nt!MiFinishCreateSection
nt!ObCreateObjectEx --> creates a new nt!_SECTION
nt!PspAllocateProcess
nt!ObCreateObject --> allocates a new EPROCESS
nt!MmInitializeProcessAddressSpace
nt!MiMapProcessExecutable
nt!MmMapViewOfSectionEx
nt!MiMapViewOfSectionExCommon
nt!MiMapViewOfSectionCommon
nt!MiMapViewOfSection
nt!MiMapViewOfImageSection
nt!MiIsVaRangeAvailable
nt!MiSelectUserAddress --> the case of address conflict
nt!PsCallImageNotifyRoutines --> Notify if EPROCESS:UniqueProcessId != 0
nt!PsMapSystemDll --> Map ntdll.dll
Setting EPROCESS:UniqueProcessId
nt!PspInsertThread
nt!PspCallProcessNotifyRoutines
nt!PspCallThreadNotifyRoutines
共有メモリで使われる構造
ファイル、セクション オブジェクト、そしてマッピングで使われるプロトタイプ PTE などの構造同士の関係について図にまとめました。
実行可能ファイルはセクションという単位に分かれており、セクションの種類によって必要になる属性が異なります。例えばコード領域である .text セクションは READ と EXECUTE、読み取り専用のデータ (const のグローバル変数など) が置かれる .rdata セクションは READ のみ、などです。それぞれのセクションは nt!_SUBSECTION という構造体で表され、サイズに応じて 1 つ、または複数のプロトタイプ PTE を持ち、マッピング状態を管理しています。
図の上部にある黄色で示したオブジェクトは、プロセスが作成されるたびに新しく作られます。一方、下部の青色で示したオブジェクトは、プロセスが終了しても維持され、次回起動時に再利用されるオブジェクトです。
イメージベースはどこから
プロセスが作成されると、そのイメージベースが nt!_EPROCESS::SectionBaseAddress に保存されます。フィールドに値をセットするのは nt!MiMapProcessExecutable ですが、イメージベースの値を決めているのは nt!MiMapViewOfImageSection です。この関数は第一引数に _CONTROL_AREA を取って r13 レジスターにキャッシュしており、以下に示すコードで _SEGMENT::BasedAddress の値を読み込んでいます。
0: kd> dt nt!_CONTROL_AREA Segment->BasedAddress
+0x000 Segment :
+0x020 BasedAddress : Ptr64 Void
nt!MiMapViewOfImageSection+0x3a:
fffff805`7ec35dfa 498b7d00 mov rdi,qword ptr [r13]
<<<< r13 = nt!_CONTROL_AREA, rdi = nt!_CONTROL_AREA::Segment
...snip...
nt!MiMapViewOfImageSection+0x84:
fffff805`7ec35e44 488b4f20 mov rcx,qword ptr [rdi+20h]
<<<< rdi = nt!_SEGMENT; rcx = nt!_SEGMENT::BasedAddress
fffff805`7ec35e48 4889442450 mov qword ptr [rsp+50h],rax
fffff805`7ec35e4d 48894c2470 mov qword ptr [rsp+70h],rcx <<<< Base
...snip...
nt!MiMapViewOfImageSection+0x2ba:
fffff805`7ec3607a 488b8598000000 mov rax,qword ptr [rbp+98h] <<<< &X
fffff805`7ec36081 8b38 mov edi,dword ptr [rax]
fffff805`7ec36083 48037c2470 add rdi,qword ptr [rsp+70h] <<<< Base + X
fffff805`7ec36088 48897c2468 mov qword ptr [rsp+68h],rdi
...snip...
nt!MiMapViewOfImageSection+0x535:
fffff805`7ec362f5 488b7c2468 mov rdi,qword ptr [rsp+68h] <<<< Base + X
fffff805`7ec362fa 488b4588 mov rax,qword ptr [rbp-78h] <<<< Y; always 0?
fffff805`7ec362fe 48c1e010 shl rax,10h
fffff805`7ec36302 4803f8 add rdi,rax
fffff805`7ec36305 488b8590000000 mov rax,qword ptr [rbp+90h]
fffff805`7ec3630c 48897c2468 mov qword ptr [rsp+68h],rdi
fffff805`7ec36311 488938 mov qword ptr [rax],rdi
<<<< Will be set to EPROCESS
正確には、二つの変数を含めた _SEGMENT::BasedAddress + X + (Y << 16) という値を計算しています。
アセンブリを見ても、Y は常に 0 になっているローカル変数にしか見えず正体は謎です。高性能な逆アセンブラが欲しいですね。X の方は、nt!MiMapViewOfImageSection の第四引数です。この値は ZwMapViewOfSection でいうところの SectionOffset と同様の役割を持つのではないか思います。
ZwMapViewOfSection function (wdm.h) - Windows drivers | Microsoft Docs
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-zwmapviewofsection
nt!MiMapViewOfImageSection は必ずしも _SEGMENT::BasedAddress のアドレスにイメージをマップするとは限りません。nt!MiMapViewOfImageSection の第三引数はマップしたアドレスを呼び出し側で受け取るポインターになっていますが、関数呼び出し時に 0 以外の値を入れておくとそのアドレスにイメージがマップされます。これは ZwMapViewOfSection のパラメーター BaseAddress の役割と同じです。
これら 2 つのパラメーター (SectionOffset と BaseAddress) の出自を追跡すると、いずれも nt!MiMapProcessExecutable が nt!MmMapViewOfSectionEx を呼ぶときに渡すローカル変数であり、常に 0 です。したがって、_SEGMENT::BasedAddress の値がそのままイメージベースになります。
nt!MiMapProcessExecutable+0x48:
fffff805`7ec4e668 488364245000 and qword ptr [rsp+50h],0
fffff805`7ec4e66e 488d4dff lea rcx,[rbp-1]
fffff805`7ec4e672 8364244800 and dword ptr [rsp+48h],0
fffff805`7ec4e677 4c8d4df7 lea r9,[rbp-9] <<<< &SectionOffset
fffff805`7ec4e67b 418b06 mov eax,dword ptr [r14]
fffff805`7ec4e67e 4c8d456f lea r8,[rbp+6Fh] <<<< &BaseAddress
fffff805`7ec4e682 4883657f00 and qword ptr [rbp+7Fh],0
fffff805`7ec4e687 83e010 and eax,10h
fffff805`7ec4e68a 4883656f00 and qword ptr [rbp+6Fh],0 <<<< BaseAddress = 0
fffff805`7ec4e68f 41bf04000000 mov r15d,4
fffff805`7ec4e695 488365f700 and qword ptr [rbp-9],0 <<<< SectionOffset = 0
fffff805`7ec4e69a 498bd5 mov rdx,r13
fffff805`7ec4e69d c744244001000000 mov dword ptr [rsp+40h],1
fffff805`7ec4e6a5 48894c2438 mov qword ptr [rsp+38h],rcx
fffff805`7ec4e6aa 488bcb mov rcx,rbx
fffff805`7ec4e6ad c1e019 shl eax,19h
fffff805`7ec4e6b0 44897c2430 mov dword ptr [rsp+30h],r15d
fffff805`7ec4e6b5 89442428 mov dword ptr [rsp+28h],eax
fffff805`7ec4e6b9 488d457f lea rax,[rbp+7Fh]
fffff805`7ec4e6bd 4889442420 mov qword ptr [rsp+20h],rax
fffff805`7ec4e6c2 48c745ff05000000 mov qword ptr [rbp-1],5
fffff805`7ec4e6ca 48c7450720000000 mov qword ptr [rbp+7],20h
fffff805`7ec4e6d2 e8592ec7ff call nt!MmMapViewOfSectionEx (fffff805`7e8c1530)
前述したように、_CONTROL_AREA や _SEGMENT 構造体はプロセスが終了しても保持されます。nt!MiCreateImageOrDataSection が nt!MiReferenceControlArea や nt!MiShareExistingControlArea という関数を呼び出していることから、メモリ マネージャーは、セクション オブジェクトを作成するときにそのファイルに対する _CONTROL_AREA が存在するかどうかを何らかの方法で調べ、再利用しています。その _CONTROL_AREA が参照する _SEGMENT が、セクション オブジェクトをマップする既定のアドレスをただ一つ保存していることが、プロセスを何度実行してもイメージベースが同じアドレスになる仕掛けです。
_CONTROL_AREA が破棄される条件は調べていませんが、仮にキャッシュされる _CONTROL_AREA の数に上限があるとしたら、異なる EXE ファイルを大量に実行すると古い _CONTROL_AREA が破棄されてイメージベースが変わるかもしれません。
イメージベースが変わる例外
キャッシュされた既存のベースアドレスが使われない例外が幾つか存在します。それぞれのケースについて、どのように新しいイメージベース アドレスが生成されるのかを調べました。
例外 1: ファイルが変わった時
t.exe を削除し、全く同じファイルを別の場所からコピーして実行するとイメージベースが変わります。ファイルが変わったことで _CONTROL_AREA が新しく作られ、そのときに BasedAddress も新たに割り振られるためです。
nt!MiCreateImageOrDataSection は、nt!MiReferenceControlArea を使って _CONTROL_AREA を取得しています。実行可能ファイルに対応する _CONTROL_AREA が存在しなかった場合、nt!MiReferenceControlArea は新しく _CONTROL_AREA を作成して BeingCreated フラグを 1 にします。nt!MiCreateImageOrDataSection は、取得した _CONTROL_AREA の BeingCreated フラグを見て、もし 0 だった場合は nt!MiShareExistingControlArea、1 の場合は nt!MiCreateNewSection を実行するように分岐します。
nt!MiCreateNewSection は名前が紛らわしいですが、_SECTION をではなく _SEGMENT を作成する関数です。キャッシュが存在しなかったため nt!MiReferenceControlArea が新たに _CONTROL_AREA を作成した場合は、nt!MiCreateNewSection が新しい BasedAddressを含む _SEGMENT の作成を行ない、_CONTROL_AREA に割り当てます。
0: kd> dt nt!_CONTROL_AREA u.Flags.BeingCreated
+0x038 u :
+0x000 Flags :
+0x000 BeingCreated : Pos 1, 1 Bit
nt!MiCreateImageOrDataSection+0x166:
fffff805`7ec303f6 4c8d8424f0000000 lea r8,[rsp+0F0h] <<<< will be a control area
fffff805`7ec303fe 488bcb mov rcx,rbx
fffff805`7ec30401 488d542430 lea rdx,[rsp+30h]
fffff805`7ec30406 e84917c5ff call nt!MiReferenceControlArea (fffff805`7e881b54)
fffff805`7ec3040b 8bf8 mov edi,eax
fffff805`7ec3040d 85c0 test eax,eax
fffff805`7ec3040f 0f88ea010000 js nt!MiCreateImageOrDataSection+0x36f (fffff805`7ec305ff)
fffff805`7ec30415 4c8bbc24f0000000 mov r15,qword ptr [rsp+0F0h]
fffff805`7ec3041d 4c897b40 mov qword ptr [rbx+40h],r15
fffff805`7ec30421 418b4738 mov eax,dword ptr [r15+38h] <<<< _CONTROL_AREA::u.Flags
fffff805`7ec30425 a802 test al,2 <<<< checking BeingCreated bit
fffff805`7ec30427 0f852e010000 jne nt!MiCreateImageOrDataSection+0x2cb (fffff805`7ec3055b)
----> Jump if a control area is being created
(If BeingCreated is OFF)
nt!MiCreateImageOrDataSection+0x19d:
fffff805`7ec3042d 488bcd mov rcx,rbp
fffff805`7ec30430 e87b82c1ff call nt!KeLeaveCriticalRegionThread (fffff805`7e8486b0)
fffff805`7ec30435 488bcb mov rcx,rbx
fffff805`7ec30438 e87bf1ffff call nt!MiShareExistingControlArea (fffff805`7ec2f5b8)
fffff805`7ec3043d 8bf8 mov edi,eax
fffff805`7ec3043f 85c0 test eax,eax
fffff805`7ec30441 0f88eeeb1b00 js nt!MiCreateImageOrDataSection+0x1beda5 (fffff805`7edef035)
(If BeingCreated is ON)
nt!MiCreateImageOrDataSection+0x2cb:
fffff805`7ec3055b 488d9424f8000000 lea rdx,[rsp+0F8h] <<<< will be a segment
fffff805`7ec30563 488bcb mov rcx,rbx
fffff805`7ec30566 e8a9250100 call nt!MiCreateNewSection (fffff805`7ec42b14)
fffff805`7ec3056b 8bf8 mov edi,eax
fffff805`7ec3056d 85c0 test eax,eax
fffff805`7ec3056f 488b8424f8000000 mov rax,qword ptr [rsp+0F8h]
fffff805`7ec30577 0f88a5000000 js nt!MiCreateImageOrDataSection+0x392 (fffff805`7ec30622)
fffff805`7ec3057d 488b08 mov rcx,qword ptr [rax] <<<< rcx = _CONTROL_AREA
fffff805`7ec30580 48894b40 mov qword ptr [rbx+40h],rcx
fffff805`7ec30584 e86b41c7ff call nt!MiGetControlAreaPartition (fffff805`7e8a46f4)
BasedAddress の生成は、nt!MiCreateNewSection → nt!MiRelocateImage → nt!MiSelectImageBase と呼ばれた先の nt!MiSelectImageBase で行われます。ロードされるときの条件によって幾つかの計算式があるのだと思いますが、置き換えた t.exe を初回実行したときの BasedAddress は以下のコードで生成されています。
nt!MiSelectImageBase+0x1fa:
fffff805`7ec3ac62 8d4d01 lea ecx,[rbp+1]
fffff805`7ec3ac65 e806eac6ff call nt!ExGenRandom (fffff805`7e8a9670)
fffff805`7ec3ac6a 8bc0 mov eax,eax
fffff805`7ec3ac6c 41b801000200 mov r8d,20001h
fffff805`7ec3ac72 410fb7ce movzx ecx,r14w
fffff805`7ec3ac76 4899 cqo
fffff805`7ec3ac78 4c2bc1 sub r8,rcx
fffff805`7ec3ac7b 49f7f8 idiv rax,r8
fffff805`7ec3ac7e 488dba0000f67f lea rdi,[rdx+7FF60000h]
fffff805`7ec3ac85 48c1e710 shl rdi,10h <<<< New imagebase
例外 2: 既定のアドレスが予約済みの場合
実行可能ファイルのイメージベースが予約済み、という状況を作り出すことはできませんが、LoadLibrary で別の実行可能ファイルのプロセスに EXE をロードすることは可能です。mmdemo リポジトリの t.exe と u.exe を組み合わせて実行すると、u.exe が t.exe を LoadLibrary でロードして、そのベースアドレスを出力するようになります。
t.exe
--> launch t.exe as a child
--> launch u.exe as a child, loading t.exe via LoadLibrary
以下の出力例から、LoadLibrary した場合でも単独実行したときと同じ 00007FF75FA20000 がベースアドレスに選ばれていることが分かります。
C:\MSWORK> t.exe C:\MSWORK\u.exe
151c: 00007FF75FA20000
1GB Page: 000001F800000000
2MB Page: 000001F7FDC00000
0aec: 00007FF75FA20000 C:\MSWORK\t.exe
07fc: 00007FF75FA20000
C:\MSWORK> t.exe C:\MSWORK\u.exe
0d44: 00007FF75FA20000
1GB Page: 0000016500000000
2MB Page: 00000164FE400000
0ba4: 00007FF75FA20000 C:\MSWORK\t.exe
1964: 00007FF75FA20000
既定のアドレスが事前に分かっているので、u.exe の中で VirtualAlloc を使って00007FF75FA20000 を予約しておいてから LoadLibrary を呼び出すとどうなるのかを確かめます。WARMUP という環境変数が定義されているときに VirtualAlloc でアドレスを予約するようにしたので、以下のように実行します。
C:\MSWORK>set WARMUP=1
C:\MSWORK> t.exe C:\MSWORK\u.exe
0f28: 00007FF75FA20000
1GB Page: 0000016440000000
2MB Page: 0000016411800000
0514: 000001CB4A320000 C:\MSWORK\t.exe
0814: 00007FF75FA20000
C:\MSWORK> t.exe C:\MSWORK\u.exe
0484: 00007FF75FA20000
1GB Page: 00000231C0000000
2MB Page: 00000231ADC00000
0520: 000001B06DD60000 C:\MSWORK\t.exe
0d18: 00007FF75FA20000
一回目は 000001CB4A320000、二回目は 000001B06DD60000 というように、毎回異なるアドレスにマップされるようになりました。この動作は、前の章でも出てきた nt!MiMapViewOfImageSection に実装されています。
前述のように、nt!MiMapViewOfImageSection は呼び出し元から引数として渡された BaseAddress、または nt!_SEGMENT::BasedAddress のどちらかをマップ先のアドレスにします。どちらかを選んだ後 nt!MiIsVaRangeAvailable を呼び出して、候補となるアドレスが既に VAD ツリー内に存在するかどうかを確認します。もし VAD が存在して候補のアドレスが使えないと分かると、nt!MiSelectUserAddress を使って新しいイメージベースを生成します。
nt!MiMapViewOfImageSection+0x383:
fffff805`7ec36143 498b4608 mov rax,qword ptr [r14+8]
fffff805`7ec36147 4d8bc4 mov r8,r12 <<<< Segment Size
fffff805`7ec3614a 4d8b0e mov r9,qword ptr [r14]
fffff805`7ec3614d 488bd7 mov rdx,rdi <<<< BasedAddress
fffff805`7ec36150 498bcf mov rcx,r15 <<<< EPROCESS
fffff805`7ec36153 4889442420 mov qword ptr [rsp+20h],rax
fffff805`7ec36158 e88b090000 call nt!MiIsVaRangeAvailable (fffff805`7ec36ae8)
fffff805`7ec3615d 85c0 test eax,eax
fffff805`7ec3615f 0f848a040000 je nt!MiMapViewOfImageSection+0x82f (fffff805`7ec365ef)
----> Jump if address is already in VAD
nt!MiMapViewOfImageSection+0x3a5:
fffff805`7ec36165 4c8b8598000000 mov r8,qword ptr [rbp+98h]
fffff805`7ec3616c bf03000040 mov edi,40000003h
fffff805`7ec36171 488b4c2468 mov rcx,qword ptr [rsp+68h] <<<< imagebase
fffff805`7ec36176 41ba00000000 mov r10d,0
...snip...
fffff805`7ec36573 c3 ret
nt!MiMapViewOfImageSection+0x82f:
fffff805`7ec365ef ba4c010000 mov edx,14Ch
...snip...
fffff805`7ec36634 8b4630 mov eax,dword ptr [rsi+30h]
fffff805`7ec36637 488d4c2468 lea rcx,[rsp+68h] <<<< will be a new imagebase
fffff805`7ec3663c 48894c2448 mov qword ptr [rsp+48h],rcx
fffff805`7ec36641 4d8bc4 mov r8,r12
fffff805`7ec36644 488d4d80 lea rcx,[rbp-80h]
fffff805`7ec36648 c1e807 shr eax,7
fffff805`7ec3664b 48894c2440 mov qword ptr [rsp+40h],rcx
fffff805`7ec36650 83e01f and eax,1Fh
fffff805`7ec36653 418b4e28 mov ecx,dword ptr [r14+28h]
fffff805`7ec36657 44894c2438 mov dword ptr [rsp+38h],r9d
fffff805`7ec3665c 4c8b8d88000000 mov r9,qword ptr [rbp+88h]
fffff805`7ec36663 89442430 mov dword ptr [rsp+30h],eax
fffff805`7ec36667 4c896c2428 mov qword ptr [rsp+28h],r13
fffff805`7ec3666c 4c89542420 mov qword ptr [rsp+20h],r10
fffff805`7ec36671 e8fa6affff call nt!MiSelectUserAddress (fffff805`7ec2d170)
fffff805`7ec36676 8bf8 mov edi,eax
fffff805`7ec36678 85c0 test eax,eax
fffff805`7ec3667a 0f88aba61b00 js nt!MiMapViewOfImageSection+0x1baf6b (fffff805`7edf0d2b)
nt!MiMapViewOfImageSection+0x8c0:
fffff805`7ec36680 4c8ba588000000 mov r12,qword ptr [rbp+88h] <<<< Segment size
fffff805`7ec36687 4c8b6c2458 mov r13,qword ptr [rsp+58h]
fffff805`7ec3668c 4981fd00002000 cmp r13,200000h
fffff805`7ec36693 0f85ccfaffff jne nt!MiMapViewOfImageSection+0x3a5 (fffff805`7ec36165)
----> Jump to the block above
例外 3: ImageBaseOkToReuse フラグが OFF の場合
_CONTROL_AREA 構造体には u2.e2.ImageBaseOkToReuse というフラグがあり、t.exe に対応する _CONTROL_AREA の ImageBaseOkToReuse フラグを見ると 1 になっています。
0: kd> dt nt!_CONTROL_AREA u2.e2.
+0x058 u2 :
+0x000 e2 :
+0x000 NumberOfSystemCacheViews : Uint4B
+0x000 ImageRelocationStartBit : Uint4B
+0x004 WritableUserReferences : Int4B
+0x004 ImageRelocationSizeIn64k : Pos 0, 16 Bits
+0x004 SystemImage : Pos 16, 1 Bit
+0x004 CantMove : Pos 17, 1 Bit
+0x004 StrongCode : Pos 18, 2 Bits
+0x004 BitMap : Pos 20, 2 Bits
+0x004 ImageActive : Pos 22, 1 Bit
+0x004 ImageBaseOkToReuse : Pos 23, 1 Bit
+0x008 FlushInProgressCount : Uint4B
+0x008 NumberOfSubsections : Uint4B
+0x008 SeImageStub : Ptr64 _MI_IMAGE_SECURITY_REFERENCE
デバッガーでフラグの値を 0 に書き換えてから t.exe を再度実行すると、イメージベースが変わります。_SEGMENT::BasedAddress も新たなアドレスに置き換えられるため、ここで割り当てられた新しいアドレスが今後のプロセスにも持続的に反映されます。
ImageBaseOkToReuse がチェックされるのは、例外 1 でも出てきた nt!MiSelectImageBase です。ただし今回のケースは nt!MiCreateNewSection を経由するのではなく、nt!MiCreateImageOrDataSection → nt!MiShareExistingControlArea → nt!MiValidateExistingImage → nt!MiRelocateImageAgain → nt!MiSelectImageBase という経路で呼び出されます。
フラグをチェックしている箇所は以下のコードです。これは例外 1 の場合と全く同じです。というのも、例外 1 で出てきた BeingCreated フラグ付きの新しい _CONTROL_AREA も ImageBaseOkToReuse が 0 になっているためです。
nt!MiSelectImageBase+0x1c5:
fffff805`7ec3ac2d 8b465c mov eax,dword ptr [rsi+5Ch] <<<< _CONTROL_AREA.u2.e2
fffff805`7ec3ac30 0fbae017 bt eax,17h <<<< Checking ImageBaseOkToReuse
fffff805`7ec3ac34 7324 jae nt!MiSelectImageBase+0x1f2 (fffff805`7ec3ac5a)
---> Jump if ImageBaseOkToReuse is off
nt!MiSelectImageBase+0x1f2:
fffff805`7ec3ac5a 85ed test ebp,ebp
fffff805`7ec3ac5c 0f85a3000000 jne nt!MiSelectImageBase+0x29d (fffff805`7ec3ad05)
nt!MiSelectImageBase+0x1fa:
fffff805`7ec3ac62 8d4d01 lea ecx,[rbp+1]
fffff805`7ec3ac65 e806eac6ff call nt!ExGenRandom (fffff805`7e8a9670)
fffff805`7ec3ac6a 8bc0 mov eax,eax
fffff805`7ec3ac6c 41b801000200 mov r8d,20001h
fffff805`7ec3ac72 410fb7ce movzx ecx,r14w
fffff805`7ec3ac76 4899 cqo
fffff805`7ec3ac78 4c2bc1 sub r8,rcx
fffff805`7ec3ac7b 49f7f8 idiv rax,r8
fffff805`7ec3ac7e 488dba0000f67f lea rdi,[rdx+7FF60000h]
fffff805`7ec3ac85 48c1e710 shl rdi,10h <<<< New imagebase!
nt!MiSelectImageBase は、第五引数で受け取るポインターを介してイメージベースを呼び出し元に返します。nt!MiRelocateImageAgain は、nt!MiSelectImageBase から返ったイメージベースがそれまで _SEGMENT に保存されていた値を同じかどうかを比較し、もし異なるアドレスが返された場合には nt!MiSwitchBaseAddress という関数を呼んで _CONTROL_AREA 及び _SEGMENT を更新します。
nt!MiRelocateImageAgain が nt!MiSelectImageBase を呼び出すには条件があり、ImageBaseOkToReuse の隣に位置する ImageActive フラグが 0 のときに限られています。さらに nt!MiRelocateImageAgain は、nt!MiSelectImageBase を実行した直後に ImageActive フラグを 1 にします。以下の部分です。
nt!MiRelocateImageAgain+0x62:
fffff805`7ec3b322 f7435c00004000 test dword ptr [rbx+5Ch],400000h
<<<< Checking _CONTROL_AREA::u2.e2.ImageActive
fffff805`7ec3b329 743e je nt!MiRelocateImageAgain+0xa9 (fffff805`7ec3b369)
----> Jump if ImageActive is off
fffff805`7ec3b32b 488b8c24a0000000 mov rcx,qword ptr [rsp+0A0h]
fffff805`7ec3b333 498bd6 mov rdx,r14
fffff805`7ec3b336 e8fdffc6ff call nt!MI_UNLOCK_RELOCATIONS_EXCLUSIVE (fffff805`7e8ab338)
fffff805`7ec3b33b 4885ed test rbp,rbp
...snip...
fffff805`7ec3b367 c3 ret
nt!MiRelocateImageAgain+0xa9:
fffff805`7ec3b369 4c8b6e20 mov r13,qword ptr [rsi+20h] <<<< _SEGMENT::BasedAddress
fffff805`7ec3b36d 488d0dcc396100 lea rcx,[nt!MiState+0x2680 (fffff805`7f24ed40)]
fffff805`7ec3b374 ba01000000 mov edx,1
fffff805`7ec3b379 e82290c3ff call nt!MiReservePtes (fffff805`7e8743a0)
fffff805`7ec3b37e 488be8 mov rbp,rax
fffff805`7ec3b381 4885c0 test rax,rax
fffff805`7ec3b384 0f8416681b00 je nt!MiRelocateImageAgain+0x1b68e0 (fffff805`7edf1ba0)
fffff805`7ec3b38a 488d842490000000 lea rax,[rsp+90h] <<<< will be a new imagebase
fffff805`7ec3b392 4533c9 xor r9d,r9d
fffff805`7ec3b395 458bc4 mov r8d,r12d
fffff805`7ec3b398 4889442420 mov qword ptr [rsp+20h],rax
fffff805`7ec3b39d 498bd6 mov rdx,r14
fffff805`7ec3b3a0 488bce mov rcx,rsi
fffff805`7ec3b3a3 e8c0f6ffff call nt!MiSelectImageBase (fffff805`7ec3aa68)
fffff805`7ec3b3a8 8bf8 mov edi,eax
fffff805`7ec3b3aa 85c0 test eax,eax
fffff805`7ec3b3ac 0f8879ffffff js nt!MiRelocateImageAgain+0x6b (fffff805`7ec3b32b)
fffff805`7ec3b3b2 0fba6b5c16 bts dword ptr [rbx+5Ch],16h <<<< turning on ImageActive
fffff805`7ec3b3b7 488bcb mov rcx,rbx
fffff805`7ec3b3ba e8c517c7ff call nt!MiGetControlAreaLoadConfig (fffff805`7e8acb84)
fffff805`7ec3b3bf 488bb42490000000 mov rsi,qword ptr [rsp+90h]
...snip...
fffff805`7ec3b3dc 493bf5 cmp rsi,r13
<<<< Comparing a returned imagebase to the old one
fffff805`7ec3b3df 7528 jne nt!MiRelocateImageAgain+0x149 (fffff805`7ec3b409)
----> Jump if a new imagebase was returned
nt!MiRelocateImageAgain+0x121:
fffff805`7ec3b3e1 33ff xor edi,edi
fffff805`7ec3b3e3 e943ffffff jmp nt!MiRelocateImageAgain+0x6b (fffff805`7ec3b32b)
nt!MiRelocateImageAgain+0x149:
fffff805`7ec3b409 f705edff6b0000400000 test dword ptr [nt!MiFlags (fffff805`7f2fb400)],4000h
...snip...
fffff805`7ec3b437 448bc8 mov r9d,eax
fffff805`7ec3b43a 4c8bc5 mov r8,rbp
fffff805`7ec3b43d 488bd6 mov rdx,rsi
fffff805`7ec3b440 488bcb mov rcx,rbx
fffff805`7ec3b443 e844dc0900 call nt!MiSwitchBaseAddress (fffff805`7ecd908c)
fffff805`7ec3b448 eb97 jmp nt!MiRelocateImageAgain+0x121 (fffff805`7ec3b3e1)
ImageActive フラグが 0 になるのは以下のタイミングで、プロセスが終了してセクションが削除されるときです。
1: kd> ub . l1
nt!MiImageUnused+0x7a:
fffff805`7e90b4b2 0fba735c16 btr dword ptr [rbx+5Ch],16h
1: kd> knL10
# Child-SP RetAddr Call Site
00 fffff40d`0e2e76a0 fffff805`7e8a3b02 nt!MiImageUnused+0x7f
01 fffff40d`0e2e76d0 fffff805`7e8a38b2 nt!MiCheckControlArea+0x232
02 fffff40d`0e2e7750 fffff805`7ec33d33 nt!MiDereferenceControlAreaBySection+0x2a
03 fffff40d`0e2e7780 fffff805`7ebfaef0 nt!MiSectionDelete+0x83
04 fffff40d`0e2e77b0 fffff805`7e861277 nt!ObpRemoveObjectRoutine+0x80
05 fffff40d`0e2e7810 fffff805`7e86119e nt!ObfDereferenceObjectWithTag+0xc7
06 fffff40d`0e2e7850 fffff805`7ec5d818 nt!HalPutDmaAdapter+0xe
07 fffff40d`0e2e7880 fffff805`7ed06596 nt!PspRundownSingleProcess+0x340
08 fffff40d`0e2e7910 fffff805`7ed09a8e nt!PspExitThread+0x5f6
09 fffff40d`0e2e7a10 fffff805`7ea071b5 nt!NtTerminateProcess+0xde
0a fffff40d`0e2e7a80 00007ffd`8b78c534 nt!KiSystemServiceCopyEnd+0x25
0b 000000e9`f774f8c8 00000000`00000000 ntdll!NtTerminateProcess+0x14
イメージベースを変えるには
プロセス起動時には、その実行可能ファイルに対応する _CONTROL_AREA が既に作られているかどうかを確認し、既に作られている場合には _SEGMENT 構造体に保存された BasedAddress が使われることが分かりました。この動作に反して、キャッシュされたイメージベースを使わせないようにする方法はないでしょうか。
例外 1 の動作を利用して、キャッシュされた _CONTROL_AREA 自体を何らかの方法で破棄させることはできるかもしれません。もしくは、例外 3 の動作を利用して _CONTROL_AREA の ImageBaseOkToReuse フラグを 0 に書き換えることもできそうです。しかしいずれの場合も、親プロセスが起動しているとき (= ImageActive フラグ が 1 のとき) に子プロセスのイメージベースを書き換えることはできません。
例外 2 の動作は使えそうではあります。EPROCESS が作成された後、nt!MiMapViewOfImageSection が呼び出される前に VAD ツリーを書き換えて、既定のベースアドレスが予約済みであるかのように見せかけることはできそうです。しかし残念ながら nt!MiMapViewOfImageSection が呼ばれる前のタイミングで受け取れるコールバックは見つかりませんでした。
詰んだ・・・。
これでは何のためにここまで調べたのか分からなくなるので、多少無茶な方法を使ってもいいことにして一つ方法を思いつきました。それが例外 3 で出てきた nt!MiSwitchBaseAddress という関数です。この関数のパラメーターは比較的シンプルで、_CONTROL_AREA と設定したいイメージベース、あとはセッション ID と nt!MiReservePtes からの戻り値を渡すだけで _CONTROL_AREA を更新してくれます。
nt!MiRelocateImageAgain+0xad:
fffff805`7ec3b36d 488d0dcc396100 lea rcx,[nt!MiState+0x2680 (fffff805`7f24ed40)]
fffff805`7ec3b374 ba01000000 mov edx,1
fffff805`7ec3b379 e82290c3ff call nt!MiReservePtes (fffff805`7e8743a0)
fffff805`7ec3b37e 488be8 mov rbp,rax
...snip...
fffff805`7ec3b41a 65488b0c2588010000 mov rcx,qword ptr gs:[188h]
fffff805`7ec3b423 488b89b8000000 mov rcx,qword ptr [rcx+0B8h]
fffff805`7ec3b42a e8815ac2ff call nt!MmGetSessionIdEx (fffff805`7e860eb0)
fffff805`7ec3b42f 488bb42490000000 mov rsi,qword ptr [rsp+90h]
fffff805`7ec3b437 448bc8 mov r9d,eax <<<< return value from nt!MmGetSessionIdEx
fffff805`7ec3b43a 4c8bc5 mov r8,rbp <<<< return value from nt!MiReservePtes
fffff805`7ec3b43d 488bd6 mov rdx,rsi <<<< a new imagebase
fffff805`7ec3b440 488bcb mov rcx,rbx <<<< _CONTROL_AREA
fffff805`7ec3b443 e844dc0900 call nt!MiSwitchBaseAddress (fffff805`7ecd908c)
親プロセス起動直後のコールバックで、自分自身の _CONTROL_AREA を更新してしまえば子プロセス起動時にそれが反映されるのではないかと思って試してみたところうまくいきました。
自前のデバイス ドライバーを使います。
msmania/hk: Injector with kernel power
https://github.com/msmania/hk
結果は以下の通り。ロードされるたびにイメージベースが 0x10000 ずつ小さくなるようになりました。
C:\MSWORK> C:\hk\hkc.exe --donttrythis t.exe 2
C:\MSWORK> t.exe C:\MSWORK\u.exe
168c: 00007FF6BAD90000
1GB Page: 0000020200000000
2MB Page: 00000201CB600000
14d4: 000001B5A7810000 C:\MSWORK\t.exe
08a8: 00007FF6BAD80000
C:\MSWORK> t.exe C:\MSWORK\u.exe
18d0: 00007FF6BAD70000
1GB Page: 0000026580000000
2MB Page: 0000026567200000
09e8: 0000020BF1EE0000 C:\MSWORK\t.exe
120c: 00007FF6BAD60000
C:\MSWORK> t.exe C:\MSWORK\u.exe
0880: 00007FF6BAD50000
1GB Page: 0000011D80000000
2MB Page: 0000011D52400000
17a8: 000001B2C27D0000 C:\MSWORK\t.exe
0020: 00007FF6BAD40000
nt!MiSwitchBaseAddress などの関数はエクスポートされていないので、カーネルベースからのオフセットをハードコードするという禁断の方法を使っています。また、_CONTROL_AREA へアクセスするときの排他処理は完全無視です。BSOD しても構わない環境でのみ実行して下さい。
おわりに
以上、Thanksgiving の時間に思いついて実装したハックでした。
冒頭にも書きましたが、同じ実行可能ファイルでは、プロセスが違ってもイメージベースは共通になるように作られていると結論付けていいと思います。特に、_CONTROL_AREA にある ImageActive と ImageBaseOkToReuse というフラグの存在から、イメージベースを再利用させたいという意図がわりと強く感じられます。そのメリットがいまいち分かりませんが。WriteProcessMemory のテクニックを利用できるようにするため、なんでしょうかね。だとしたら公式にドキュメント化して欲しいところです。
本来なら、nt!MiReservePtes の動作ももう少し調べて、これまでの記事で書いたページングの動作と絡めたかったのですが、それだと年内に終わらなそうだったので、アドベント カレンダーの記事としてはここで終わります。
Happy holidays!