Patch it out: Patching x86 Binaries

Sample for this section: PMAT-labs\labs\2-4.BinaryPatching\SimplePatchMe

Introduction

Our job as malware analysts is to discern what a malware specimen is doing. Sometimes, the malware is designed to prevent us from accomplishing that goal. We need to outsmart the malware when this is the case. Binary patching is one example of how we can outsmart the malware specimen!

Let’s examine a very simple binary patching technique. More importantly, I will walk through the methodology at the Assembly level so it’s clear why binary patching even works in the first place.

Setting Up

On FLAREVM, make a copy of main.exe called main2.exe:

PS C:\Users\husky> cp .\main.exe .\main_2.exe

On FLAREVM, open Cutter and open main2.exe up in Cutter. Make sure to click the “Load in write mode” box:

image

Let’s examine the source code of this program so we have a strong reference point for when we dive into the ASM insanity.

SimplePatchMe Source Code - main.nim


The program performs a GET request to http://freetshirts.local/key.crt and writes the body of the response to a variable. Then, it calculates the SHA256sum of the body of the response and compares it to a preset value. If the two values are the same, it executes the run_payload() procedure, which simply prints “[+] Boom!”. If the SHA256sum does not match the preset value, it says “[-] No dice, sorry :(”. This program is extremely simple to allow for better clarity when we get into the ASM and decompiled output in Cutter.

In Cutter, we open the main() function in the Decompiler panel:

image

Recall that a Nim compiled program will have a few wrapper functions around the true main() function, so we will need to drill down a few levels. Double click on _NimMain():

image

We can ignore _PreMain() and _initStackBottomWith() for now. These two functions are boilerplate for Nim compiled binaries. We can click on the _NimMainInner value to jump to the NimMainInner() function:

imageAnd finally, we get to the true main() of a Nim program: NimMainModule()

The main() module of a Nim program is nested this far down because the source code for a typical Nim binary looks like this:

proc do_a_thing(): void = 
	echo "[+] Thing is now done!"

when isMainModule:
    do_a_thing()

Double click on the NimMainModule() function to go to the real heart of the program:

image

What are we working with here? The symbols of this binary have been left in, so the function names are nice and easy to read. Two of them are interesting here: evaluate_http_body() and run_payload(). Graph view may help us understand what is going on:

image

The call to evaluate_http_body() splits this graph into two paths. One path runs the run_payload() function that we saw in the source code (”[+] Boom!”). The other path echoes the other string ("[-] No dice, sorry :("). 

The jne 0x43f1c0 instruction splits the program into two paths. Let’s start at this split and work our way upwards.

image

jne stands for (J)ump if (N)ot (E)qual to, which really means “Jump if the condition is not met.” What condition, exactly? The one on the previous instruction, which is test al,al.

test is used to perform the logical bitwise AND operation on two operands. In this case, we are AND’ing the contents of al against itself. al is the lower 8 bits of the eax register. Basically, when a function is executed, its return value will be stored in eax for comparison and evaluation. If it’s small enough, it can be stored in al because al is only 8 bits.

imageSource: https://www.cs.virginia.edu/

Now, the result of test al, al instruction sets the Zero Flag (ZF), Sign Flag (SF), and Parity Flag (PF) registers to certain values. For the purpose of this demonstration, we can ignore Sign Flag and Parity Flag and focus on Zero Flag.

The Zero Flag can be either “1” or “0” (imagine that) and is set based on the result of the previous test instruction. test will set the ZF to one value or the other based on the result of the bitwise AND.

If the Zero Flag is equal to 0, the JNE instruction will be taken. If the Zero flag is equal to 1, the JNE instruction will not be taken. When you see the program splitting to one path or the other because of a JNE instruction, it’s going one way or the other way depending on if the Zero Flag is set (equal to zero).

One instruction higher than the test al, al is xor eax,1. This is the true deciding point in the program, because the value of eax has been set by the evaluate_http_body() function:

image

image

We know from the source code that the evaluate_http_body() function returns a Boolean value. So let’s examine the result of XORing a 1 against a resulting TRUE and FALSE value:

True Case

image

False Case

image

So depending on if the returned value from evaluate_http_body() is TRUE or FALSE, our XOR operation returns either a 1 or a 0. If the result was TRUE, our XOR returns a 0. If FALSE, it returns a 1.

This is then evaluated by test al, al where the Zero Flag will be set to 0 or 1 depending on the result. Finally, the jne [location] instruction sends us to one side of the graph or the other.

Ok, so what does this actually do?

So let’s back up for a second. So far, we are:

  • Doing a thing (evaluate_http_body())
  • Writing the return value of the thing to a variable (TRUE or FALSE)
  • XOR this result against the value of 1 (xor eax,1)
  • TEST the resulting value of the eax register and set the Zero Flag based on the result of this TEST (test al, al).
  • Jump to one side of the code path if ZF == 0, and jump to the other side if ZF == 1 (jne [memory address]).

The Problem Here

Let’s assume for this example that this is a piece of malware that calls to http://freetshirts.local and grabs the body of the endpoint at key.crt. And let’s assume that this endpoint is now offline, or has been changed.

We know the payload triggers if the SHA256 sum of the contents of key.crt equals a pre-defined SHA256 in the binary. We can even see this in plain text in the binary itself. So what’s the issue?

The issue is that there is no possible way we could know the contents of that endpoint at this point. We have a SHA256 hash, but it is basically impossible to reverse the SHA256 sum back into its original contents. 

So if we ever want this binary to trigger and get to the run_payload() code path, we’re basically out of luck.

Or, are we?

The Patch

We’re going to patch this binary so it will run the payload regardless of the result of the evaluate_http_body() function.

This binary exists on our machine. We have full control over it. Who says we can’t write new instructions inside of it? Who says we can’t manipulate the bytes themselves to bend them to our will? We can and we will do just that.

The basic idea here is to insert or alter instructions into the binary so it will reach our intended code path, regardless of how the program is supposed to run. There are plenty of ways to do this, but let’s keep it very simple for this run.

Running and Patching the Exe

On FLAREVM, add freetshirts.local to your hosts file and have it point to 127.0.0.1.

The binary throws an exception if there is no webserver to talk to at all:

PS C:\Users\husky> .\main.exe
[-] Error: No connection could be made because the target machine actively refused it.

[-] No dice, sorry :(

So if we stand up a webserver, we can run the binary again and get a different result. Remember, we need a key.crt file, though we don’t know what the contents would have been:

image

C:\Users\husky>python -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...

PS C:\Users\husky> .\main.exe
[-] No dice, sorry :(

This is still not the result that we want, so we must patch!

Back in Cutter, go to the jne instruction and open it up in the Disassembler:

image

To patch this, we have tons of options. We can make sure that the value is different by the time it hits this XOR instruction. Or we could insert a JMP to jump over this code block completely. But why not keep it simple?

The opposite of jne is je, which is Jump if Equal To. This does the exact opposite of jne: if the Zero Flag is set to 1, the jump will be taken. So let’s patch this by changing the jne instruction to a je. What will that do?

Well, let’s see!

Right click on the jne instruction and select “Edit → Reverse Jump”:

image

Now, save and close out of Cutter. We should still have our two binaries and our Rizin database file:

PS C:\Users\husky> ls main*


    Directory: C:\Users\husky


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         7/30/2022   2:23 PM         634150 main.exe
-a----         7/30/2022   2:23 PM         634150 main2.exe
-a----         7/30/2022   2:29 PM        6277600 main2.exe.rzdb

Now we test our patched binary:

PS C:\Users\husky> .\main.exe
[-] No dice, sorry :(
PS C:\Users\husky> .\main2.exe
[!] Boom!

…and it woks!

Summary

In this section, we examined a simple binary patching technique. The malicious program was designed to only trigger if it met a certain condition, but we rewrote the bytes of the binary to coerce it to trigger anyway. This technique is simple but extremely powerful. Next, we will iterate on this technique to learn how to defeat more complex forms of anti-analysis.

Complete and Continue