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: Image showing the unpatched program

After patching: Image showing the patched program

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.