RCE Endeavors 😅

December 1, 2021

Reverse Engineering REST APIs: Conclusion (12/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 9:16 AM

Table of Contents:

Reverse engineering the REST APIs that power the Age of Empires IV multiplayer lobby system is complete at this point.

Throughout the series, we took different approaches at achieving this goal. Initially, we started off using third-party tools that allowed us to set up a reverse proxy and route the game’s traffic through it. From this, we were able to see the decrypted request and response content. We were fortunate that the game did not use certificate pinning, otherwise this technique would have been rendered useless. The series then took a turn towards debugging the game and getting some basic information from the game’s strings dump. From this, it was found that the game used an OpenSSL feature that allowed the generated keys to be written out to a file specified by the SSLKEYLOGFILE environment variable. We were able to import the key information from this file and decrypt captured request and response data in Wireshark.

The series then got more technical as the game was reverse engineered. By attaching a debugger and setting breakpoints on the Winsock send and recv functions, we were able to walk the call stack backwards until we had a point in the code where we had access to the plaintext data. The functions responsible for encrypting and decrypting the data were reverse engineered and their prototypes were extracted. Using these prototypes, we were able to create and set our hook functions. From within these hook functions, we had direct access to the plaintext request and response data. In the example source code, we logged the data out to console, or a file, but we can do whatever we want with it.

Hopefully this series has been helpful in describing what goes in to reverse engineering a processes network communication. In the best case scenario, it can be a trivial task that can be accomplished with the use of third-party tools. In the more complex scenario, it is a very technical process that involves a deep dive into the code and a strong understanding of reverse engineering at the assembly level, dynamic analysis via a debugger, and an understanding of what to look for. I hope that readers of this series have learned something and walk away with a better understanding of what it takes to reverse engineer software.

Reverse Engineering REST APIs: Ingress – Monitoring (11/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 9:13 AM

Table of Contents:

This post will cover the home stretch of the series: hooking the response decrypt function and showing the complete request-response flow. The technique to do this is the same as what was done to output the request data, so the code snippets here will be more brief. As before, we will start with a signature scan of the process memory for the response decrypt function.

void* FindDecryptPacketAddress(void* baseAddress)
{
    std::array<unsigned char, 39> signature = {
        0x48, 0x89, 0x5C, 0x24, 0x08,                        /* mov qword ptr ss:[rsp+8],rbx            */
        0x48, 0x89, 0x6C, 0x24, 0x10,                        /* mov qword ptr ss:[rsp+10],rbp           */
        0x48, 0x89, 0x74, 0x24, 0x18,                        /* mov qword ptr ss:[rsp+18],rsi           */
        0x57,                                                /* push rdi                                */
        0x48, 0x81, 0xEC, 0x20, 0x01, 0x00, 0x00,            /* sub rsp,120                             */
        0x48, 0x63, 0xC2,                                    /* movsxd rax,edx                          */
        0x49, 0x8B, 0xD9,                                    /* mov rbx,r9                              */
        0x49, 0x8B, 0xF8,                                    /* mov rdi,r8                              */
        0x48, 0x8B, 0xF1,                                    /* mov rsi,rcx                             */
        0x48, 0x8D, 0x2C, 0x40                               /* lea rbp,qword ptr ds:[rax+rax*2]        */
    };

    return PerformSignatureScan(baseAddress, signature);
}

Here we take the bytes that make up the instructions in the response decrypt function that was found in the previous post. We then scan the process memory for these instructions and return the address at which they were found. After finding this address, we place a hook on the function

__declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, LPVOID reserved)
{
    static HookEngine hookEngine{};
    static HMODULE baseAddress{ GetModuleHandle(NULL) };
    static void* targetSendAddress{ FindSendPacketAddress(baseAddress) };
    static void* targetDecryptAddress{ FindDecryptPacketAddress(baseAddress) };

    if (dwReason == DLL_PROCESS_ATTACH) {
        // Some code omitted here ...
        (void)hookEngine.Hook(targetSendAddress, GameSendPacketHook);
        (void)hookEngine.Hook(targetDecryptAddress, GameDecryptPacketHook);
    }

    if (dwReason == DLL_PROCESS_DETACH) {
        (void)hookEngine.Unhook(targetSendAddress, GameSendPacketHook);
        (void)hookEngine.Unhook(targetDecryptAddress, GameDecryptPacketHook);
    }

    return TRUE;
}

We can define our hooks to simply output the request and response data

int WINAPI GameDecryptPacketHook(void* unknown, int alwaysZero, char* decryptBuffer,
	size_t decryptBufferMaxSize, char* errorFlag)
{
	auto original{ (GameDecryptPacketFnc)HookEngine::GetOriginalAddressFromHook(GameDecryptPacketHook) };
	int result{};
	if (original != nullptr) {
		result = original(unknown, alwaysZero, decryptBuffer, decryptBufferMaxSize, errorFlag);
	}

	while (result == -1) {
		std::cerr << "Decrypt failed... retrying..." << std::endl;
		result = original(unknown, alwaysZero, decryptBuffer, decryptBufferMaxSize, errorFlag);
	}

	auto output{ MakePrintableAscii(decryptBuffer, result) };
	for (const auto& line : output) {
		std::cerr << std::format("Decrypted Response: {}", line)
			<< std::endl;
	}

	return result;
}

int WINAPI GameSendPacketHook(void* unknown, SOCKET socket, const char* buffer, int length, int* sentSize)
{
	auto output{ MakePrintableAscii(buffer, length) };
	auto [ipAddress, port] { GetPeerInfo(socket) };
	for (const auto& line : output) {
		std::cerr << std::format("[{}:{}] - Data: {}", ipAddress, port, line)
			<< std::endl;
	}

	auto original{ (GameSendPacketFnc)HookEngine::GetOriginalAddressFromHook(GameSendPacketHook) };
	int result{};
	if (original != nullptr) {
		result = original(unknown, socket, buffer, length, sentSize);
	}

	return result;
}

Since we are hooking a function that calls SSL_read, we can add some additional logic to avoid hitting the error-handling code that we did not reverse engineer in the previous post. Per the documentation on SSL_read, we can retry the call if the function returns -1, hence the addition of the while loop in GameDecryptPacketHook.

Lets see this in action: launch Age of Empires IV and inject the DLL containing these hooks into the process. After the console is created, perform some actions in-game to cause a request to be sent out.

From this we can see that the hooks are working correctly: each request, and its corresponding response, is shown. As we did before, we can choose to do whatever we want to the data: log it, modify it, prevent it from reaching the caller, and so on. At this point we have fully achieved what we set out to do; the request and response data, which we saw as being encrypted when inspecting the network traffic, is now clearly visible. We have successfully reverse engineered the REST APIs that make the multiplayer lobby system function!

Powered by WordPress