All Articles

Magical WinDbg VOL.2 [Chapter 5: Static Analysis of DoPDriver]

This page has been machine-translated from the original page.

In Chapters 3 and 4, we analyzed DoPClient and identified the password (the first Flag).

As confirmed in Chapter 3, when the correct password is entered into DoPClient, the program prints the strings Password is Correct and Clear Stage1 in sequence, then loads the kernel driver DoPDriver.sys and uses the CreateFileW function on the driver object.

In Chapters 4 and 5, we analyze the kernel driver module DoPDriver in order to identify the second Flag.

Table of Contents

Identifying the DriverEntry Function

As confirmed in Chapter 2, the DoPDriver.sys file, which is a Windows kernel driver module, is also built as a file in PE format, just like a user-mode program.

Therefore, DoPDriver.sys can also be statically analyzed with analysis tools such as Binary Ninja, just like DoPClient.exe.

However, you need to be careful because a kernel driver module like DoPDriver has a slightly different structure from a user-mode program like DoPClient.

For example, a user-mode program written in C starts from the main function, but Windows kernel drivers do not have a main function.

In Windows kernel drivers, the DriverEntry function is configured as the entry point, and it is executed first when the kernel starts the driver module.1

Therefore, when statically analyzing a kernel driver file, identifying this DriverEntry function first is one useful approach.

The DriverEntry function of a Windows kernel driver module is defined as follows. The first argument is a pointer to a DRIVER_OBJECT structure, and the second argument is a pointer to the path string of the driver’s registry key.2

DRIVER_INITIALIZE DriverEntry;

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

Note that the example above is code for creating a WDM (Windows Driver Model) driver, which has been available since Windows 98. However, the fact that the DriverEntry function is executed when the driver starts and that pointers to a DRIVER_OBJECT structure and a registry-key path string are passed as arguments is also common to KMDF/UMDF, which are supported from Windows Vista onward. (That said, KMDF and UMDF drivers require significantly different code from WDM drivers—for example, they must create a WDFDRIVER object with the WdfDriverCreate function.)3

The easiest way to identify the DriverEntry function is to use an analysis tool such as Binary Ninja to find the entry point that receives DriverObject and RegistryPath as arguments, and then identify the function called with DriverObject as an argument.

The _start function in DoPDriver

However, in this chapter, we will try another approach that uses the characteristics of the DriverEntry function to identify its address.

To identify the DriverEntry function by analyzing DoPDriver.sys, first load DoPDriver.sys into Binary Ninja and open the Symbols window.

List of symbols in DoPDriver

Because no symbols are present, you cannot find the DriverEntry function from the Symbols window.

However, because the exported functions do not include Wdf* or Wpp*, we can judge that it is more likely a WDM driver than a KMDF/UMDF driver.

For example, a simple KMDF driver looks like the following when analyzed in Binary Ninja. (This is not the DoPDriver.sys analysis screen.)

Example symbol list for a KMDF driver

In WDM drivers, as a rule, one or more device objects representing devices must first be created in DriverEntry.4

Because device objects are created with the IoCreateDevice function 5, in many cases you can identify the code executed by the DriverEntry function by checking where this API function is called.

If you are using Binary Ninja, click IoCreateDevice in the .rdata section of the Symbols window.

Then the address 0x1400011cc appears in the [Code References] area of the Cross References window.

Code References screen

Clicking this address reveals a function that receives a pointer to a DRIVER_OBJECT structure as an argument and calls the IoCreateDevice function.

Identifying the DriverEntry function

This function is DriverEntry, so rename it if necessary.

At this point, we have identified the DriverEntry function.

Also, if a device driver implements an IOCTL interface 6, it should register an IOCTL dispatch routine in DriverEntry to handle each IOCTL request.

An IOCTL dispatch routine is registered with assembly code such as mov qword [rcx+0x70], <function pointer of the dispatch routine> starting at offset 0x70 of the DRIVER_OBJECT structure 7.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;

Therefore, another possible way to identify the DriverEntry function is to look for code that accesses offset 0x70 of the DRIVER_OBJECT structure.

If you are using Binary Ninja, first click the {T} icon in the menu on the left side of the screen to open the Types window.

There, search for the DRIVER_OBJECT structure and click it to display its information.

After clicking PDRIVER_DISPATCH MajorFunction[***]; at offset 0x70 of the DRIVER_OBJECT structure and then checking the Code References window, you can confirm that it shows the address of the DriverEntry function we renamed earlier.

Identifying the DriverEntry function from structure information

In this way, information from structures and similar sources can sometimes also be used to refer to the address of the function you want to identify.

Analyzing the DriverEntry Function

Now that we have identified the address of the DriverEntry function, we can analyze its execution code in Binary Ninja’s Graph view, just as we did in Chapter 3.

The following is the disassembly of the DriverEntry function shown in Graph view.

Graph view of the DriverEntry function

The first block creates a device object with the IoCreateDevice function.

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

The IoCreateDevice function takes the following arguments and creates the device object used by the driver.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
);

Clicking the IoCreateDevice function in Binary Ninja’s Graph view is very convenient because it analyzes the arguments for you in the Cross References window, as shown below.

Values passed as arguments to IoCreateDevice

The first argument, arg1, uses the pointer to the DRIVER_OBJECT structure that the DriverEntry function received as an argument.

Also, DeviceName uses the name of the device object defined inside the function.

At this point, note that the string used as DeviceName is a pointer to an object of the UNICODE_STRING structure 9.

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

Many functions used by Windows kernel drivers use safe string objects, such as the UNICODE_STRING structure, that take proper buffer handling into account from the standpoint of software safety.10

As mentioned above, a string represented by a UNICODE_STRING structure is not a simple string. It is passed to functions such as IoCreateDevice as a structure that includes elements such as the buffer size.

For that reason, you need to keep this point in mind especially when performing dynamic analysis with a debugger.

Let’s actually read the code that prepares DeviceName, which is an argument to IoCreateDevice.

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 interprets the stack area at address RSP+0x40 as DeviceName.

However, in the code mov dword [rsp+0x40 {DeviceName}], 0x240022, the value stored in that area is 0x240022 rather than a string.

This is because, as explained earlier, DeviceName is defined not as a plain string but as an object of the UNICODE_STRING structure.

In a UNICODE_STRING structure, the first 2 bytes store Length and the next 2 bytes store MaximumLength.

In other words, storing 0x240022 in the stack area at RSP+0x40 corresponds to writing a UNICODE_STRING structure whose Length is 0x22 and whose MaximumLength is 0x24.

The pointer to the actual string (\Device\DoPDriver) is stored in the Buffer field of the UNICODE_STRING structure.

You can also confirm this by running the dt nt!_UNICODE_STRING rsp+0x40 command when dynamically analyzing the DriverEntry function during kernel debugging.

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

Also, DeviceType, the argument after DeviceName, receives a value indicating the device type.

In DoPDriver, DeviceType is set to 0x22, which corresponds to FILE_DEVICE_UNKNOWN, indicating that it is not a standard Windows device.11

When the IoCreateDevice function is called with these arguments, a pointer to the DEVICE_OBJECT structure is stored at the address pointed to by the DeviceObject argument.

If creating the device object succeeds, the following code after 0x140001238 is executed.

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

This code uses the IoCreateSymbolicLink function 12 to create a symbolic link (\??\DoPDriver) corresponding to the device object.

The actual device object created by the IoCreateDevice function exists at \Device\DoPDriver, but as explained in Chapter 3, a user-mode process cannot directly access device objects in the \Device directory.

Therefore, if a user-mode program such as DoPClient needs to access the driver, the kernel driver must create a symbolic link in the \GLOBAL\?? directory and link it to the name of the device object in the \Device directory.

In the code above, the symbolic link for the device object at \Device\DoPDriver is registered as \??\DoPDriver.

Analyzing the Dispatch Routine Callback Functions

In the DriverEntry function, only the device object was created and the symbolic link for that device object was registered.

So where is the code that runs when the user-mode program DoPClient uses DoPDriver?

In the case of WDM drivers, the interface commonly used to execute driver operations in response to requests from applications is the dispatch routine defined by the entries in the MajorFunction array of the driver object.

The MajorFunction array of the driver object contains callback functions corresponding to open (CreateFile) and close (CloseHandle), as well as read and write (ReadFile/WriteFile), and DeviceIoControl operations from user-mode programs.

When a user-mode program performs these operations on a device driver, the system’s I/O manager allocates an I/O Request Packet (IRP) and executes the driver’s dispatch routine corresponding to that request.13

As confirmed earlier in this chapter, the MajorFunction array is defined as PDRIVER_DISPATCH MajorFunction[***]; starting at offset 0x70 of the DRIVER_OBJECT structure.

In DoPDriver, the callback functions for two dispatch routines were set in the driver object at 0x1400011f8 and 0x1400011fe in the DriverEntry function.

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

The MajorFunction array is defined as PDRIVER_DISPATCH MajorFunction[***];, and in a C implementation it would be written as DriverObject->MajorFunction[IRP_MJ_CREATE] = <pointer to the callback function>.

Constants such as IRP_MJ_CREATE and IRP_MJ_CLOSE are defined in wdm.h and correspond to the following values.

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

In other words, because DoPDriver registers dispatch routines at RCX+0x70 and RCX+0x80, we can determine that DriverObject->MajorFunction[0(IRP_MJ_CREATE)] and DriverObject->MajorFunction[2(IRP_MJ_CLOSE)] are implemented.

Analyzing the IRP_MJ_CREATE Callback Function

The callback function registered in DriverObject->MajorFunction[0(IRP_MJ_CREATE)] is at address 0x1400011a0.

The result of analyzing this function in Binary Ninja’s Graph view is shown below.

Function at address 0x1400011a0

The IofCompleteRequest function 14 called first is a macro that completes I/O processing and returns the IRP received by the driver to the I/O manager.

This code appears to return the IRP immediately without performing any I/O processing, but if a device driver uses an IRP, some operation is performed on that IRP before IofCompleteRequest is executed.

Besides IofCompleteRequest, this callback function also executes the PsSetCreateProcessNotifyRoutine function 15.

PsSetCreateProcessNotifyRoutine is a function that can add or remove a callback routine specified by the device driver to the routines executed each time a process is created or deleted.

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

By using this function, a device driver can execute arbitrary code whenever a new process is created or deleted in the system.

PsSetCreateProcessNotifyRoutine is also used in part by PROCMON24.SYS, which Sysinternals Procmon depends on.

Inside DoPDriver, PsSetCreateProcessNotifyRoutine is called with the following code.

Here, the function pointer at 0x1400012e0 is passed as the first argument, and PsSetCreateProcessNotifyRoutine adds it as a callback routine.

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

Because the function at address 0x1400012e0 that is added to the callback routine is related to identifying the second Flag, we will analyze it in a later section.

Analyzing the IRP_MJ_CLOSE Callback Function

The address of the callback function registered in the other DriverObject->MajorFunction[2(IRP_MJ_CLOSE)] is 0x140001180.

The disassembled code for this function is shown below, and it does nothing except return the IRP through IofCompleteRequest.

Therefore, it seems safe to ignore this function.

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}

Analyzing the Callback Function Registered with PsSetCreateProcessNotifyRoutine

The address of the callback function registered with PsSetCreateProcessNotifyRoutine inside the dispatch routine registered by DriverObject->MajorFunction[0(IRP_MJ_CREATE)] was 0x1400012e0.

Because this function references the string FLAG{The_important_process_is_, we can expect that it performs some processing related to identifying the second Flag.

Function at address 0x1400012e0

First, let’s analyze the following code block immediately after the function call.

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

This code block first calls the PsLookupProcessByProcessId function 16 with the process ID that the callback function received as its second argument.

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

Because this function is the callback routine registered with PsSetCreateProcessNotifyRoutine, it receives three arguments: ParentId, ProcessId, and Create.17

PCREATE_PROCESS_NOTIFY_ROUTINE PcreateProcessNotifyRoutine;

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

In other words, the PsLookupProcessByProcessId function receives the ProcessId value taken by this callback routine and obtains a pointer to the EPROCESS structure for that process.

The pointer to the EPROCESS structure obtained here is stored in the stack area RBP-0x60.

The next code uses this pointer address to execute the PsGetProcessImageFileName function 18, obtaining the image file name of the process captured by the callback routine.

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

PsGetProcessImageFileName is an undocumented function, but it has been introduced in sources such as Sysinternals newsletters as a function that can be used to obtain a process image file name.

After PsGetProcessImageFileName obtains the image file name, the function at address 0x140001000 is executed using the obtained file name as an argument.

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

When analyzing this function in Graph view, you can see that it implements very complex branching using the received file name, and at first glance it is difficult to determine the detailed behavior.

Function at address 0x140001000

However, as shown below, a code block containing FLAG{The_important_process_is_ can be reached only when the function at address 0x140001000 returns 0, so it is highly likely that the function at address 0x140001000 performs some kind of check on the process image file name. (The detailed behavior of the function at address 0x140001000 will be described later.)

Function at address 0x1400011a0

If the image file name passes the validation performed by the function at address 0x140001000, the following code is executed inside the callback function registered with 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]

The ExAllocatePoolWithTag function 19 executed here allocates a memory pool in the system and returns the allocated pointer.

This function’s third argument specifies the pool tag to assign to the allocated memory area.

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

Therefore, although Binary Ninja does not recognize it, the 0x67616c66 loaded into the R8 register by the line mov r8d, 0x67616c66 is a 4-byte string used to specify the pool tag g(0x67) a(0x61) l(0x6c) f(0x66).

The subsequent processing is somewhat difficult to analyze from the disassembly result alone, but if you refer to Binary Ninja’s decompilation, the behavior becomes easy to understand.

Decompilation result of the callback routine

First, the pool region obtained by ExAllocatePoolWithTag is associated with the Buffer of a UNICODE_STRING structure named Destination.

Then, the image file name obtained by PsGetProcessImageFileName is converted into a UNICODE_STRING structure by the RtlInitAnsiString and RtlAnsiStringToUnicodeString functions.

Finally, the image file name is appended to the Flag string by RtlAppendUnicodeStringToString, which concatenates UNICODE_STRING structures, and the string FLAG{The_important_process_is_<image file name>} is written into the memory pool with the pool tag flag.

Analyzing the Function That Validates the Image File Name

From the analysis so far, we now know that if a process with an image file name that passes the validation performed by the function at address 0x140001000 starts, the correct Flag will be written into the memory pool.

As mentioned earlier, this function implements extremely complex branching that uses the received file name, and at first glance it is impossible to determine the detailed behavior.

However, if we can identify the file name that passes this validation, it looks like we should be able to identify the second Flag.

Function at address 0x140001000

So, to make the analysis easier, we will refer to the decompilation result of this function.

The structure of the function is very similar to the function in DoPClient that was validating the password.

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, "<hardcoded byte sequence>", 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;
          }

      {omitted}

      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;
}

We know that if validation ultimately succeeds, this function returns 0.

In other words, the important part is the following section executed inside the while loop above.

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

In this code, the first thing it does is compare the value in the RDX register with the UINT32 value pointed to by the R10 register.

Also, if the value in the RDX register matches the UINT32 value pointed to by the R10 register, it increments the R9 register and updates the values of the R10 and R11 registers to the next elements.

Then, if the incremented value in the R9 register becomes 13 (0xd) or more, the loop exits and the function returns 0.

Because the R9 register is initialized to 0 and incremented inside the while loop, it can be considered a counter for the number of loop iterations.

We can also see that the R11 register stores the pointer to the image file name received by the function as an argument, as shown by the code char* r11 = arg1;.

Furthermore, it appears that the RDX register holds the result of some operation performed on the characters of the image file name one at a time, as shown by code such as rdx = ((rax_2 * 0x1c) + 0xf74);.

From the analysis so far, we can see that this function takes the image file name string received as an argument, extracts it one character at a time, performs some operation on it, and compares the result with hardcoded integer values.

Also, because the loop exits when the loop counter, whose initial value is 0, becomes 13 (0xd) or more, we can determine that the correct image file name is 13 characters long.

All that remains is to perform a brute-force attack with a debugger, just as we did for DoPClient, and we should be able to identify the correct image file name that becomes the second Flag.

Summary

In this chapter, we performed static analysis of DoPDriver using Binary Ninja.

It seems likely that the Flag in DoPDriver can also be identified by brute-forcing it with a debugger.

We will perform dynamic analysis using kernel debugging in Chapter 6.


  1. Windows Vista Device Driver Programming, p.36 (浜田 憲一郎 / SoftBank Creative / 2007)

  2. DRIVERINITIALIZE callback function (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. DriverEntry routine for WDF drivers https://learn.microsoft.com/ja-jp/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers

  4. Complete Guide to WDM Device Driver Programming, Vol. 1, p.185 (Edward N. Dekker, Joseph M. Newcomer / translated by クイック / ASCII / 2000)

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

  6. Device Input and Output Control (IOCTL) https://learn.microsoft.com/ja-jp/windows/win32/devio/device-input-and-output-control-ioctl-

  7. DRIVEROBJECT structure (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. Reverse Engineering - Binary Analysis Techniques with Python, p.172 (Justin Seitz / translated by 安藤 慶一 / O’Reilly Japan / 2010)

  9. UNICODESTRING structure (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 kernel-mode safe string library https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/windows-kernel-mode-safe-string-library

  11. Specifying device types https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/specifying-device-types

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

  13. Complete Guide to WDM Device Driver Programming, Vol. 1, p.146 (Edward N. Dekker, Joseph M. Newcomer / translated by クイック / ASCII / 2000)

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

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

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

  17. PCREATE_PROCESS_NOTIFY_ROUTINE callback function (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 function https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag