Reading and Writing into Process's Memory

Get the basic understanding on the remote process memory read and write all by windows 32 API and create your own game hacks.

Reading and Writing into Process's Memory

Hello, world! Till now you might have seen programs are changing the bits and bytes in their own memory, by changing the state of variables either by user input or network requests or hardcoded in the software. But today, I will teach you one of the most interesting topics among the hackers - playing with the different process's memory.

Motivation

My curiosity on this topic started while playing the IGI game after starting a different application that was used to provide me unlimited health and open all the missions – The IGI game crack. Since then I always wondered how does a different application is working to change the state of another application so accurately that it knows where the variables are stored and etc.

Then I read more about reading and writing process memory and recently tried to create my own demonstration of that we will be discussing in the following sections. In this post, I will share that knowledge to get started. This knowledge is the building block of process injection that I will cover later with different techniques and creating your own game hacks.

Writing the Victim Process Code

In this section, I will guide you to create a demo victim process that will accept a string in the CLI argument and assign it to the buffer. Then it will show the address of the buffer and copied text and total length of the string.

For any of the attacker processes, it is required to open the process by its ID. So let's start off by printing the process id. Luckily there is a function from processthreadsapi.h GetCurrentProcessId() which will return the process id of the calling process.

DWORD GetCurrentProcessId();
Signature of the GetCurrentProcessId function from the processthreadsapi.h header file

Next, you can create a buffer using VirtualAlloc function and use the CopyMemory() function which is just a macro for the RtlCopyMemory() function from the wdm.h file.

LPSTR lpBuff = (LPSTR)VirtualAlloc(nullptr, txtLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
CopyMemory(lpBuff, argv[1], txtLen);
Allocate memory buffer and copy the CLI argument value to this buffer
Even RtlCopyMemory is a macro of memcpy function (read this). Therefore, there is no difference if you use CopyMemory function, the compiler will automatically resolve it to memcpy.

Print the value of the address and the contents for both of the buffer and argv[1] variables.

std::cout << "=====================================\n";
std::cout << "       Variables before Tamper       \n";
std::cout << "=====================================\n\n";
std::cout << "Argument address: 0x" << std::hex << (PVOID) argv[1] << "\t\t\tArgument Value: " << argv[1] << std::endl;
std::cout << "Buffer address:   0x" << std::hex << (PVOID) lpBuff << "\t\t\tBuffer Value: " << lpBuff << std::endl;
std::cout << "Length of Chars: " << std::dec << txtLen << std::endl;
Print original buffer details

Now it's time to wait for the process so that the attacker write process can perform tamper with the base address of the string. After this print the same details, if the value tampers, it would be different from the one that is previously printed

std::cout << "=====================================\n";
std::cout << "       Variables after Tamper        \n";
std::cout << "=====================================\n\n";
std::cout << "Argument address: 0x" << std::hex << (PVOID) argv[1] << "\t\t\tArgument Value: " << argv[1] << std::endl;
std::cout << "Buffer address: 0x" << std::hex << (PVOID) lpBuff << "\t\t\tBuffer Value: " << lpBuff << std::endl;
Printing tampered values

Writing the Attacker Process

Finally we come to the section where actual magic will happen! Before moving forward let's think of a question and try to understand it. Why does modern OS like windows allow different process to peek into the address space of another process even though it is like each process address space is isolated from another and so on? Well the answer is simple, when you do the development it is required to debug your process to find the issues and resolve them. Now it is required to peek into the memory of function frames to show developers the exact picture of what is going on and why. This approach then was used by offensive security researchers to perform read/write in the remote process.

The application will be accepting at least 3 required parameter and one optional parameter – Victim's process id (PID), Hexadecimal base address of the string variable and Text to overwrite. The optional parameter will basically used to read the amount of data, this is default to 1. A single page is about 4kb (4096 bytes).

To convert the hex address from string to number type, you can use StrToInt64ExA() function for this. It provided an option STIF_SUPPORT_HEX to allow parsing hexadecimal string into LONGLONG type.

if (!StrToInt64ExA(argv[2], STIF_SUPPORT_HEX, &llBaseAddr)) {
	PrintError("StrToInt64ExA()", TRUE);
}
Convert the base address of to the numerical form

Open the process handle with access to PROCESS_VM_READ, PROCESS_VM_WRITE and PROCESS_VM_OPERATION to allow the attacker process to read the remote process memory and write the contents into it. This can be done by passing the combination of access rights in the OpenProcess() function. This function will return NULL for either the invalid process id or protected processes.

HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwPID);
if (hProcess == NULL) {
	PrintError("OpenProcess()", TRUE);
}
Get the handle of process from id with appropriate access rights

Reading from Memory

The function ReadProcessMemory() will mainly take the process's handle, base address from where to start reading the contents and a buffer to return the contents. When the operation is successful, it will return TRUE, otherwise FALSE.

// read the buffer from the remote process and store in the local buffer
LPVOID lpReadBuffer = (LPVOID) VirtualAlloc(nullptr, PAGE_SIZE * dwPages, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpReadBuffer == nullptr) {
    PrintError("VirtualAlloc()", TRUE);
}

if (!ReadProcessMemory(hProcess, (LPCVOID) llBaseAddr, lpReadBuffer, PAGE_SIZE * dwPages, nullptr)) {
    PrintError("ReadProcessMemory()", TRUE);
} else {
    std::cout << "Read Buffer: " << (LPSTR) lpReadBuffer << std::endl;
}
Reading the process memory and storing it in the local buffer lpReadBuffer

This LONGLONG to LPCVOID is used to convert the numerical format of address into pointer form because the addresses are always stored in the pointer variables. The address of llBaseAddr (&llBaseAddr) will give the address of the variable instead and it will produce different output.

Writing into Memory

This can be done by WriteProcessMemory() function and the syntax is similar to the RPM method we have just looked. Instead of page size, that we have used earlier, in this case we have to use the length of the buffer we want to write, strlen(argv[3]) + 1. This plus 1 is used to tell the function to include the NULL character in the string.

if (!WriteProcessMemory(hProcess, (LPVOID)llBaseAddr, argv[3], strlen(argv[3]) + 1, nullptr)) {
	PrintError("WriteProcessMemory()", TRUE);
}
Writing the buffer from the 3rd CLI argument to the remote process buffer.

Once this is successful, you can press any key on the victim process and see the difference. You can check the working of this code in the following video.

Code in Action 🚀

Once you have implemented the code as guided above, the exploit will work as shown below