State-of-the-art binary code analysis tools

One of the features added in IDA 7.6 was automatic renaming of variables in the decompiler. 

Unlike PIT, it is not limited to stack variables but also handles variables stored in registers and not just calls but also assignments and some other expressions. It also tries to interpret function names which include a verb (get, make, fetch, query etc.) and rename the assigned result accordingly.

Triggering renaming manually

To cover situations where automatic renaming fails or insufficient, the decompiler also supports a manual action called “Quick Rename” with the default hotkey ShiftN. It can be used to propagate names across assignments and other expressions. Usually it only renames dummy variables which were not explicitly named by the user (v1, v2, etc.). Here is an incomplete list of rules used by the action:

  • by name of the opposite variable in assignments: v1 = myvar: rename v1 -> myvar1
  • by name of the opposite variable in comparisons: offset < v1: rename v1 -> offset1
  • as pointer to a well-named variable: v1 = &Value: rename v1 -> p_Value
  • by structure field in expressions: v1 = x.Next: rename v1 -> Next
  • as pointer to a structure field: v1 = &x.left: rename v1 -> p_left
  • by name of formal argument in a call: close(v1): rename v1 -> fd
  • by name of a called function: v1=create_table(): rename v1 -> table
  • by return type of called function: v1 = strchr(s, '1'): rename v1 -> str
  • by a string constant: v1 = fopen("/etc/fstab", "r"): rename v1 -> etc_fstab
  • by variable type: error_t v1: rename v1 -> error
  • standard name for the result variable: return v1: rename v1 -> ok if current function returns bool

Example: Windows driver

We’ll inspect the driver used by Process Hacker to perform actions requiring kernel mode access. On opening kprocesshacker.sys, IDA automatically applies well-known function prototype to the DriverEntry entrypoint and loads kernel mode type libraries, so the default decompilation is already decent:

NTSTATUS __stdcall DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
  NTSTATUS result; // eax
  NTSTATUS v5; // r11d
  PDEVICE_OBJECT v6; // rax
  struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-18h] BYREF
  PDEVICE_OBJECT DeviceObject; // [rsp+60h] [rbp+8h] BYREF

  qword_132C0 = (__int64)DriverObject;
  VersionInformation.dwOSVersionInfoSize = 284;
  result = RtlGetVersion(&VersionInformation);
  if ( result >= 0 )
    result = sub_15100(RegistryPath);
    if ( result >= 0 )
      RtlInitUnicodeString(&DestinationString, L"\\Device\\KProcessHacker3");
      result = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject);
      v5 = result;
      if ( result >= 0 )
        v6 = DeviceObject;
        DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)&sub_11008;
        qword_132D0 = (__int64)v6;
        DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)&sub_1114C;
        DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&sub_11198;
        DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_150EC;
        v6->Flags &= ~0x80u;
        return v5;
  return result;

However, to make sense of it we need to make some changes.  The indexes into the MajorFunction array are so-called IRP Major Function Codes which have symbolic names starting with IRP_MJ_. So we can apply the Enum action (M hotkey) to convert numbers to the corresponding symbolic constants available in the type library.

Afterwards we can rename the corresponding routines and make the pseudocode look very similar to the standard DriverEntry:

To get rid of the casts, set the proper prototypes to the dispatch routines using the “Set item type” action (Y hotkey). We can use the same prototype string for all three routines:

This works because function name is not considered to be a part of function prototype and is ignored by IDA. For the unload function, the prototype is different:
void Unload(PDRIVER_OBJECT Driver)

After setting the prototypes, no more casts:

Now we can go into KhDispatchDeviceControl to investigate how it works. Thanks to the preset prototype, the initial pseudocode looks plausible at the first glance:

NTSTATUS __stdcall KhDispatchDeviceControl(PDEVICE_OBJECT Device, PIRP Irp)

  v13 = Irp;
  CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation;
  FsContext = CurrentStackLocation->FileObject->FsContext;
  Parameters = CurrentStackLocation->Parameters.CreatePipe.Parameters;
  Options = CurrentStackLocation->Parameters.Create.Options;
  LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart;
  AccessMode = Irp->RequestorMode;
  if ( !FsContext )
    v9 = -1073741595;
    goto LABEL_105;
  if ( LowPart != -1718018045
    && LowPart != -1718018041
    && (dword_132CC == 2 || dword_132CC == 3)
    && (*FsContext & 2) == 0 )


But on closer inspection, some oddities become apparent. The Parameters member of  the _IO_STACK_LOCATION structure is a union which contains  the request-specific parameters. With insufficient information, the decompiler picked the first matching members, but they do not make sense for the request we’re dealing with. For IRP_MJ_DEVICE_CONTROL, the DeviceIoControl struct should be used.  

Thus, we can use the “Select union field” action (AltY hotkey) to choose DeviceIoControl on the three references to CurrentStackLocation->Parameters to see which parameters are  actually being used.

The references have been changed, but the variable names and types remain. In such situation, we can update the names by using Quick rename (ShiftN) on the assignments.

To get rid of the cast, we can either change the Type3InputBuffer variable type to void* manually, or simply refresh the decompilation (F5). This causes the decompiler to rerun the type derivation algorithm and update types of automatically typed variables.

Now the pseudocode more closely reflects what is going on. In particular, we can see that the first comparisons are checking the IoControlCode against some expected values, which makes more sense than the original LowPart.

Other uses

Quick rename can be useful when automatic renaming fails due to a name conflict. For example, if we go back to DriverEntry, we can see that DeviceObject is copied to a temporary variable v6:

        v6 = DeviceObject;
        DriverObject->MajorFunction[IRP_MJ_CREATE] = KhDispatchCreate;
        qword_132D0 = (__int64)v6;
        DriverObject->MajorFunction[IRP_MJ_CLOSE] = KhDispatchClose;
        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = KhDispatchDeviceControl;
        DriverObject->DriverUnload = KhUnload;
        v6->Flags &= ~0x80u;

We can rename v6 manually, or simply press ShiftN on the assignment and the decompiler will reuse the name with a numerical suffix to resolve the conflict:

        DeviceObject1 = DeviceObject;
        DriverObject->MajorFunction[IRP_MJ_CREATE] = KhDispatchCreate;
        qword_132D0 = (__int64)DeviceObject1;
        DriverObject->MajorFunction[IRP_MJ_CLOSE] = KhDispatchClose;
        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = KhDispatchDeviceControl;
        DriverObject->DriverUnload = KhUnload;
        DeviceObject1->Flags &= ~0x80u;