LoginSignup
5
5

More than 3 years have passed since last update.

実行可能ファイルはどこにロードされるのか

Last updated at Posted at 2020-12-23

はじめに

貸し切り状態の 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 のものです。

プロセスが実行されるまで

プロセスが作成されるときのカーネル側のエントリポイントは 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 を持ち、マッピング状態を管理しています。

図の上部にある黄色で示したオブジェクトは、プロセスが作成されるたびに新しく作られます。一方、下部の青色で示したオブジェクトは、プロセスが終了しても維持され、次回起動時に再利用されるオブジェクトです。

network.PNG

イメージベースはどこから

プロセスが作成されると、そのイメージベースが 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!

5
5
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5