IDA PRO life easier : a python plugin turning function parameters to enum values

As an introduction to IDA python, we’re going to write a script that turns immediate values function parameters into enum values. It is a very common things to do, and a simple python script can handle it in most cases. So let’s make our lives easier with some python !

Example

AS an example, we’re going to consider the very simple example below, compiled with Visual Studio :

#include <stdio.h>
#include <stdlib.h>

enum C2_COMMANDS {
    LIST_FILE = 0xF,
    UPLOAD_FILE = 0xA,
    GET_FILE_SIZE = 0xC,
    RUN_FILE = 0xE
};

void send_command(int command_type, char* parameter) {
    printf("Sending 0x%x / %s\n", command_type, parameter);
}

int main (int argc, char** argv) {
    send_command(UPLOAD_FILE, "testfile.txt");
    send_command(LIST_FILE, "directory/test");
    send_command(GET_FILE_SIZE, "testfile.txt");
    send_command(RUN_FILE, "testfile.exe");
    send_command(LIST_FILE, "testfile.txt");
}

(You can download the binary here : test_enum.exe)

In my experience, this kind of mechanism is often used by malware for binary protocols. A general communication function is implemented, and a parameter is taken to transmit which type of message is being send.

I like to list all those values in an enum in IDA, which let me :

  • See all possible values
  • Jump to any usage instantly (using the enum values cross references)

Now in our simple example there are only 5 usages, close to one another. But in a realistic scenario, we could be looking at 30 or more, all in different places around the binary code. Detecting those calls and getting the int parameter is fairly easy, so it can be scripted in python : it will be faster, and this script can be reused later.

Script conception

The assembly of the main function looks like this:

Test_enum.exe in IDA

The send_command function is obviously sub_401000, and we want the push values 0xA, 0xF etc … to be placed in a enum.
We can recover the parameters easily by reading the last few instructions before the call : we’ll replace the immediate value of the operand of every last push before the call to sub_401000.

Writing the script

We’ll use those imports from IDA:

from idc import *
from idaapi import *
from idautils import *
from ida_enum import *

First, let’s write the function prototype. To be as generic as we can, we’ll take as parameter:

  • the address of the function (that works with struct members too)
  • the number of push to look for up in the code, which will let us choose which function parameter to consider
  • the enum name we want to create or update.

2 more parameters can be usefull :

  • How much up in the code we agree to go: it is always a good idea to limit code exploration to avoid jumping to another function or even basic block (when analysis fails for example)
  • a parameter indicating wether the values should be displayed in decimal or hex in the enum names (depends on the use case).

So here we are :

def replace_pushed_int(function_ea, target_push_n, target_enum_name, before_limit=0x30, int_type="hex"):
    """
    Replace the <target_push_n> last immediate value push before by enum value if possible

    function_ea : target function ea (will check Xref to this ea)
                  for structs use get_name_ea_simple
    target_push_n : how many push back we want, starts at 1
    target_enum_name : enum to target (created if doesn't exists)
    before_limit : how much back we agree to go
    str_type : "hex" or "dec", used for the enum value names (in hex or dec number)
    """

First thing to do will be to get the enum object, and create it if it doesn’t exists:

    target_enum = get_enum(target_enum_name)
    if target_enum == BADADDR:
        if int_type == "hex":
            target_enum = add_enum(0, target_enum_name, hex_flag())
        else:
            target_enum = add_enum(0, target_enum_name, dec_flag())

The hex_flag() and dec_flag() indicates to IDA how we want the values displayed :

HEX values in enum

It can be changed afterwards in the enum options.

Then we’ll proceed to analyse every Xref to the function, and check the instructions before the call :

    for xref in XrefsTo(function_ea, 0):
        current_ea = xref.frm
        push_n = 0

        while current_ea != BADADDR:
            current_ea = prev_head(current_ea, xref.frm - before_limit)

Note the before_limit paramter is used here, to limit of much back in the code we can go. We can then chec if the instruction at current_ea is a push, and count the number of push we saw to fond the good one :

if print_insn_mnem(current_ea) == "push":
                push_n += 1

                if push_n == target_push_n: # that's the push we are looking for

Now we need to check the push parameter : is it a register (push eax) or an immediate value like we are looking for ? We’ll use get_operand_type for that.

                    type_n = get_operand_type(current_ea, 0)
                    if type_n == 5: # immediate value
                        value = get_operand_value(current_ea, 0)

And finaly, get the enum value, create it if it doesn’t exist, and apply it to the push operand. Note that we are checking for the int_type parameter, to name the enum values here:

                        enum_value = get_enum_member(target_enum, value, 0, 0)
                        if enum_value == BADADDR:
                            # Create a new enum value
                            if int_type == "hex":
                                enum_val_name = "{:02X}".format(value)
                            else:
                                enum_val_name = str(value)

                            enum_value = add_enum_member(target_enum, get_enum_name(target_enum) + "_" + enum_val_name, value)

                        op_enum(current_ea, 0, target_enum, 0)

We could also print a warning when the type is a not an immediate value. Printing hexadecimal addresses in the console makes them clickable in IDA, which is very useful to correct errors afterwards.

                    else: # not an immediate value
                        print(f"Help needed @ {hex(current_ea)}")

                    break # Done here, break to the next Xref

And this is it, let’s run it!

Running the final result

The final plugin code can be found here: https://github.com/jeremybeaume/tools/blob/master/IDA_plugin/param_enum.py

Import your file in IDA with the menu File > Script File (Or ALT + F7). The function will be available in the python console.
We can move to sub_401000, and run:

replace_pushed_int(here(), 1, "ENUM_COMMAND")

And here is the result:

test_enum.exe after script

This simple script can be very efficient, but could still be improved. For example, it doesn’t handle cases like mov [esp], 0xA (which is the assembly produced by MingW). I still gained a crazy amount of time last week using it (more than a 100 calls) !

Leave a Reply

Your email address will not be published.