Latest available version: IDA and decompilers v8.4.240320 see all releases
Hex-Rays logo State-of-the-art binary code analysis tools
email icon

Normally, to change environment variables in a running process, one has to terminate the process, edit the environment variables and re-run the process. In this blog entry we are going to write an IDAPython script that allows us to add, edit or delete environment variables in a running process directly. To achieve this we will use Appcall to manage the variables and a custom viewer that serves as the graphical interface.

envedit.gif

Background

In MS Windows, environment variables can be managed using various API calls. For our script we are only interested in 3 calls:

  • GetEnvironmentStrings: Returns a block of memory pointing to all the environment variables in the process. This block is a list of zero terminated strings, the end of the list is marked with two \0 characters (such list is also known as MULTI_SZ)
  • FreeEnvironmentStrings: Frees the block returned by GetEnvironmentStrings()
  • SetEnvironmentVariable: Creates or edits an environment variable

In order to modify the environment variables in a process, we need to issue those API calls in the context of that process. One solution is to inject a DLL into the running process and ask it to call those APIs. Another simpler solution is to write a script that uses Appcall to issue those API calls.

Setting up Appcall

Let us use Appcall to retrieve 3 callable objects corresponding to the APIs in question:

GetEnvironmentStrings =
    Appcall.proto("kernel32_GetEnvironmentStringsA",
                  "unsigned int __stdcall GetEnvironmentStrings();")
SetEnvironmentVariable =
    Appcall.proto("kernel32_SetEnvironmentVariableA",
                  "int __stdcall SetEnvironmentVariable(const char *lpName, const char *lpValue);")
FreeEnvironmentStrings =
    Appcall.proto("kernel32_FreeEnvironmentStringsA",
                  "int __stdcall FreeEnvironmentStrings(unsigned int block);")

Now we can use those callable objects to call the APIs in the context of the debugged process, for example:

# Add a new environment variable
SetEnvironmentVariable("MY_VAR", "Some value")

Please note that:

  • Both GetEnvironmentStrings() and FreeEnvironmentStrings() prototypes are re-declared to accept an unsigned int instead of a character pointer because Appcall does not support the MULTI_SZ type.
  • We are using the ASCII version of those APIs. Modifying the script to work with the unicode version is not very difficult to achieve.

Retrieving all environment variables

We need to write a function to retrieve all the environment variables in a list:

  1. Call the GetEnvironmentStrings() to retrieve a pointer to the environment block in the process. Save that pointer.
  2. Read the debuggee’s memory at that block and retrieve the variables accordingly.
  3. Free the block by calling FreeEnvironmentStrings()

To read the debuggee’s memory, we can use idc.Bytes(), idaapi.get_many_bytes() or idaapi.dbg_read_memory(). Since we don’t know how big the block is, we will read one character at a time and process it. For demonstration purposes, we will use yet another mechanism to read from the debuggee’s memory as if we were reading from a file. We will use the loader_input_t class followed by a call to open_memory() function:

def Variables():
    """Returns all environment blocks"""
    # Get all environment strings
    env_ptr = GetEnvironmentStrings()
    if env_ptr == 0:
        return None
    # Always refresh the memory after a call
    # that could change the memory configuration
    idc.RefreshDebuggerMemory()
    # Open process memory
    f = idaapi.loader_input_t()
    f.open_memory(env_ptr)
    # Parse all environment variables into a list of variables
    vars = []
    var = []
    while True:
        ch = f.get_char()
        if not ch:
            break
        if ch == '\x00':
            # double-null -> end of list
            if not var:
                break
            vars.append(''.join(var))
            var = []
        else:
            var.append(ch)
    f.close()
    # Free the block
    FreeEnvironmentStrings(env_ptr)
    return vars

Writing a custom viewer to manage the environment variables

Now that we have all the required supporting code, we can write a custom viewer that does the following:

  • Retrieves all the environment variables: call GetEnvironmentStrings() and parse the result
    • Parse each environment variable into KEY/VALUE components
    • Add a colored line into the custom viewer with different colors for the KEY and VALUE
  • Add popup menu entries and handle keypresses to support three operations:
    • Insert: Ask the user for a key and value using the idaapi.askstr() then call the SetEnvironmentVariable() API as shown in the previous example
    • Edit: Retrieve the cursor position, extract the key name, ask the user for a new value only (w/o asking for a key) and call the SetEnvironmentVariable() API
    • Delete: Retrieve the key name and call SetEnvironmentVariable() API with value pointing to NULL

Before running the script, make sure that the debugged process is suspended. Right-click on an environment variable to edit/insert or delete.

Please download the script from here.