RE-Invent: Byte Patching functions for unfair advantages
Disclaimer⌗
The RE-Invent blog posts are designed to break down complex information into simpler terms, specifically for beginners learning the basics of Reverse Engineering. These posts are strictly for educational and experimental use and are not intended to target any software. Please ensure to use this knowledge responsibly and in accordance with ethical and legal standards.
Introduction⌗
This article expects basic knowledge about sections in applications (
.text
,.data
), offsets, assembly and debugging knowledge.
Many of us have experienced being stuck in a video game, unable to progress due to resource limitations or not having enough coins to purchase valuable items. Similarly, if you’ve ever wanted to use a program beyond its free trial period without paying, you might have wondered about the complexity of manipulating the program to achieve your goal. Large programs can be very intricate, making it challenging for beginners to locate the function responsible for time or coin management. So, where do you begin, and once you succeed, how can you create a program that automatically applies these changes? Let’s explore this.
Goals⌗
Lets split up our final goal into smaller sections.
- Finding the corresponding function and assembly code that checks the coin balance or the time via debugging or reading the programs’ code
- Creating a pattern so we always find the bytes we have to replace
- Writing a program to write the bytes
Finding the function⌗
To begin, let’s first figure out where we need to make changes to a function to achieve the desired result. Consider a program that verifies whether you have sufficient funds to purchase the master sword.
//target price stored somewhere in memory
int masterSwordPrice = 5000;
//function that checks if the balance is enough to buy the sword
bool hasEnoughMoney(int balance)
{
if (balance >= masterSwordPrice)
return true;
return false;
}
void buyMasterSword()
{
...
//get our current balance
int currentBalance = ...
if(hasEnoughMoney(currentBalance))
{
puts("Buying the sword...");
...
}
else
{
puts("Not enough money...");
return;
}
...
}
If a user lacks the required funds, they can’t purchase the master sword and will receive the message “Not enough money…” instead. The critical part here is the hasEnoughMoney
function, which checks if the user has sufficient funds to buy the sword at its specified price. Now, let’s examine the assembly code output:
.text:001000 ; bool __fastcall hasEnoughMoney(int balance)
.text:001000 ?hasEnoughMoney@@YA_NH@Z proc near ; Function prologue
.text:001000
.text:001000 arg_0 = dword ptr 8
.text:001000
.text:001000 89 4C 24 08 mov [rsp+arg_0], ecx ; Move the balance argument into local stack storage
.text:001004 8B 05 2A 40 00 00 mov eax, cs:?masterSwordPrice ; Load the masterSwordPrice into the EAX register
.text:00100A 39 44 24 08 cmp [rsp+arg_0], eax ; Compare balance with the masterSwordPrice
.text:00100E 7C 04 jl short loc_140001014 ; Jump if balance is less than masterSwordPrice
.text:001010 B0 01 mov al, 1 ; Set AL (the lower 8 bits of RAX) to 1 (indicating enough money)
.text:001012 EB 02 jmp short locret_140001016 ; Jump to Function epilogue
.text:001014 ; ---------------------------------------------------------------------------
.text:001014
.text:001014 loc_140001014: ; Jump target if balance is insufficient
.text:001014 32 C0 xor al, al ; Set AL to 0 (indicating insufficient funds)
.text:001016
.text:001016 locret_140001016: ; Function epilogue
.text:001016 C3 retn ; Return from the function
.text:001016 ?hasEnoughMoney@@YA_NH@Z endp
As observed, the hasEnoughMoney
function essentially compares the input balance with the masterSwordPrice and makes conditional jumps based on whether the balance is sufficient or not. Our main focus is on the al
register because it holds the function’s return value. If al
is 0, the function returns false. In this case, we examine loc_140001014
, which is the point where the value 0 is placed into the al
register.
The opcode bytes for xor al, al
are 32 C0
, a common and efficient way to zero a register. However, it has the same length as the mov al, 1
, B0 01
. So if we would like to always return true, we replace the xor al, al
with mov al, 1
.
The example was clear and had code that was easy to understand. However, imagine a scenario where you can’t locate the part of the code where the comparison takes place. This can happen if the function is extensive, intricate, and also purchases the master sword by subtracting its price from your current balance:
.text:001000 ; char __fastcall checkBalanceAndBuySword(int *balance)
.text:001000 ?checkBalanceAndBuySword@@YA_NAEAH@Z proc near ; Function prologue
.text:001000
.text:001000 var_18 = dword ptr -18h
.text:001000 arg_0 = qword ptr 8
.text:001000
.text:001000 mov [rsp+arg_0], rcx ; Move balance argument into local stack storage
.text:001005 sub rsp, 18h ; Allocate space on the stack
.text:001009 mov eax, cs:?masterSwordPrice ; Load masterSwordPrice into EAX
.text:00100F mov [rsp+18h+var_18], eax ; Store masterSwordPrice on the stack
.text:001012 mov rax, [rsp+18h+arg_0] ; Load the value pointed to by balance into RAX
.text:001017 mov ecx, [rsp+18h+var_18] ; Load masterSwordPrice into ECX
.text:00101A cmp [rax], ecx ; Compare the value pointed to by balance with masterSwordPrice
.text:00101C jge short loc_140001022 ; Jump if greater or equal (indicating sufficient funds)
.text:00101E xor al, al ; Set AL to 0 (indicating insufficient funds)
.text:001020 jmp short loc_140001047 ; Function epilogue
.text:001022 ; ---------------------------------------------------------------------------
.text:001022
.text:001022 loc_140001022: ; Jump target if balance is sufficient
.text:001036 mov eax, [rax] ; Load the value pointed to by balance into EAX
.text:001038 sub eax, ecx ; Subtract masterSwordPrice from EAX
.text:00103A mov rcx, [rsp+18h+arg_0] ; Reload balance into RCX
.text:00103F mov [rcx], eax ; Update the value pointed to by balance with the new balance
.text:001041 mov al, 1 ; Set AL to 1 (indicating successful purchase)
.text:001043 jmp short loc_140001047 ; Function epilogue
.text:001045 ; ---------------------------------------------------------------------------
.text:001045
.text:001045 loc_140001045: ; Jump target if balance is insufficient
.text:001045 xor al, al ; Set AL to 0 (indicating insufficient funds)
.text:001047
.text:001047 loc_140001047: ; Function epilogue
.text:001047 add rsp, 18h ; Deallocate stack space
.text:00104B retn ; Return from the function
.text:00104B ?checkBalanceAndBuySword@@YA_NAEAH@Z endp
The assembly code snippet has been trimmed to remove unnecessary parts.
This assembly code snippet is much larger and more complex than the previous one, but the key part to focus on is loc_140001022
. Here, the balance is subtracted and updated. Typically, you need to track movements from global variables, like mov eax, cs:?masterSwordPrice
, which eventually ends up in the ecx
register, or observe what happens with the rcx
register, the pointer to our balance. The balance pointer is moved into the stack at [rsp+arg_0]
, later loaded into rax
, and in loc_140001022
, the balance value is read and stored in eax
using mov eax, [rax].
It’s evident that sub eax, ecx
calculates the new balance and writes it back into the balance pointer.
For our purpose of always getting a sword without spending money, we just need to replace jge loc_140001022
with jmp loc_140001022
and sub eax, ecx
with add eax, ecx
. All the opcode bytes are the same length, so replacing a total of 4 bytes is sufficient for unlimited swords and funds.
Creating a pattern⌗
Now, we create a suitable pattern for our pattern scanner in our program, which will replace specific bytes. The first pattern we need is for jge loc_140001022
, so that the money addition code runs, regardless of the balance. The opcode bytes 7D 04
are quite short, so we combine them with the opcode bytes 32 C0 EB 25
from the two assembly instructions below. This results in the final pattern 7D 04 32 C0 EB 25
. Technically, we could replace the second and last byte of the pattern with wildcards because the conditional jump offsets might change, but it’s highly unlikely that a function solely checking the balance and buying a sword will change in the future.
As for the sub eax, ecx
with the opcode bytes 2B C1
, we add the opcode bytes 48 8B 4C 24 20
from the assembly instruction below, resulting in the final pattern 2B C1 48 8B 4C 24 20
. These two patterns are unique enough for our program.
Now, we just need to find the corresponding patched opcode bytes: a jmp loc_140001022
has the opcode bytes EB 04
, and an add eax, ecx
has the opcode bytes 01 C8
. That’s all there is to it.
Writing the program⌗
I won’t provide a detailed explanation of how the Windows API functions work; instead, I’ll demonstrate how to use them with an example.
To develop a program that can alter a target process’s memory, we need to utilize Windows APIs to initially acquire a handle to the target process with the required permissions to read and write memory.
DWORD targetProcessID = 1234;
// Open the target process with full access.
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetProcessID);
Next, we need to obtain the address of the patterns so that we can correctly insert our opcode bytes into the target process at the right location.
uint64_t jmpAddress = getAddressByPattern("7D 04 32 C0 EB 25");
uint64_t subAddress = getAddressByPattern("2B C1 48 8B 4C 24 20");
// Now, how do we write?
The only remaining problem is that the .text
section of the target process is not initially writable. Attempting to write to the .text
section will trigger an exception. To address this issue, we must modify the protection of the page by utilizing the VirtualProtectEx
function, perform the necessary write operation, and afterward, restore the page protection to its original state. We can employ the WriteProcessMemory
function to write our data in the specified location within the target process.
// Change memory protection to PAGE_EXECUTE_READWRITE.
// We use the for the dwSize subAddress - jmpAddress + 2, because we change the protection from jmpAddress to subAddress + 2 because of the two bytes we have replace at the subAddress.
DWORD oldProtect;
if (VirtualProtectEx(hProcess, (LPVOID)jmpAddress, subAddress - jmpAddress + 2, PAGE_EXECUTE_READWRITE, &oldProtect)) {
//our jmp opcode
const char* jmpOpcode = "\xEB\x04";
// Write data to the target process.
SIZE_T bytesWritten;
WriteProcessMemory(hProcess, (LPVOID)jmpAddress, jmpOpcode, 2, &bytesWritten);
//our sub opcode
const char* subOpcode = "\x01\xC8";
// Write data to the target process.
WriteProcessMemory(hProcess, (LPVOID)subAddress, subOpcode, 2, &bytesWritten);
// Restore the original memory protection.
VirtualProtectEx(hProcess, (LPVOID)jmpAddress, subAddress - jmpAddress + 2, oldProtect, &oldProtect);
}
Before patching:
After patching:
Conclusion⌗
Understanding assembly code and logic is an essential skill when it comes to patching programs. Once you grasp the fundamentals, the process remains consistent. This knowledge is incredibly potent and applicable to a wide range of software, enabling you to circumvent paywalls and enhance your experience in various applications, particularly in offline games. However, a word of caution: in the realm of byte-patching, altering the wrong bytes can lead to the complete failure of the program. Therefore, while it grants great power, it must be wielded with care and responsibility.