Target
Our target in this lesson will be Wesnoth 1.14.9.
Overview
In previous lessons, we used Cheat Engine to search for memory addresses and change their values. Cheat Engine is a type of program known as a memory scanner. Memory scanners allow you to search for and edit memory inside a process.
Our goal in this lesson is to create a memory scanner that will operate on DWORD values for the game Wesnoth.
Understand
Memory scanners have three main operations:
- Search all memory for a certain value.
- Filter previously identified addresses against a new value.
- Set a memory address to a certain value.
In the previous lesson, we created a pattern scanner that would search the
main Wesnoth module for a series of bytes. We can use the same technique
to search memory for a value. However, in this case, we will scan all
memory from 0x00000000
to 0x7FFFFFFF
. This range
of addresses represents all the virtual address space that a 32-bit Windows executable has access to. When scanning, we will
save any address that is set to a certain value.
To filter these addresses, we will perform the same scan operation
described above with one major difference: instead of scanning from
0x00000000
to 0x7FFFFFFF
, we will only scan
saved addresses identified from the previous scan step. Any addresses that
still match a provided value will again be saved. In this way, we can
continue to filter down the list of valid addresses.
Finally, to write to an address, we can use the same WriteProcessMemory technique identified in the External Memory Hack lesson.
Program Structure
Before we write our program, we need to determine how we will handle the multiple operations and passing data from one operation to another.
Since we have three distinct operations for our memory scanner to perform, we need to determine how to handle these cases. One approach is to create a separate program for each operation and then transfer data between the three programs. However, this approach would require us to duplicate logic between multiple programs, such as the logic to open a process handle.
For this lesson, we will use command-line arguments to designate which operation we want to perform. For example, if we want to search for the value 50, we will call our program like:
MemoryScanner.exe search 50
Since we need to call our scanner multiple times, we need a way to pass results from one operation to the next. The easiest way to accomplish this is to use a file. For example, if we search for a value, the file will be filled with all addresses that match this value. When we filter, addresses will be read from this file, and then new addresses are placed in the file if they still match.
Process Handle
To read and write memory from Wesnoth, we need a process handle. We will use the same approach we used in the previous lesson to accomplish this:
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
int main(int argc, char** argv) {
HANDLE process_snapshot = 0;
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
process_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(process_snapshot, &pe32);
do {
if (wcscmp(pe32.szExeFile, L"wesnoth.exe") == 0) {
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
// handle operations
CloseHandle(process);
break;
}
} while (Process32Next(process_snapshot, &pe32));
return 0;
}
We will pass this process handle to all of our operations.
Operations
Next, we can add in our operations. To access command-line arguments passed to our program, we can use the argv argument. argv[0] will always hold our program’s name on the command-line (MemoryScanner.exe), with argv[1] representing the first argument.
All arguments are passed in as strings. We want our program to search for DWORD values. To convert from a string to a value that we can use to search for DWORD’s, we will use strtol, or (str)ing (to) (l)ong:
// handle operations
char* p;
long value = strtol(argv[2], &p, 10);
if(strcmp(argv[1], "search") == 0) {
search(process, value);
}
else if(strcmp(argv[1], "filter") == 0) {
filter(process, value);
}
else if (strcmp(argv[1], "write") == 0) {
write(process, value);
}
With the base in place, we can now write each of these functions.
Search
We will start with our search function:
void search(const HANDLE process, const int passed_val) {
Like we discussed in the previous section, we will store the results of the search in a text file. Like we did back in the Creating an External Client lesson, we will use fopen_s to create a text file we can write to:
FILE* temp_file = NULL;
fopen_s(&temp_file, "res.txt", "w");
As we know, memory does not have a particular structure. For example, the
memory from 0x12345678
to 0x1234567C
could hold
the values 0x44 0x45 0x41 0x45
. If read as a DWORD, this
memory would hold the value 1145389381. However, if each byte is read as a
char, this memory would hold the value DEAD. In memory scanners like Cheat
Engine, you can select the type of data to scan for. In this lesson, we will
scan all memory as if it was a DWORD. This will allow us to search for
values that are numbers, such as gold.
Our search operation will scan all memory from 0x00000000
to
0x7FFFFFFF
and compare each 4 bytes to the value passed in
the second argument. Previously, we used
ReadProcessMemory to read a single 4-byte
DWORD. However, ReadProcessMemory allows
us to read any size of memory into any type of allocated buffer.
While Wesnoth can use all memory from 0x00000000
to
0x7FFFFFFF
, it first needs to request access via several
API’s, like VirtualAlloc. If Wesnoth has not requested
access to a certain piece of memory, it will not be able to read or write
data to it. We are using Wesnoth’s handle to read memory, so we will need
to account for this behavior.
If we try to read all memory from 0x00000000
to
0x7FFFFFFF
with one ReadProcessMemory call,
the call will fail. This is because ReadProcessMemory’s
behavior is to immediately fail and place a NULL value in
our buffer if we encounter a section of memory we do not have access to.
As a result, we will need to split our read requests up into blocks. That
way, if we attempt to scan a block that Wesnoth has not allocated, only
that block’s read will fail.
We can choose any value for our block size, but there is a trade-off
between speed and accuracy. The larger each block is, the faster the scan
process will take, but more areas of memory may not be read successfully
due to part of the block being inaccessible. For this lesson, we will choose
a block size of 2056, or 0x808
:
#define size 0x00000808
We will then allocate a buffer that can hold a block-size worth of data:
unsigned char* buffer = (unsigned char*)calloc(1, size);
Next, we will loop through each block of memory from
0x00000000
to 0x7FFFFFFF
and read that block
into the buffer:
DWORD bytes_read = 0;
for (DWORD i = 0x00000000; i < 0x7FFFFFFF; i += size) {
ReadProcessMemory(process, (void*)i, buffer, size, &bytes_read);
Finally, we will cast each 4 bytes of our buffer as a DWORD and determine if its value equals the argument passed. If so, we will write its location to our results file:
for (int j = 0; j < size - 4; j += 4) {
DWORD val = 0;
memcpy(&val, &buffer[j], 4);
if (val == passed_val) {
fprintf(temp_file, "%x\n", i + j);
}
}
If a read fails, our buffer will contain nothing but 0’s and this final step will find nothing. After we finish with our ReadProcessMemory loop, we will close the file and free the buffer’s memory:
fclose(temp_file);
free(buffer);
Our search function is now finished. If you build this code and search for a gold value inside Wesnoth, you will see that res.txt now contains a several addresses:
MemoryScanner.exe search 75
Filtering
The next operation we will focus on is filtering. The filtering operation will take a list of addresses produced by the search operation and check to see if those addresses equal a new value. If the address does equal the value, it will be saved. If it does not, it will be deleted:
void filter(const HANDLE process, const int passed_val) {
We will conduct the filtering operation in two parts:
- Read each memory address from res.txt and if it matches the new value, save it to res_fil.txt.
- Copy res_fil.txt to res.txt and delete res_fil.txt.
The end result will be a new res.txt file that contains only the filtered addresses. This model will allow us to filter multiple times. First, we will open res.txt for reading (r) and res_fil.txt for writing (w):
FILE* temp_file = NULL;
FILE* temp_file_filter = NULL;
fopen_s(&temp_file, "res.txt", "r");
fopen_s(&temp_file_filter, "res_fil.txt", "w");
We will then read each address from res.txt line by line and read Wesnoth’s memory at that address. If the value matches our argument, we will write the address to res_fil.txt:
DWORD address = 0;
while (fscanf_s(temp_file, "%x\n", &address) != EOF) {
DWORD val = 0;
DWORD bytes_read = 0;
ReadProcessMemory(process, (void*)address, &val, 4, &bytes_read);
if (val == passed_val) {
fprintf(temp_file_filter, "%x\n", address);
}
}
With all the filtered addresses in res_fil.txt, we will then close both res.txt and res_fil.txt. Then, we will open up these files in the opposite order from above, with res.txt for writing and res_fil.txt for reading:
fclose(temp_file);
fclose(temp_file_filter);
fopen_s(&temp_file, "res.txt", "w");
fopen_s(&temp_file_filter, "res_fil.txt", "r");
Next, we will loop through each address in res_fil.txt and copy it to res.txt:
while (fscanf_s(temp_file_filter, "%x\n", &address) != EOF) {
fprintf(temp_file, "%x\n", address);
}
With res.txt now containing our addresses, we will close each file and delete res_fil.txt:
fclose(temp_file);
fclose(temp_file_filter);
remove("res_fil.txt");
We can now search for and filter addresses. If you search for your gold in Wesnoth, buy a unit, and then filter your gold value, you should be left with a single value. If you open up Cheat Engine and repeat these steps, you can verify that the address you identified and the address from Cheat Engine match. This shows that our scanner is properly finding memory addresses.
MemoryScanner.exe filter 54
Writing
The final main operation of a memory scanner is writing values to identified memory addresses:
void write(const HANDLE process, const int passed_val) {
This operation is identical to the approach we used in the External Memory Hack lesson. For each address in res.txt, we will use WriteProcessMemory to write the provided argument value to the address:
FILE* temp_file = NULL;
fopen_s(&temp_file, "res.txt", "r");
DWORD address = 0;
while (fscanf_s(temp_file, "%x\n", &address) != EOF) {
DWORD bytes_written = 0;
WriteProcessMemory(process, (void*)address, &passed_val, 4, &bytes_written);
}
fclose(temp_file);
With this code, we can now write whatever value we want to the previously searched for and filtered addresses:
MemoryScanner.exe write 555
The full code for this lesson is available on github for comparison.