3 章と 4 章では、DoPClient の解析を行い、パスワード(1 つ目の Flag)を特定しました。
3 章で確認した通り、DoPClient に正しいパスワードを入力すると、プログラムは Password is Correct
と Clear Stage1
という文字列を順に出力した後に、カーネルドライバ DoPDriver.sys をロードし、ドライバオブジェクトに対して CreateFileW 関数を使用することがわかっています。
4 章と 5 章では、2 つ目の Flag を特定するために、カーネルドライバモジュール DoPDriver の解析を行います。
もくじ
- DriverEntry 関数を特定する
- DriverEntry 関数を解析する
- ディスパッチルーチンのコールバック関数を解析する
IRP_MJ_CREATE
のコールバック関数を解析するIRP_MJ_CLOSE
のコールバック関数を解析する- PsSetCreateProcessNotifyRoutine で登録したコールバック関数を解析する
- イメージファイル名の検証を行う関数を解析する
- 本章のまとめ
- 各章へのリンク
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 を引数として呼び出される関数を特定することです。
ただし、本章では DriverEntry 関数の性質を利用した他のアプローチで関数アドレスを特定してみます。
DoPDriver.sys ファイルを解析して DriverEntry 関数を特定するため、まずは、Binary Ninja で DoPDriver.sys ファイルをロードして Symbols ウインドウを参照します。
シンボルが存在しないため DriverEntry 関数を Symbols ウインドウから見つけることはできません。
しかし、エクスポートされている関数に Wdf*
や Wpp*
が含まれていないことから、KMDF/UMDF ではなく WDM ドライバの可能性が高いと判断できます。
例えば、シンプルな KMDF ドライバの場合、Binary Ninja で解析した結果が以下のようになります。(DoPDriver.sys の解析画面ではありません)
WDM ドライバでは、原則として DeviceEntry 関数で最初にデバイスを表すデバイスオブジェクトを 1 つ以上生成する必要があります。4
デバイスオブジェクトの作成には IoCreateDevice 関数 5 が使用されるため、多くの場合この API 関数の呼び出し箇所を確認することで DriverEntry 関数の実行コードを特定できます。
Binary Ninja を使用する場合は Symbols ウインドウで .rdata セクションの IoCreateDevice をクリックします。
すると、Cross References ウィンドウの [Code References] 欄に 0x1400011cc のアドレスが表示されます。
このアドレスをクリックすると、DRIVER_OBJECT
構造体のポインタアドレスを引数として受け取り、IoCreateDevice 関数を実行している関数を見つけることができます。
この関数が 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 関数のアドレスを特定できたので、3 章と同じように Binary Ninja の 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 ウインドウで引数を解析してくれるので非常に便利です。
第 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_CREATE
や IRP_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+0x70
と RCX+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 ビューで解析した結果は以下の通りです。
最初に実行される 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 に関わる何らかの処理が行われる関数であることを予想できます。
まずは関数呼び出し直後の以下のコードブロックを解析します。
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 の関数の戻り値が 0 の場合にのみ FLAG{The_important_process_is_
を含むコードブロックに到達できることから、アドレス 0x140001000 の関数ではプロセスのイメージファイル名に対して何らかのチェックを行っている可能性が高いと推察できます。(アドレス 0x140001000 の関数の詳細な挙動については後述します)
アドレス 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 を特定できそうです。
そこで、解析を簡単にするため、この関数のデコンパイル結果を参照することにします。
関数の構造としては 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 章 環境構築
- 2 章 DoPClient と DoPDriver の表層解析
- 3 章 DoPClient の静的解析を行う
- 4 章 DoPClient を動的解析する
- 5 章 DoPDriver の静的解析を行う
- 6 章 DoPDriver を動的解析する
-
Windows Vistaデバイスドライバプログラミング P.36 (浜田 憲一郎 著 / ソフトバンククリエイティブ / 2007 年)
↩ -
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)
↩ -
WDF ドライバー用 DriverEntry ルーチン https://learn.microsoft.com/ja-jp/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
↩ -
WDMデバイスドライバプログラミング完全ガイド 上 P.185 (Edward N.Dekker, Joseph M.Newcomer 著 / クイック 翻訳 / アスキー / 2000 年)
↩ -
IoCreateDevice 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice
↩ -
デバイス入出力制御 (IOCTL) https://learn.microsoft.com/ja-jp/windows/win32/devio/device-input-and-output-control-ioctl-
↩ -
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)
↩ -
リバースエンジニアリング - Pythonによるバイナリ解析技法 P.172 (Justin Seitz 著 / 安藤 慶一 翻訳 / オライリージャパン / 2010 年)
↩ -
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)
↩ -
Windows カーネルモード セーフ文字列ライブラリ https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/windows-kernel-mode-safe-string-library
↩ -
デバイスの種類の指定 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/specifying-device-types
↩ -
IoCreateSymbolicLink 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatesymboliclink
↩ -
WDMデバイスドライバプログラミング完全ガイド 上 P.146 (Edward N.Dekker, Joseph M.Newcomer 著 / クイック 翻訳 / アスキー / 2000 年)
↩ -
IofCompleteRequest 関数 (wdm.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocompleterequest
↩ -
PsSetCreateProcessNotifyRoutine 関数 (ntddk.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetcreateprocessnotifyroutine
↩ -
PsLookupProcessByProcessId 関数 (ntifs.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntifs/nf-ntifs-pslookupprocessbyprocessid
↩ -
↩PCREATE_PROCESS_NOTIFY_ROUTINE
コールバック関数 (ntddk.h) https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntddk/nc-ntddk-pcreateprocessnotify_routine -
PsGetProcessImageFileName https://doxygen.reactos.org/d2/d9f/ntoskrnl2ps2process_8c.html#a3f0cede0033a188f9525531fb104c482
↩ -
ExAllocatePoolWithTag 関数 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag
↩