CS6038/CS5138 Malware Analysis, UC

Course content for UC Malware Analysis

View on GitHub
16 February 2020

Simple Program Flow Editing with Immunity

by Coleman Kane

Now, we will perform some program flow editing on a few example binaries. This will help better utilize some of the features that make Immunity Debugger so powerful for working with malware. Once of the common challenges is when malware running in our lab doesn’t follow the same execution paths that it would follow on a user’s system. In these events, we need to modify program flow to accommodate this and modify program behavior so it performs the operations we want to analyze.

Install the “MingW” developmnent suite in your Kali VM to be able to cross-compile Windows binaries on Linux:

apt install mingw-w64

Despite the name, this will install the 32-bit utilities, too, which is mainly what we want. This gives us the ability to build new Windows executables from C and C++ code within the Kali environment.

If you recall, when we analyzed the “Revolution Backdoor” sample, there were only about 4-5 functions defined in the C++ code, but Ghidra displayed many functions to us. Through analysis, we determined that the entry point of the program is not pointing at the main function that was written into the source code, but rather to another function which did some start-up work and eventually called our main function once a number of additional “start up” work was completed.

Example code for the exercises below:

Writing a simplified EXE

Building a simplified EXE is helpful, as it allows us to analyze specific code without having to navigate through all of the additional “standard library” code provided by the compiler. We can write a function like the following, using the WinMain function as our entry point, and have a simpler analysis project:

#include <windows.h>

int WINAPI
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpszCmdLine, int nCmdShow) {
  MessageBoxA(NULL, "TEST", "TEST", MB_OK);
  return 1;
}

We also need to tell the compiler this is what we want, as it will not automatically determine what we are trying to do here. Using the following command, with the above code in ex1.cpp, we can compile a slimmed-down EXE:

i686-w64-mingw32-g++ -o ex1.exe -mconsole -nostdlib -s ex1.cpp -luser32

Looking at the file, we can see that it is only 2048 bytes, which is tiny:

-rwxr-xr-x 1 root root 2048 Feb 16 23:24 ex1.exe

If I left out the -nostdlib parameter, additional code will be added, making the EXE grow to over 11kB in size.

Analyzing our Simple EXE

The EXE that was built contains a single function, named WinMain in the source (but the -s option will strip the symbols for us), which simply calls the MessageBoxA function to display a dialog to the user, pausing program execution, until the user clicks “OK”. It then returns to Windows with a return value of 1.

We can load this up in Immunity Debugger and we can easily make out the components of the program from the source code, in the disassembly view:

Immunity Debugger Looking at ex1.exe

As you can see, the only code displayed in the disassembly view (and, if you load it up in Ghidra, the only code at all) is the disassembly for the WinMain function that was provided above. The program is paused, and the first instruction to execute next is:

PUSH EBP

Looking at the code, our function call begins at line 4, where the value 0 is moved onto the stack, representing the value that the MB_OK macro represents. It is worth noting that, when arguments to functions are passed on the stack, they are typically moved onto the stack in reverse order, from last to first (right to left).

Doing a step-into (F7) or step-over (F8) 8 times places the EIP and the cursor on the CALL EAX line.

Immunity Debugger on CALL EAX

Immunity Debugger recognizes this as a function call to MessageBoxA and updates the stack view and the CPU state’s register view to reflect this. In the stack view (lower right), you can now see the parameter names and which values they correspond to have been noted by the debugger. You can even see that the Style parameter was interpreted for us as MB_OK|MB_APPLMODAL, which was very handy. These values are documented in the MSDN documentation and this informs us about how the dialog will behave. This is a key benefit to Immunity Debugger - it contains a bunch of features that will decode well-known parameter types in Windows for us.

MessageBoxA Call Parameter Stack

Doing a step-over (F8) at this point, will cause the dialog box to display. It may pop up behind your debugger, so you might need to Alt+Tab to find it. You’ll also notice that the lower-right corner of the debugger window displays “Running” rather than “Paused” now. This is because the code inside of the MessageBoxA function contains a loop which will loop forever until the “OK” button is clicked. When we go ahead and click this button, the program will get Paused again, but this time will be waiting on the following line of code:

SUB ESP,10

Next, click the “Restart Program” button, which looks like the two arrows pointing left: ⏪. This will restore the program execution state to where we first started.

If you run the program from the beginning, using the “Play” button (Run, F9) the program will run, then wait for you to click “OK”, then terminate. Restart the program again after you try this, and we’ll move on to the next lesson.

Modifying Program Flow

The next example we will work from is a modification to the last example, which will be named ex2.cpp:

#include <windows.h>
#include <wincon.h>

void
writestr(const char *outstr) {
  int slen = lstrlenA(outstr);
  HANDLE std_out = GetStdHandle(STD_OUTPUT_HANDLE);
 
  WriteConsoleA(std_out, outstr, slen, NULL, NULL);
}

int WINAPI
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpszCmdLine, int nCmdShow) {
  MessageBoxA(NULL, "TEST", "TEST", MB_OK);
  writestr("This is printed to the console!\n");
  return 1;
}

The above code adds a new function call just after the MessageBoxA call that we looked at last time. So basically, this does one more action. As I’m trying to simplify the example, I’ve written a very simplistic implementation of the puts function, named writestr, which writes a string to the command line console. In a nutshell, the above code will run, wait for you to click “OK” in the dialog, and then write the string “This is printed to the console!” after that, and then exits.

Compiling this program is similar to before, with a slight modification that we have to tell the compiler that WinMain is the entry point. I helpful note here, C++ will change the function name to _WinMain@16, and we have to provide that name to the linker in order to get the program to build and execute properly. Otherwise, the first function encountered, writestr, will be presumed to be the entry point. This is definitely not what we want.

i686-w64-mingw32-g++ -o ex2.exe -mconsole -nostdlib -s ex2.cpp -luser32 -lkernel32 -Wl,-e_WinMain@16

Again, ex2.exe is a small 2048kB program:

-rwxr-xr-x 1 root root 2048 Feb 17 14:18 ex2.exe

Loading ex2.exe up into ImmunityDebugger, I see that the entry point is no longer at 0x00401000, but now points to 0x40105e (your mileage may vary slightly). In fact, I can scroll the disassembly view up in Immunity and reveal the disassembly for my writestr function, in addition to the disassembly for my WinMain function.

Disassembly of WinMain and writestr

The first goal that I will present will be: We would like to use the debugger to execute the program such that it will display the message to the console, but never prompt the user to click OK to the dialog box.

Bypassing using stepping

The first and most straightforward approach would be to use single-stepping to bypass the code. For its part, when you load the program into Immunity Debugger, it also pops up a terminal window that will display any console output.

What we will do next is single-step until the cursor is on, but doesn’t execute, the CALL EAX line (the one listed in the image at 0x00401088). This occurs by hitting the F8/F7 key, or doing step-over/into command, 8 times.

Once you have the CALL EAX selected with the execution cursor, left-click, then right-click on the following instruction (which will move the blue selection cursor down) SUB ESP,10. Select the New origin here choice, and it will move the execution cursor (representing EIP) down to the selected instruction.

Immunity Debugger "New Origin Here"

If you then click run, the program will display the console output, and terminate normally. You have successfully used the debugger to manually bypass execution of undesirable code. If you don’t see the terminal window, you might need to navigate to it. The terminal window title contains the full path of the program.

Bypassing using NOP patching

Being able to bypass the undesirable code using the above technique is nice, but it is also tedious. Thankfully, there’s a better way using a technique called run-time “patching”.

To begin, we will load the same ex2.exe EXE again, and display the view we had at the beginning of the previous section. Select the CALL EAX instruction again. Then, either right-click on the selected instruction to bring up the same menu as last time, and choose Assemble, or simply hit the SPACE bar, which is the short cut for the Assemble operation. The Assemble operation allows you to overwrite one line of code with your own instruction. In this case, we want to replace code that does a call, with code that will have no effect. In x86-32 and x86-64, this is known as the NOP instruction. This is represented with the single-byte value of 0x90.

Immunity Debugger Assemble

When you see the Assemble dialog pop up, it will display the instruction that you have selected. Replace the text in this textbox with the text NOP as illustrated above. Make sure that the Fill with NOP’s option is selected, and click Assemble.

Immunity Debugger Patched Call

This leaves a modification in memory, but not on disk, for this particular program. We can now run the program (F9) and see that it runs to completion, displaying the output to the console, without presenting the “OK” dialog.

Immunity Debugger Console Output

For a quicker method of doing this to multiple instruction, we can select multiple lines and then use the “Fill” operations. First, restart the program using the “rewind” button. Next, we will left-click on the MOV DWORD PTR SS:[ESP+C],0 line, and drag all the way down to the CALL EAX instruction. You should see 6 lines highlighted, which comprise the 4 argument moves and the indirect call operation (performed by moving the function pointer into EAX) and then CALL EAX:

Immunity Debugger Selected 6 Instructions

Right-click on the selection to bring up the context menu. Navigate to Binary and choose the Fill with NOPs option. This will immediately fill all 6 of the selected instructions with NOP bytes 0x90, replacing any other data there. You should now see a long line of NOP instructions - about 38 of them to be precise, and your program no longer fits on a single screen (unless your font size is really small).

Similar to above, you can now run the program and it will not prompt you to click “OK” in the dialog, but it will still print output to the console. What we’ve just done here is not only to replace the function CALL EAX, but also to replace all of the code responsible for passing its arguments. The single-line patch still left behind the argument preparation behavior, but then simply discarded the arguments after moving them to the stack. This second approach is arguably cleaner, as it results in less movement of data during execution, by replacing the function call as well as the code who’s sole function is preparation for that call.

Patch Window

When you restart the program, it restores the orginal bytes for all of the patches you’ve made. However, Immunity Debugger does not forget about them. You can click the “p” button in the toolbar, and it will bring up the “Patches” window. This window will allow you to review and manage all of the patches you’ve put in place, as well as its current status (Active vs. Removed).

Immunity debugger Patches

Using the right-click context menu after selecting a patch in the window, you can apply the patch (shorcut is also SPACE BAR). If you want Immunity to forget the patch, simply choose Delete record, or press the Del key (its shortcut).

Saving Changes

Immunity Debugger does have the capability to save your changes to disk. Conveniently, this can be done easily to another file, enabling you to manage changes across multiple output files. The effect here is that you end up with an EXE that runs with the modifications just like the examples we did inside Immunity Debugger, but you no longer need Immunity Debugger to get the desired behavior. Among the best uses for this are cases where the malware blocks with a user prompt, detects when it is running inside a VM, or contains an encrypted section that needs to be decrypted to execute properly.

When the patches are applied, you can right click anywhere on the disassembly listing to bring up the context menu again. From there, you can choose the Copy to executable sub-menu, which will display Selection and, if you have the patch(es) applied, All modifications. Choose the All modifications option and another prompt will display confirming you want to Copy all. Once clicked, a new window will show up, displaying disassembly, and having the title of the current EXE.

Immunity debugger Copy to File Example

This window gives one more chance to review before the data is saved to a file. Right click and choose Save file, and you will be prompted with the familiar Windows File Save dialog. In this dialog you can choose to overwrite the existing file (not recommended), or you can choose to write the data to a new file. Create a new EXE of your own choosing (I chose ex2_2.exe for my example).

Once complete, you should be able to open a cmd.exe window, cd to the directory where you saved the file, and run this patched version of the EXE, as many times as you want, without having the dialog pop up. Congratulations, you’ve successfully patched a bypass of undesirable code into the EXE.

Running ex2_2.exe a bunch

home

tags: malware - immunity debugger - disassembly - patching - editing - lecture