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 Shift–N. 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] BYREFqword_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:
NTSTATUS Dispatch(PDEVICE_OBJECT Device, PIRP Irp)
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) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]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 (Alt–Y 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 (Shift–N) 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 Shift–N 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;
Article Link: Igor’s tip of the week #76: Quick rename – Hex Rays