A quick guide on simple x86 binary patching at the assembly level.
@July 30, 2022
This is a free and open section of Practical Malware Analysis & Triage (PMAT), available on TCM Security Academy:
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.
The sample for this note is located in the PMAT-labs repository:
So download main.exe
and let's get going!
On FLAREVM, make a copy of main.exe
called main2.exe
(Windows, it seems, does not like when binaries are called x_patched.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:
Let’s examine the source code of this program so we have a strong reference point for when we dive into the ASM insanity.
(Click to expand) 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:
Recall that Nim has a few wrapper functions around the true main()
function of a program, so we will need to drill down a few levels. Double click on _NimMain()
:
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:
And 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 this function to go to the real heart of the program:
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:
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 is the splitting method. Let’s start at this split and work our way upwards.
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 a 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. So smol.
Source: 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. 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:
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
False Case
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.
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
Add freetshirts.local
to your /etc/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:
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:
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”:
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!
That’s all, folks! But for homework, think about (and research) other common binary patching techniques!
-Husky
— — — — — — > Back to Notes
🌐 Where You Can Find Me
🐦 Twitter | 📡 Main Blog | 👽 GitHub | 📺 YouTube
📒Recent Notes
8/30/22 Content Creators, I Will Teach You Cyber Jiu-Jitsu
8/12/22 The Responsible Red Teamer’s Manifesto
7/30/22 On Patching Binaries
7/16/22 MS-Interloper: On the Subject of Malicious MSIs
4/22/22 Failing All The Way To Token Manipulation, Part 1
4/16/22 COM Hijacking Creative Cloud