All Articles

Magical WinDbg VOL.2【5 章 DoPDriver の静的解析を行う】

3 章と 4 章では、DoPClient の解析を行い、パスワード(1 つ目の Flag)を特定しました。

3 章で確認した通り、DoPClient に正しいパスワードを入力すると、プログラムは Password is CorrectClear Stage1 という文字列を順に出力した後に、カーネルドライバ DoPDriver.sys をロードし、ドライバオブジェクトに対して CreateFileW 関数を使用することがわかっています。

4 章と 5 章では、2 つ目の Flag を特定するために、カーネルドライバモジュール DoPDriver の解析を行います。

もくじ

DriverEntry 関数を特定する

2 章で確認した通り、Windows のカーネルドライバモジュールである DoPDriver.sys ファイルも、ユーザモードプログラムと同じく PE ファイルフォーマットのファイルとしてビルドされています。

そのため、DoPDriver.sys ファイルについても、DoPClient.exe ファイルと同じく Binary Ninja などの解析ツールを使用して静的解析を行うことができます。

ただし、DoPDriver のようなカーネルドライバモジュールは DoPClient のようなユーザモードプログラムとは少し異なる構造になっているので注意が必要です。

例えば、C 言語で作成したユーザモードプログラムの場合は main 関数が最初に実行されますが、Windows カーネルドライバに main 関数は存在しません。

Windows のカーネルドライバでは、DriverEntry 関数がエントリポイントとして設定されており、カーネルがドライバモジュールを起動する際に初めに実行されます。1

そのため、カーネルドライバファイルの静的解析を行う場合にはまず、この DriverEntry 関数を特定することが有効なアプローチの 1 つといえます。

Windows カーネルドライバモジュールの DriverEntry 関数は以下のように定義されており、第 1 引数で DRIVER_OBJECT 構造体へのポインタが、第 2 引数でドライバのレジストリキーへのパス文字列へのポインタが受け渡されます。2

DRIVER_INITIALIZE DriverEntry;

_Use_decl_annotations_ NTSTATUS DriverEntry( 
struct _DRIVER_OBJECT  *DriverObject,
PUNICODE_STRING  RegistryPath 
)
{
    // Function body
}

なお、上記は Windows 98 以降で利用できる WDM(Windows Driver Model) ドライバを作成する場合のコードサンプルですが、ドライバの起動時に DriverEntry 関数が実行される点と、引数に DRIVER_OBJECT 構造体へのポインタとレジストリキーへのパス文字列へのポインタが受け渡される点は Windows Vista 以降でサポートされる KMDF/UMDF でも共通です。(ただし、KMDF や UMDF ドライバの場合は WdfDriverCreate 関数で WDFDRIVER オブジェクトを生成する必要があるなど、WDM ドライバとは必要な記述が大きく異なります)

DriverEntry 関数を特定する最も簡単なアプローチは、Binary Ninja などの解析ツールで DriverObject と RegistryPath を引数として受け取るエントリポイントを特定し、その中からさらに DriverObject を引数として呼び出される関数を特定することです。

DoPDriver の _start 関数

ただし、本章では DriverEntry 関数の性質を利用した他のアプローチで関数アドレスを特定してみます。

DoPDriver.sys ファイルを解析して DriverEntry 関数を特定するため、まずは、Binary Ninja で DoPDriver.sys ファイルをロードして Symbols ウインドウを参照します。

DoPDriver のシンボル一覧画面

シンボルが存在しないため DriverEntry 関数を Symbols ウインドウから見つけることはできません。

しかし、エクスポートされている関数に Wdf*Wpp* が含まれていないことから、KMDF/UMDF ではなく WDM ドライバの可能性が高いと判断できます。

例えば、シンプルな KMDF ドライバの場合、Binary Ninja で解析した結果が以下のようになります。(DoPDriver.sys の解析画面ではありません)

KMDF ドライバのシンボル一覧画面の例

WDM ドライバでは、原則として DeviceEntry 関数で最初にデバイスを表すデバイスオブジェクトを 1 つ以上生成する必要があります。4

デバイスオブジェクトの作成には IoCreateDevice 関数 5 が使用されるため、多くの場合この API 関数の呼び出し箇所を確認することで DriverEntry 関数の実行コードを特定できます。

Binary Ninja を使用する場合は Symbols ウインドウで .rdata セクションの IoCreateDevice をクリックします。

すると、Cross References ウィンドウの [Code References] 欄に 0x1400011cc のアドレスが表示されます。

Code References 画面

このアドレスをクリックすると、DRIVER_OBJECT 構造体のポインタアドレスを引数として受け取り、IoCreateDevice 関数を実行している関数を見つけることができます。

DriverEntry 関数の特定

この関数が DriverEntry 関数ですので、必要に応じて関数名のリネームを行いましょう。

これで DriverEntry 関数を特定することができました。

なお、デバイスドライバが IOCTL インターフェース 6 を実装している場合には、各 IOCTL リクエストを処理するための IOCTL ディスパッチルーチンを DriverEntry 関数で登録しているはずです。

IOCTL ディスパッチルーチンは DRIVER_OBJECT 構造体 7 のオフセット 0x70 以降に mov qword [rcx+0x70], <ディスパッチルーチンの関数ポインタ> というアセンブリコードで登録されます。8

typedef struct _DRIVER_OBJECT {
  CSHORT             Type;
  CSHORT             Size;
  PDEVICE_OBJECT     DeviceObject;
  ULONG              Flags;
  PVOID              DriverStart;
  ULONG              DriverSize;
  PVOID              DriverSection;
  PDRIVER_EXTENSION  DriverExtension;
  UNICODE_STRING     DriverName;
  PUNICODE_STRING    HardwareDatabase;
  PFAST_IO_DISPATCH  FastIoDispatch;
  PDRIVER_INITIALIZE DriverInit;
  PDRIVER_STARTIO    DriverStartIo;
  PDRIVER_UNLOAD     DriverUnload;
  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

そのため、DRIVER_OBJECT 構造体のオフセット 0x70 にアクセスしているコードを探すアプローチからも、DriverEntry 関数を特定できる可能性があります。

Binary Ninja を使用する場合はまず、画面左のメニューから {T} のマークをクリックして Types ウインドウを開きます。

そこで、DRIVER_OBJECT 構造体を検索し、クリックして情報を表示します。

DRIVER_OBJECT 構造体のオフセット 0x70 にある PDRIVER_DISPATCH MajorFunction[***]; をクリックした後に Code References ウインドウを参照すると、先ほどリネームした DriverEntry 関数のアドレスが表示されることを確認できます。

構造体情報から DriverEntry 関数を特定する

このようにして、構造体の情報などからも、特定したい関数のアドレスを参照することができる場合があります。

DriverEntry 関数を解析する

DriverEntry 関数のアドレスを特定できたので、3 章と同じように Binary Ninja の Graph ビューを使用して関数の実行コードを解析します。

以下は、DriverEntry 関数の逆アセンブル結果を Graph ビューで表示したものです。

DriverEntry 関数の Graph ビュー

最初のブロックでは IoCreateDevice 関数によるデバイスオブジェクトの作成を行っています。

DriverEntry:
mov     r11, rsp {__return_addr}
push    rbx {__saved_rbx}
sub     rsp, 0x60
lea     rax, [rel sub_140001280]
mov     dword [rsp+0x40 {DeviceName}], 0x240022
mov     qword [rcx+0x68], rax  {sub_140001280}
lea     r8, [r11-0x28 {DeviceName}]
lea     rax, [rel sub_1400011a0]
mov     r9d, 0x22
mov     qword [rcx+0x70], rax  {sub_1400011a0}
xor     edx, edx  {0x0}
lea     rax, [rel sub_140001180]
mov     qword [rcx+0x80], rax  {sub_140001180}
lea     rax, [rel data_140001590]  {u"\Device\DoPDriver"}
mov     qword [r11-0x20 {var_20}], rax  {data_140001590, u"\Device\DoPDriver"}
lea     rax, [r11+0x8 {DeviceObject}]
mov     qword [r11-0x38 {var_38}], rax {DeviceObject}
mov     byte [rsp+0x28 {var_40}], 0x0
and     dword [rsp+0x20 {var_48}], 0x0
call    qword [rel IoCreateDevice]
test    eax, eax
jns     0x140001238

IoCreateDevice 関数は以下の引数を取り、ドライバで使用するデバイスオブジェクトの作成を行う関数です。5

NTSTATUS IoCreateDevice(
  [in]           PDRIVER_OBJECT  DriverObject,
  [in]           ULONG           DeviceExtensionSize,
  [in, optional] PUNICODE_STRING DeviceName,
  [in]           DEVICE_TYPE     DeviceType,
  [in]           ULONG           DeviceCharacteristics,
  [in]           BOOLEAN         Exclusive,
  [out]          PDEVICE_OBJECT  *DeviceObject
);

Binary Ninja の Graph ビューで IoCreateDevice 関数をクリックすると、以下のように Cross References ウインドウで引数を解析してくれるので非常に便利です。

IoCreateDevice の引数に渡される値

第 1 引数の arg1 には、DriverEntry 関数が引数として受け取った DRIVER_OBJECT 構造体へのポインタが使用されます。

また、DeviceName には関数内で定義したデバイスオブジェクトの名前が使用されます。

この時、DeviceName として利用される文字列は UNICODE_STRING 構造体 9 のオブジェクトへのポインタであることに注意が必要です。

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

Windows カーネルドライバが使用する多くの関数では、ソフトウェアの安全性の観点から UNICODE_STRING 構造体などの適切なバッファ処理が考慮された安全な文字列オブジェクトを利用します。10

前述の通り、UNICODE_STRING 構造体で表現される文字列は単純な文字列ではなく、文字列のバッファサイズなどの要素を持つ構造体として IoCreateDevice などの関数に渡されます。

そのため、特にデバッガを使用して動的解析を行う場合には、この点を考慮する必要があります。

実際に IoCreateDevice 関数の引数である DeviceName を用意しているコードを読んでみます。

mov     r11, rsp {__return_addr}
push    rbx {__saved_rbx}
sub     rsp, 0x60
***
mov     dword [rsp+0x40 {DeviceName}], 0x240022
***
lea     r8, [r11-0x28 {DeviceName}]
***
lea     rax, [rel data_140001590]  {u"\Device\DoPDriver"}
mov     qword [r11-0x20 {var_20}], rax  {data_140001590, u"\Device\DoPDriver"}

Binary Ninja がアドレス RSP+0x40 のスタック領域を DeviceName として解釈しています。

しかし、mov dword [rsp+0x40 {DeviceName}], 0x240022 のコードではこの領域に文字列ではなく 0x240022 という値を格納しています。

これはなぜかというと、前述した通り DeviceName は単なる文字列ではなく、UNICODE_STRING 構造体のオブジェクトとして定義されているためです。

UNICODE_STRING 構造体では、最初の 2 バイトに Length、次の 2 バイトに MaximumLength の値が定義されます。

つまり、RSP+0x40 のスタック領域に 0x240022 を格納する操作は、Length が 0x22、MaximumLength が 0x24 の UNICODE_STRING 構造体を書き込む操作に当たります。

そして、実際の文字列(\Device\DoPDriver)を指すポインタは、UNICODE_STRING 構造体の Buffer に格納されます。

これは、カーネルデバッグで DriverEntry 関数の動的解析を行う際に、dt nt!_UNICODE_STRING RSP+0x40 コマンドを実行することでも確認できます。

kd> dt nt!_UNICODE_STRING rsp+0x40
***
+0x000 Length           : 0x22
+0x002 MaximumLength    : 0x24
+0x008 Buffer           : 0xfffff807`100a1590  "\Device\DoPDriver"

また、DeviceName の次の引数である DeviceType にはデバイスの種類を示す値が渡されます。

DoPDriver では DeviceType の値が 0x22 に設定されていますが、これは FILE_DEVICE_UNKNOWN という、Windows の標準デバイスではないことを示す値に該当します。

これらの引数を使用して IoCreateDevice 関数を実行すると、DeviceObject 引数のポインタが指すアドレスに DEVICE_OBJECT 構造体へのポインタが格納されます。

そして、デバイスオブジェクトの作成に成功した場合、0x140001238 以降の以下のコードが実行されます。

lea     rax, [rel data_140001570]  {u"\??\DoPDriver"}
mov     dword [rsp+0x50 {SymbolicLinkName}], 0x1c001a
lea     rdx, [rsp+0x40 {DeviceName}]
mov     qword [rsp+0x58 {var_10_1}], rax  {data_140001570, u"\??\DoPDriver"}
lea     rcx, [rsp+0x50 {SymbolicLinkName}]
call    qword [rel IoCreateSymbolicLink]
mov     ebx, eax
test    eax, eax
jns     0x140001274

このコードでは、IoCreateSymbolicLink 関数 12 を使用してデバイスオブジェクトと対応するシンボリックリンク(\??\DoPDriver)を作成します。

IoCreateDevice 関数で作成したデバイスオブジェクトの実体は \Device\DoPDriver に存在しますが、3 章で述べた通り、ユーザモードプロセスは \Device ディレクトリ内のデバイスオブジェクトに直接アクセスができません。

そのため、DoPClient のようなユーザモードプログラムからドライバにアクセスする必要がある場合は、カーネルドライバ側で \GLOBAL\?? ディレクトリ内にシンボリックリンクを作成し、\Device ディレクトリ内のデバイスオブジェクトの名前とリンクさせておく必要があります。

上記のコードでは、\Device\DoPDriver に存在するデバイスオブジェクトのシンボリックリンクを \??\DoPDriver として登録しています。

ディスパッチルーチンのコールバック関数を解析する

DriverEntry 関数では、デバイスオブジェクトの作成とデバイスオブジェクトに対するシンボリックリンクの登録のみを行っていました。

では、ユーザモードプログラム DoPClient が DoPDriver を使用する場合に実行されるコードはどこにあるのでしょうか。

WDM ドライバの場合、アプリケーションからの要求を受けてドライバ操作を実行するためのインターフェースとしては、ドライバオブジェクトの MajorFunction 配列のエントリで定義されるディスパッチルーチンが一般に利用されます。

ドライバオブジェクトの MajorFunction 配列には、オープン(CreateFile)やクローズ(CloseHandle)、または読み取りや書き込み(ReadFile/WriteFile)、そしてユーザモードプログラムからの DeviceIoControl 操作などと対応するコールバック関数が含まれます。

ユーザモードプログラムからデバイスドライバに対してこれらの操作を実施した場合、システムの I/O マネージャは I/O Request Packet(IRP) を割り当て、その要求と対応するドライバのディスパッチルーチンが実行されます。13

本章で確認した通り、MajorFunction 配列は DRIVER_OBJECT 構造体のオフセット 0x70 以降に PDRIVER_DISPATCH MajorFunction[***]; として定義されています。

DoPDriver では、DriverEntry 関数の 0x1400011f8 と 0x1400011fe で 2 つのディスパッチルーチンのコールバック関数がドライバオブジェクトに設定されていました。

mov     qword [rcx+0x70], rax  {sub_1400011a0}
***
mov     qword [rcx+0x80], rax  {sub_140001180}

MajorFunction 配列は PDRIVER_DISPATCH MajorFunction[***]; として定義されており、C 言語で実装する場合は DriverObject->MajorFunction[IRP_MJ_CREATE] = <コールバック関数のポインタ> のように記述します。

そして、IRP_MJ_CREATEIRP_MJ_CLOSE などの定数は wdm.h で定義されており、以下の値と対応しています。

IRP_MJ_CREATE                   0x00
IRP_MJ_CREATE_NAMED_PIPE        0x01
IRP_MJ_CLOSE                    0x02
IRP_MJ_READ                     0x03
IRP_MJ_WRITE                    0x04
IRP_MJ_QUERY_INFORMATION        0x05
IRP_MJ_SET_INFORMATION          0x06
IRP_MJ_QUERY_EA                 0x07
IRP_MJ_SET_EA                   0x08
IRP_MJ_FLUSH_BUFFERS            0x09
IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
IRP_MJ_SET_VOLUME_INFORMATION   0x0b
IRP_MJ_DIRECTORY_CONTROL        0x0c
IRP_MJ_FILE_SYSTEM_CONTROL      0x0d
IRP_MJ_DEVICE_CONTROL           0x0e
IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f
IRP_MJ_SHUTDOWN                 0x10
IRP_MJ_LOCK_CONTROL             0x11
IRP_MJ_CLEANUP                  0x12
IRP_MJ_CREATE_MAILSLOT          0x13
IRP_MJ_QUERY_SECURITY           0x14
IRP_MJ_SET_SECURITY             0x15
IRP_MJ_POWER                    0x16
IRP_MJ_SYSTEM_CONTROL           0x17
IRP_MJ_DEVICE_CHANGE            0x18
IRP_MJ_QUERY_QUOTA              0x19
IRP_MJ_SET_QUOTA                0x1a
IRP_MJ_PNP                      0x1b
IRP_MJ_PNP_POWER                IRP_MJ_PNP
IRP_MJ_MAXIMUM_FUNCTION         0x1b

つまり、DoPDriver では RCX+0x70RCX+0x80 にディスパッチルーチンが登録されていることから、DriverObject->MajorFunction[0(IRP_MJ_CREATE)]DriverObject->MajorFunction[2(IRP_MJ_CLOSE)] が実装されていると判断できます。

IRP_MJ_CREATE のコールバック関数を解析する

DriverObject->MajorFunction[0(IRP_MJ_CREATE)] に登録されてるコールバック関数のアドレスは 0x1400011a0 でした。

この関数を Binary Ninja の Graph ビューで解析した結果は以下の通りです。

アドレス 0x1400011a0 の関数

最初に実行される IofCompleteRequest 14 は、I/O 処理を終了してドライバが受け取った IRP を I/O マネージャに返却するマクロです。

このコードでは特に I/O 処理を行わずに IRP をすぐに返却しているようですが、もしデバイスドライバが IRP を使用する場合は IofCompleteRequest を実行する前に IRP に対して何らかの操作が行われます。

このコールバック関数の中では IofCompleteRequest 以外に、PsSetCreateProcessNotifyRoutine 関数 15 が実行されます。

PsSetCreateProcessNotifyRoutine は、プロセスが作成もしくは削除されるたびに実行されるルーチンの中に、デバイスドライバが指定するコールバックルーチンを追加、もしくは削除することができる関数です。

NTSTATUS PsSetCreateProcessNotifyRoutine(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  [in] BOOLEAN                        Remove
);

この関数を使用することで、デバイスドライバはシステムで新しいプロセスが作成、もしくは削除される度に任意のコードを実行することができます。

この PsSetCreateProcessNotifyRoutine は、Sysinternals の Procmon などが依存している PROCMON24.SYS でも一部利用されています。

DoPDriver 内では、PsSetCreateProcessNotifyRoutine は以下のコードで呼び出されています。

ここでは、0x1400012e0 の関数ポインタを第 1 引数として PsSetCreateProcessNotifyRoutine によりコールバックルーチンを追加しています。

xor     edx, edx  {0x0}
lea     rcx, [rel sub_1400012e0]
add     rsp, 0x28
jmp     qword [rel PsSetCreateProcessNotifyRoutine]

ここでコールバックルーチンに追加しているアドレス 0x1400012e0 の関数は 2 つ目の Flag の特定に関わるため、後の項で解析します。

IRP_MJ_CLOSE のコールバック関数を解析する

もう一方の DriverObject->MajorFunction[2(IRP_MJ_CLOSE)] に登録されているコールバック関数のアドレスは 0x140001180 でした。

この関数を逆アセンブルしたコードは以下の通りで、IofCompleteRequest による IRP の返却のみを行っています。

そのため、この関数については無視しても問題なさそうです。

sub_140001180:
sub     rsp, 0x28
and     dword [rdx+0x30], 0x0
mov     rcx, rdx
and     qword [rdx+0x38], 0x0
xor     edx, edx  {0x0}
call    qword [rel IofCompleteRequest]
xor     eax, eax  {0x0}
add     rsp, 0x28
retn     {__return_addr}

PsSetCreateProcessNotifyRoutine で登録したコールバック関数を解析する

DriverObject->MajorFunction[0(IRP_MJ_CREATE)] で登録したディスパッチルーチン内で PsSetCreateProcessNotifyRoutine を使用して登録していたコールバック関数のアドレスは 0x1400012e0 でした。

この関数の中では FLAG{The_important_process_is_ という文字列が参照されていることから、2 つ目の Flag に関わる何らかの処理が行われる関数であることを予想できます。

アドレス 0x1400012e0 の関数

まずは関数呼び出し直後の以下のコードブロックを解析します。

mov     qword [rsp+0x8 {__saved_rbx}], rbx
mov     qword [rsp+0x18 {__saved_rdi}], rdi
push    rbp {__saved_rbp}
mov     rbp, rsp {__saved_rbp}
sub     rsp, 0x80
and     qword [rbp-0x60 {var_68}], 0x0
mov     rax, rdx
mov     rcx, rax
lea     rdx, [rbp-0x60 {var_68}]
call    qword [rel PsLookupProcessByProcessId]
mov     rcx, qword [rbp-0x60 {var_68}]
call    PsGetProcessImageFileName
mov     rcx, qword [rbp-0x60 {var_68}]
mov     rbx, rax
call    qword [rel ObfDereferenceObject]
mov     rcx, rbx
call    sub_140001000
test    eax, eax
jne     0x1400013ec

このコードブロックではまずはじめに、コールバック関数が第 2 引数として受け取ったプロセス ID を引数として PsLookupProcessByProcessId 関数 16 が実行されています。

NTSTATUS PsLookupProcessByProcessId(
  [in]  HANDLE    ProcessId,
  [out] PEPROCESS *Process
);

この関数は PsSetCreateProcessNotifyRoutine で登録したコールバックルーチンですので、引数として ParentId、ProcessId、および Create の 3 つを受け取ります。17

PCREATE_PROCESS_NOTIFY_ROUTINE PcreateProcessNotifyRoutine;

void PcreateProcessNotifyRoutine(
  [in] HANDLE ParentId,
  [in] HANDLE ProcessId,
  [in] BOOLEAN Create
)
{...}

つまり、PsLookupProcessByProcessId 関数は、このコールバックルーチンが受け取った ProcessId の値を受け取り、そのプロセスの EPROCESS 構造体へのポインタを取得していることがわかります。

ここで受け取った EPROCESS 構造体へのポインタはスタック領域 RBP-0x60 に格納されます。

そして、次のコードではこのポインタアドレスを使用して PsGetProcessImageFileName 関数 18 を実行し、コールバックルーチンがキャプチャしたプロセスのイメージファイル名を取得しています。

LPSTR NTAPI PsGetProcessImageFileName(PEPROCESS Process){
    return (LPSTR)Process->ImageFileName;
}

PsGetProcessImageFileName 関数はドキュメント化されていない関数ではありますが、Sysinternals のニュースレターなどでプロセスのイメージファイル名を取得する用途で利用できる関数として紹介されています。19

PsGetProcessImageFileName 関数でイメージファイルのファイル名を取得した後は、取得したファイル名を引数としてアドレス 0x140001000 の関数が実行されます。

call    PsGetProcessImageFileName
***
mov     rbx, rax
***
mov     rcx, rbx
call    sub_140001000

この関数を Graph ビューで解析してみると、受け取ったファイル名を使用して非常に複雑な条件分岐が実装されており、一見する限りでは詳しい挙動を特定できません。

アドレス 0x140001000 の関数

しかし、以下の通りアドレス 0x140001000 の関数の戻り値が 0 の場合にのみ FLAG{The_important_process_is_ を含むコードブロックに到達できることから、アドレス 0x140001000 の関数ではプロセスのイメージファイル名に対して何らかのチェックを行っている可能性が高いと推察できます。(アドレス 0x140001000 の関数の詳細な挙動については後述します)

アドレス 0x1400011a0 の関数

アドレス 0x140001000 の関数で行われるイメージファイル名の検証にパスした場合、PsSetCreateProcessNotifyRoutine で登録したコールバック関数内では以下のコードが実行されます。

xorps   xmm0, xmm0
lea     ecx, [rax+0x1]
mov     edi, 0x100
mov     r8d, 0x67616c66
mov     edx, edi  {0x100}
movups  xmmword [rbp-0x58 {Destination.Length} {Destination.MaximumLength} {Destination.Buffer}], xmm0
call    qword [rel ExAllocatePoolWithTag]

ここで実行されている ExAllocatePoolWithTag 関数 19 は、システムにメモリプールを割り当て、割り当てられたポインタを返す関数です。

この関数の第 3 引数には割り当てたメモリ領域に指定するプールタグが指定されます。

PVOID ExAllocatePoolWithTag(
  [in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
  [in] SIZE_T                                         NumberOfBytes,
  [in] ULONG                                          Tag
);

そのため、Binary Ninja では認識されていませんが、mov r8d, 0x67616c66 の行で R8 レジスタに追加されている 0x67616c66 は、g(0x67) a(0x61) l(0x6c) f(0x66) というプールタグを指定するための 4 バイトの文字列であることがわかります。

以降の処理については、逆アセンブル結果のみから解析しようとすると少々難しく感じますが、Binary Ninja のデコンパイル結果を参照すると簡単に挙動を把握できます。

コールバックルーチンのデコンパイル結果

まず、ExAllocatePoolWithTag 関数で取得したプール領域は Destination という UNICODE_STRING 構造体の Buffer に関連づけられます。

そして、RtlInitAnsiString 関数と RtlAnsiStringToUnicodeString 関数によって、PsGetProcessImageFileName 関数で取得したイメージファイル名が UNICODE_STRING 構造体に変換されます。

最後に、UNICODE_STRING 構造体を連結する RtlAppendUnicodeStringToString 関数によって Flag 文字列にイメージファイル名が連結され FLAG{The_important_process_is_<イメージファイル名>} という文字列がプールタグ flag のメモリプールに書き込まれることになります。

イメージファイル名の検証を行う関数を解析する

ここまでの解析結果から、アドレス 0x140001000 の関数による検証をパスできるイメージファイル名を持つプロセスが起動した場合に正しい Flag がメモリプールに書き込まれることがわかりました。

前述の通り、この関数は受け取ったファイル名を使用する非常に複雑な条件分岐が実装されており、一見する限りでは詳しい挙動を特定できません。

しかし、後はこの検証をパスできるファイル名を特定すれば 2 つ目の Flag を特定できそうです。

アドレス 0x140001000 の関数

そこで、解析を簡単にするため、この関数のデコンパイル結果を参照することにします。

関数の構造としては DoPClient でパスワードの検証を行っていた関数と非常に類似しています。

int64_t sub_140001000(char* arg1)
{
  int128_t var_48;
  int64_t rax_1 = (__security_cookie ^ &var_48);
  int128_t* r10 = &var_48;

  int32_t rdx = 0;
  __builtin_memcpy(&var_48, "<ハードコードされたバイト列>", 0x34);

  char* r11 = arg1;
  int32_t r9 = 0;
  int64_t rax_3;

  while (true)
  {
    int32_t rax_2 = ((int32_t)*(uint8_t*)r11);
    if (rax_2 != 0)
    {
      if (r9 <= 6)
      {
        int32_t rdx_3;
        switch (r9)
        {
          case 0:
          {
            rdx = ((rax_2 * 0x1c) + 0xf74);
            break;
          }

      {中略}

      if (rdx == *(uint32_t*)r10)
      {
        r9 = (r9 + 1);
        r11 = &r11[1];
        r10 = ((char*)r10 + 4);
        if (r9 >= 0xd)
        {
            rax_3 = 0;
            break;
        }
        continue;
      }
    }
    rax_3 = 1;
    break;
  }
  sub_140001420((rax_1 ^ &var_48));
  return rax_3;
}

最終的に検証に成功した場合にはこの関数が 0 を返却することがわかっています。

つまり、上記のコードの while ループ内で実行されている以下の箇所がポイントになります。

if (rdx == *(uint32_t*)r10)
{
  r9 = (r9 + 1);
  r11 = &r11[1];
  r10 = ((char*)r10 + 4);
  if (r9 >= 0xd)
  {
      rax_3 = 0;
      break;
  }
  continue;
}

このコードではまず、RDX レジスタの値が R10 レジスタの持つ UINT32 型の値と一致するかを比較しています。

また、RDX レジスタの値が R10 レジスタの持つ UINT32 型の値と一致する場合には、R9 レジスタのインクリメントと、R10 および R11 レジスタの値を次の要素に変更する操作を実施しています。

そして、もしインクリメントされた R9 レジスタの値が 13(0xd) 以上となった場合には、ループを抜けて関数は 0 を返します。

R9 レジスタは 0 で初期化されており、while ループ内でインクリメントされていることから、ループ回数のカウンタであると考えられます。

また、R11 レジスタには char* r11 = arg1; のコードで関数が引数として受け取ったイメージファイル名のポインタが格納されていることがわかります。

さらに、RDX レジスタには rdx = ((rax_2 * 0x1c) + 0xf74); などのコードで、イメージファイル名の文字を 1 文字ずつ取り出したものに対して、何らかの演算を行った結果を持つようです。

ここまでの解析結果から、この関数では引数として受け取ったイメージファイル名の文字列から 1 文字ずつ取り出し、何らかの演算を行った結果をハードコードされた整数値と比較していることがわかりました。

また、初期値が 0 のループカウンタが 13(0xd) 以上となった場合にループを抜けることから、正解となるイメージファイル名の長さは 13 文字であることも特定できました。

あとは、DoPClient と同じようにデバッガを使用して総当たり攻撃を実施することで 2 つ目の Flag となる正しいイメージファイル名を特定することができます。

本章のまとめ

本章では、Binary Ninja を使用して DoPDriver の静的解析を行いました。

DoPDriver もデバッガを使用して総当たりをすることで Flag を特定できそうです。

カーネルデバッグを使った動的解析については 6 章で実施します。

各章へのリンク


  1. Windows Vistaデバイスドライバプログラミング P.36 (浜田 憲一郎 著 / ソフトバンククリエイティブ / 2007 年)

  2. DRIVERINITIALIZE コールバック関数 (wdm.h) [https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nc-wdm-driverinitialize](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nc-wdm-driver_initialize)

  3. WDF ドライバー用 DriverEntry ルーチン https://learn.microsoft.com/ja-jp/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers

  4. WDMデバイスドライバプログラミング完全ガイド 上 P.185 (Edward N.Dekker, Joseph M.Newcomer 著 / クイック 翻訳 / アスキー / 2000 年)

  5. IoCreateDevice 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice

  6. デバイス入出力制御 (IOCTL) https://learn.microsoft.com/ja-jp/windows/win32/devio/device-input-and-output-control-ioctl-

  7. DRIVEROBJECT 構造体 (wdm.h) [https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/ns-wdm-driverobject](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/ns-wdm-driver_object)

  8. リバースエンジニアリング - Pythonによるバイナリ解析技法 P.172 (Justin Seitz 著 / 安藤 慶一 翻訳 / オライリージャパン / 2010 年)

  9. UNICODESTRING 構造体 (ntdef.h) [https://learn.microsoft.com/ja-jp/windows/win32/api/ntdef/ns-ntdef-unicodestring](https://learn.microsoft.com/ja-jp/windows/win32/api/ntdef/ns-ntdef-unicode_string)

  10. Windows カーネルモード セーフ文字列ライブラリ https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/windows-kernel-mode-safe-string-library

  11. デバイスの種類の指定 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/specifying-device-types

  12. IoCreateSymbolicLink 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatesymboliclink

  13. WDMデバイスドライバプログラミング完全ガイド 上 P.146 (Edward N.Dekker, Joseph M.Newcomer 著 / クイック 翻訳 / アスキー / 2000 年)

  14. IofCompleteRequest 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocompleterequest

  15. PsSetCreateProcessNotifyRoutine 関数 (ntddk.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetcreateprocessnotifyroutine

  16. PsLookupProcessByProcessId 関数 (ntifs.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntifs/nf-ntifs-pslookupprocessbyprocessid

  17. PCREATE_PROCESS_NOTIFY_ROUTINE コールバック関数 (ntddk.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntddk/nc-ntddk-pcreateprocessnotify_routine

  18. PsGetProcessImageFileName https://doxygen.reactos.org/d2/d9f/ntoskrnl2ps2process_8c.html#a3f0cede0033a188f9525531fb104c482

  19. ExAllocatePoolWithTag 関数 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag