RCE Endeavors 😅

November 29, 2021

Reverse Engineering REST APIs: Ingress – Walking the Call Stack (9/12)

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

Table of Contents:

We now can see plaintext data for outgoing requests, but we have no idea what a response to any of these requests looks like. Fortunately, finding the response data follows a similar process to getting the request data. We can set a breakpoint on the recv function and work backwards in the call stack until we see the decrypted response. We can trigger a call to recv at-will by performing actions in-game to send out a request. When the server returns a response, our breakpoint get hit and we can begin walking the call stack.

To begin, launch Age of Empires IV and attach to the RelicCardinal.exe executable. Set a breakpoint on recv and refresh the multiplayer lobby list.

If you’ve done this, you will immediately notice that recv gets called constantly, likely preventing you from even being able to refresh the lobby list. If you were quick enough to actually hit the refresh button, it will still be hard to be certain that your call stack corresponds to the code responsible for reading a REST response. You can help ensure that you are looking at the proper code by setting a conditional breakpoint instead. Since REST responses tend to be quite large, and especially so for something like a multiplayer lobby refresh, we can set a conditional breakpoint on the value of the length argument to recv. This value will be stored in the R8 register per the x64 calling convention. Lets choose a high value like 0x1000 (4096) bytes.

Once this conditional breakpoint is in place, our debugger should no longer be breaking constantly. We can now go back in-game and refresh the multiplayer lobby list. After doing this, our breakpoint will get hit. We can put this cause-and-effect together and have high confidence that we are broken in the call stack responsible for reading a REST response. We can now take a look at the call stack window and see where we have to go from here.

As before, we set breakpoint on each of the functions in user code and resume execution of the process.

After resuming execution for a bit, we come to a place where we can clearly see the plaintext response. This happens in 00007FF7BFB0FB12, which is about halfway down the call stack. The function disassembly is shown below:

00007FF7BFB0FAC4 | 48:895C24 08                   | mov qword ptr ss:[rsp+8],rbx                      |
00007FF7BFB0FAC9 | 48:896C24 10                   | mov qword ptr ss:[rsp+10],rbp                     | [rsp+10]:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\
00007FF7BFB0FACE | 48:897424 18                   | mov qword ptr ss:[rsp+18],rsi                     |
00007FF7BFB0FAD3 | 57                             | push rdi                                          | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FAD4 | 48:81EC 20010000               | sub rsp,120                                       |
00007FF7BFB0FADB | 48:63C2                        | movsxd rax,edx                                    |
00007FF7BFB0FADE | 49:8BD9                        | mov rbx,r9                                        |
00007FF7BFB0FAE1 | 49:8BF8                        | mov rdi,r8                                        | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FAE4 | 48:8BF1                        | mov rsi,rcx                                       |
00007FF7BFB0FAE7 | 48:8D2C40                      | lea rbp,qword ptr ds:[rax+rax*2]                  |
00007FF7BFB0FAEB | E8 0093A501                    | call reliccardinal.7FF7C1568DF0                   |
00007FF7BFB0FAF0 | 48:8B8CEE E0020000             | mov rcx,qword ptr ds:[rsi+rbp*8+2E0]              | [rsi+rbp*8+2E0]:&"0²|Ä÷\x7F"
00007FF7BFB0FAF8 | B8 FFFFFF7F                    | mov eax,7FFFFFFF                                  |
00007FF7BFB0FAFD | 48:3BD8                        | cmp rbx,rax                                       |
00007FF7BFB0FB00 | 48:8BD7                        | mov rdx,rdi                                       | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FB03 | 0F47D8                         | cmova ebx,eax                                     |
00007FF7BFB0FB06 | 48:8B49 08                     | mov rcx,qword ptr ds:[rcx+8]                      |
00007FF7BFB0FB0A | 44:8BC3                        | mov r8d,ebx                                       |
00007FF7BFB0FB0D | E8 1E9DB801                    | call reliccardinal.7FF7C1699830                   |
00007FF7BFB0FB12 | 48:63F8                        | movsxd rdi,eax                                    | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FB15 | 85C0                           | test eax,eax                                      |
00007FF7BFB0FB17 | 7E 1C                          | jle reliccardinal.7FF7BFB0FB35                    |
00007FF7BFB0FB19 | 48:8BC7                        | mov rax,rdi                                       | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FB1C | 4C:8D9C24 20010000             | lea r11,qword ptr ss:[rsp+120]                    |
00007FF7BFB0FB24 | 49:8B5B 10                     | mov rbx,qword ptr ds:[r11+10]                     |
00007FF7BFB0FB28 | 49:8B6B 18                     | mov rbp,qword ptr ds:[r11+18]                     |
00007FF7BFB0FB2C | 49:8B73 20                     | mov rsi,qword ptr ds:[r11+20]                     | r11+20:"p’\v®3\x01"
00007FF7BFB0FB30 | 49:8BE3                        | mov rsp,r11                                       |
00007FF7BFB0FB33 | 5F                             | pop rdi                                           | rdi:"HTTP/1.1 200 OK\r\nDate: Wed, 24 Nov 2021 14:24:03 GMT\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nRequest-Context: appId=X\r\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\r\nRequest-Path: /game/advertisement/findAdvertisements\r\n\r\n67ed\r\n[0,[[10132504,0,\"{\\\"templateName\\\":\\\"GameSession\\\",\\\"name\\\":\\\"X\\\",\\\"scid\\\":\\\"0000
00007FF7BFB0FB34 | C3                             | ret                                               |

At our breakpoint, we see that the plaintext response is held in the address at RDI, which in turn came from the R8 register, which itself corresponds to the third argument passed into this function. We are at a point where we have direct access to the plaintext response buffer, and we can also verify that this function gets called only when decrypting a REST response. This seems like a good place to begin a deeper dive into. We know that the function we are looking at takes in the decrypt buffer as a parameter. We can see how this function is called, determine its prototype, and see if we should place a hook on it. If we’ve chosen wisely, we should have a hook that gives us access to the decrypted response buffer. The next post will cover the deep dive into this function and what information we can extract from it.

November 24, 2021

Reverse Engineering REST APIs: Egress – Monitoring (8/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:25 AM

Table of Contents:

We are at the home stretch: we’ve reverse engineered the egress flow to the point where we have plaintext data. We’ve identified a function that takes this plaintext data, along with its size, as arguments. We can now try to hook this function to inspect and modify the data. Let’s begin with the task at hand.

As before, we will be using Detours to set up the API hooks. We will be hooking the function by address, instead of by name, as in the earlier post. Because of this, we will need to add some extra logic since the game is not loaded at the same address each time. To handle this, we will perform a signature scan on the process memory: we will take some number of the bytes of the function’s assembly instructions and scan the entire process memory for them. This approach will find the function regardless of what address the game has been loaded, and also will still work if the game’s code has been modified — as long as this function was not directly modified or compiled differently.

Performing the signature scan is straightforward. Since we are already inside the address space of the process, we can just get the base address of the executable and then look through every committed memory region. If we’ve done everything correctly, we should find the function in memory. The implementation for this is below:

void* ScanMemoryForSignature(void* baseAddress, const std::span<unsigned char>& signature)
{
	MODULEINFO moduleInfo{};
	auto result{ GetModuleInformation(GetCurrentProcess(), (HMODULE)baseAddress, &moduleInfo,
		sizeof(MODULEINFO)) };
	if (!result) {
		std::cerr << std::format("Could not get process information. Error = {}.",
			GetLastError()) << std::endl;
		return nullptr;
	}

	for (auto base{ (DWORD_PTR)moduleInfo.lpBaseOfDll }; base < (DWORD_PTR)moduleInfo.lpBaseOfDll + moduleInfo.SizeOfImage; /**/) {
		MEMORY_BASIC_INFORMATION memInfo{};
		auto result{ VirtualQuery(baseAddress, &memInfo, sizeof(memInfo)) };
		if (!result) {
			std::cerr << std::format("Could not query memory region at {}. Error = {}", base, GetLastError())
				<< std::endl;
		}

		if (memInfo.State == MEM_COMMIT && memInfo.Protect != PAGE_NOACCESS) {
			for (auto i{ base }; i < base + memInfo.RegionSize - signature.size(); i++) {
				if (memcmp((void*)i, signature.data(), signature.size()) == 0) {
					return (void*)i;
				}
			}
		}

		base += memInfo.RegionSize;
	}

	return nullptr;
}

void* PerformSignatureScan(void* baseAddress, const std::span<unsigned char>& signature)
{
	void* result{ ScanMemoryForSignature(baseAddress, signature) };
	if (result == nullptr) {
		std::cerr << "Could not find function for signature." << std::endl;
	}
	else {
		std::cerr << std::format("Found function at address {}.", result);
	}

	return result;
}

void* FindSendPacketAddress(void* baseAddress)
{
    std::array<unsigned char, 45> signature = {
        0x48, 0x83, 0xEC, 0x38,                              /* sub rsp, 38h                            */
        0x83, 0x64, 0x24, 0x40, 0x00,                        /* and dword ptr[rsp + 40h], 0             */
        0x33, 0xC0,                                          /* xor eax, eax                            */
        0x48, 0x3B, 0x91, 0x58, 0x02, 0x00, 0x00,            /* cmp rdx, qword ptr[rcx + 258h]          */
        0x0F, 0x94, 0xC0,                                    /* sete al                                 */
        0x44, 0x8B, 0xD0,                                    /* mov r10d, eax                           */
        0x48, 0x8D, 0x44, 0x24, 0x40,                        /* lea rax, [rsp + 40h]                    */
        0x41, 0x8B, 0xD2,                                    /* mov edx, r10d                           */
        0x48, 0x89, 0x44, 0x24, 0x20,                        /* mov qword ptr[rsp + 20h], rax           */
        0x42, 0xFF, 0x94, 0xD1, 0x88, 0x02, 0x00, 0x00       /* call qword ptr[rcx + r10 * 8 + 288h]    */
    };

	return PerformSignatureScan(baseAddress, signature);
}

Once we have the address that we’re looking for, we need to install the hook on it. The HookEngine class of the earlier post can be expanded to support this functionality:

bool HookEngine::Hook(FncPtr originalAddress, HookFncPtr hookAddress)
{
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourAttach(&(PVOID&)originalAddress, (PVOID)hookAddress);
	auto result{ DetourTransactionCommit() };
	if (result != NO_ERROR) {
		std::cerr << std::format("Hook transaction failed with code {}.", result) << std::endl;
		return false;
	}

	m_hookToOriginal[hookAddress] = originalAddress;

	return true;
}

We can then put everything together and create our main function to get the function address and install the hook.

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

    if (dwReason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hModule);
        if (AllocConsole()) {
            (void)freopen("CONOUT$", "w", stdout);
            (void)freopen("CONOUT$", "w", stderr);
            SetConsoleTitle(L"Console");
            SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
            std::cerr << "DLL Loaded" << std::endl;
        }

        std::cerr << "Base address is 0x" << std::hex << baseAddress << std::endl;
        std::cerr << "Send data function is at 0x" << std::hex << targetAddress << std::endl;

        (void)hookEngine.Hook(targetAddress, GameSendPacketHook);
    }

    if (dwReason == DLL_PROCESS_DETACH) {
        (void)hookEngine.Unhook(targetAddress, GameSendPacketHook);
    }

    return TRUE;
}

We’ve redirected the function that we want to hook to our GameSendPacketHook function. In our hook, we can do some simple logging to start with:

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) };
	return original(unknown, socket, buffer, length, sentSize);
}

If we launch the game and inject our DLL, we should see our hook being hit. The output will show the plaintext data that is going to be sent to the server.

It looks like everything works perfectly. If we navigate around in the game and perform some actions, we will see the corresponding requests being made. The basic level hook is now in place to get access to the data. From here we can expand a bit and add some functionality. The first thing to add is a wrapper around the request data. If we monitor the output closely enough, we notice that POST and PUT requests come in multiple parts: first the HTTP header data is sent in one call, then the content is sent in the next one. It would be nice to create a HttpRequest wrapper class that can take in raw HTTP data and construct a full request out of it for scenarios like this. We can create the function to do this as follows:

std::unordered_map<SOCKET /*Socket*/, HttpRequest /*Pending request*/> m_incompleteRequests;

...

HttpRequest GetRequest(SOCKET socket, const char* buffer, int length)
{
	HttpRequest request{};
	if (m_incompleteRequests.find(socket) != m_incompleteRequests.end()) {
		request = m_incompleteRequests[socket];
		auto content{ std::vector<char>{buffer, buffer + length} };
		request.Content().insert(request.Content().end(), content.begin(), content.end());
	}
	else {
		request = HttpRequest::FromRaw(buffer, length);
	}

	auto expectedLength{ request.Header("Content-Length") };
	if ((request.HttpType() == "POST" || request.HttpType() == "PUT") && expectedLength != request.Headers().end()) {
		if (request.Content().size() < std::stol(expectedLength->second)) {
			m_incompleteRequests[socket] = request;
		}
		else {
			m_incompleteRequests.erase(socket);
		}
	}

	return request;
}

Here we take in the socket, request data, and length. We construct an HttpRequest object out of it and return it to the caller. This HttpRequest object will either be a partial object, or successfully built. A partial object will be returned in the event that we get data that indicates a POST or PUT request is being sent out. At this point we will have the request headers, but the body will be coming in the next call. On the next call, we will find the partial request that was built previously and fill in the content body. Once this is done, the request is considered to be fully built and ready to send out. If we get a GET request, then we just create and return a fully built HttpRequest from it, as GET requests are sent out in one call.

We need to modify our hook function to support this change. In our hook, we will first check to see if we have a fully built request. If so, we will call a handler for the request type, and then send the request to the server. If the request is not yet fully built then we return a successful response to the caller and wait for the content body to come in the next call.

int WINAPI GameSendPacketHook(void* unknown, SOCKET socket, const char* buffer, int length, int* sentSize)
{
	auto request{ GetRequest(socket, buffer, length) };
	if (request.IsBuilt()) {
		auto connectionInfo{ GetPeerInfo(socket) };
		if (messageHandlers.find(request.MessageType()) != messageHandlers.end()) {
			messageHandlers[request.MessageType()](request, connectionInfo);
		}
		else {
			std::cerr << std::format("No handler found for message {}", request.Path())
				<< std::endl;
			HandleGenericMessage(request, connectionInfo);
		}

		auto original{ (GameSendPacketFnc)HookEngine::GetOriginalAddressFromHook(GameSendPacketHook) };
		int result{};
		if (original != nullptr) {
			auto serializedHeader{ request.ToBytesHttpHeader() };
			result = original(unknown, socket, serializedHeader.data(), static_cast<int>(serializedHeader.size()), sentSize);
			auto serializedContent{ request.ToBytesHttpContent() };
			if (serializedContent.size() > 0) {
				result = original(unknown, socket, buffer, length, sentSize);
			}
		}

		return result;
	}
	else {
		*sentSize = length;
		return 0;
	}
}

Since there are a large number of APIs being called, we should add handlers for ones of interest. In these handlers we can decide to do some request-specific logic to inspect or modify the data. These handlers are just kept in a simple unordered map

static std::unordered_map<MessageType /*Message type*/, MessageHandler /*Handler*/> messageHandlers = {
                { MessageType::ChatMessage, HandleMatchChat },
                { MessageType::ReportMatch, HandleReportMatch }
                ...
};

In our custom handler, we can choose to inspect and modify the data. In the source code, I have added support for a large set of APIs that were observed. I have additionally created special “view” objects for each request type that map over the request data, allowing for easy modification.

void HandleMatchChat(HttpRequest& httpRequest, const std::pair<std::string, std::string>& connectionInfo)
{
	auto chatMessageView{ PostMatchChatMessageView { httpRequest } };
	// ... Do something with the chat message request here
	HandleGenericMessage(httpRequest, connectionInfo, false);
}

Lastly, we handle logging of the request data to console, and to file, in a generic handler.

void HandleGenericMessage(HttpRequest& httpRequest, const std::pair<std::string, std::string>& connectionInfo)
{
	auto messageBytes{ httpRequest.ToBytes() };
	if (WRITE_REQUEST_DATA_TO_CONSOLE) {
		auto output{ MakePrintableAscii(messageBytes.data(), messageBytes.size()) };
		auto [ipAddress, port] { connectionInfo };
		for (const auto& line : output) {
			std::cerr << std::format("[{}:{}] - Data: {}", ipAddress, port, line)
				<< std::endl;
		}
	}

	if (WRITE_REQUEST_DATA_TO_LOG) {
		auto requestBeginIndex{ httpRequest.Path().find_last_of('/') };
		auto requestEndIndex{ httpRequest.Path().find_last_of('?') };
		auto requestName{ std::string{httpRequest.Path().substr(requestBeginIndex + 1)} };
		if (requestEndIndex != std::string::npos) {
			requestName.erase(requestName.end() - 1);
		}

		auto filePath{ std::format("{}.log", requestName) };
		if (!std::filesystem::exists(filePath)) {
			std::ofstream logFile{ filePath, std::ofstream::binary };
			if (!logFile.is_open()) {
				std::cerr << std::format("Could not open {}", filePath)
					<< std::endl;
			}
			else {
				logFile.write(messageBytes.data(), messageBytes.size());
			}
		}
	}
}

Here we log all requests coming through to the console, and a request to file as well. If we run this improved hook, we should not observe any change in functionality. Everything in-game should appear to work as it did before, meaning our logic to build a HttpRequest object from the content and send it out only when ready has had no impact on the proper functioning of the game.

At this point, we have achieved what we set out to do: we have successfully found the location where we can intercept the plaintext data, and have written the logic to allow us to inspect, log, and modify it at-will. The egress flow is now finished, but we don’t know what a response to any of these requests looks like yet. That will be the subject of the remainder of this series.

November 23, 2021

Reverse Engineering REST APIs: Egress – Reversing the Request Encrypt Function (7/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 8:45 AM

Table of Contents:

We last left off in the middle of a function where we had the plaintext data; a pointer to the plaintext data, along with its size, were being passed as arguments to another function. The course of action that we decided to take is to hook this called function, allowing us direct access to the request data.

Before doing that, lets analyze the function a bit more. The relevant assembly is shown below:

00007FF721684DE7 | 48:8B8B 78100000               | mov rcx,qword ptr ds:[rbx+1078]                   |
00007FF721684DEE | 4C:8BC7                        | mov r8,rdi                                        |
00007FF721684DF1 | 49:8BD5                        | mov rdx,r13                                       |
00007FF721684DF4 | E8 2BB37701                    | call <JMP.&memmove>                               |
00007FF721684DF9 | 4C:8BAB 78100000               | mov r13,qword ptr ds:[rbx+1078]                   |
00007FF721684E00 | 4C:8BB424 88000000             | mov r14,qword ptr ss:[rsp+88]                     |
00007FF721684E08 | 48:8D8424 88000000             | lea rax,qword ptr ss:[rsp+88]                     |
00007FF721684E10 | 4C:8BCF                        | mov r9,rdi                                        |
00007FF721684E13 | 4D:8BC5                        | mov r8,r13                                        |
00007FF721684E16 | 48:894424 20                   | mov qword ptr ss:[rsp+20],rax                     |
00007FF721684E1B | 49:8BD7                        | mov rdx,r15                                       |
00007FF721684E1E | 49:8BCC                        | mov rcx,r12                                       |
00007FF721684E21 | E8 D2000000                    | call reliccardinal.7FF721684EF8                   |
00007FF721684E26 | 898424 A0000000                | mov dword ptr ss:[rsp+A0],eax                     |
00007FF721684E2D | 85C0                           | test eax,eax                                      |
00007FF721684E2F | 75 67                          | jne reliccardinal.7FF721684E98                    |

We want to set a breakpoint on 00007FF721684E21, which we have determined is the function that receives the plaintext data as a parameter. In the assembly, we can see the arguments getting set up for the call to reliccardinal.7FF68F314EF8. The call passes in five arguments: four in registers and one on the stack. We can set a breakpoint on the call at 00007FF68F314E21 and take a look at the registers

RCX gets loaded with R12, which we are not going to worry about for now. RDX gets loaded with 0xF4C, which if we look back at the top of the call stack, we would find out is equal to the outgoing socket handle.

R8 clearly holds the plaintext buffer, with R9 holding its length. Lastly, the stack parameter holds an out parameter to the size that was sent. This was found out by inspecting the result of the call in the debugger. We can now create a basic function definition for reliccardinal.7FF68F314EF8

using GameSendPacketFnc = int (WINAPI*)(void *unknown, SOCKET socket, const char *buffer, int size, int *sentSize);

We can quickly investigate the unknown pointer parameter a bit. It is passed in as the first argument, and if we see how the called function uses it, we see the following:

00007FF721684EF8 | 48:83EC 38                     | sub rsp,38                                        |
00007FF721684EFC | 836424 40 00                   | and dword ptr ss:[rsp+40],0                       |
00007FF721684F01 | 33C0                           | xor eax,eax                                       |
00007FF721684F03 | 48:3B91 58020000               | cmp rdx,qword ptr ds:[rcx+258]                    |
00007FF721684F0A | 0F94C0                         | sete al                                           |
00007FF721684F0D | 44:8BD0                        | mov r10d,eax                                      |
00007FF721684F10 | 48:8D4424 40                   | lea rax,qword ptr ss:[rsp+40]                     | [rsp+40]:"­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
00007FF721684F15 | 41:8BD2                        | mov edx,r10d                                      |
00007FF721684F18 | 48:894424 20                   | mov qword ptr ss:[rsp+20],rax                     | [rsp+20]:&"­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­�
00007FF721684F1D | 42:FF94D1 88020000             | call qword ptr ds:[rcx+r10*8+288]                 |
00007FF721684F25 | 48:8B4C24 60                   | mov rcx,qword ptr ss:[rsp+60]                     |
00007FF721684F2A | 48:8901                        | mov qword ptr ds:[rcx],rax                        | rax:&"­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
00007FF721684F2D | 48:85C0                        | test rax,rax                                      | rax:&"­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
00007FF721684F30 | 0F88 803B0302                  | js reliccardinal.7FF7236B8AB6                     |
00007FF721684F36 | 33C0                           | xor eax,eax                                       |
00007FF721684F38 | 48:83C4 38                     | add rsp,38                                        |
00007FF721684F3C | C3                             | ret                                               |

We see that offsets into this structure are referenced. At 00007FF721684F1D, there is an instruction that has an offset into a dispatch table. This appears to be used to call a handler that takes in plaintext data, among other parameters. If we step into this call and trace for a bit, we end up at the following:

...
00007FF721684F7C | 48:8BD7                        | mov rdx,rdi                                       | rdi:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=124&connect_id=X&dataChecksum=-1262884654&lastCallTime=102723&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF721684F7F | 0F47D8                         | cmova ebx,eax                                     |
00007FF721684F82 | 48:8B49 08                     | mov rcx,qword ptr ds:[rcx+8]                      |
00007FF721684F86 | 44:8BC3                        | mov r8d,ebx                                       |
00007FF721684F89 | E8 825E7201                    | call reliccardinal.7FF722DAAE10                   |
00007FF721684F8E | 48:63D0                        | movsxd rdx,eax                                    |
00007FF721684F91 | 85C0                           | test eax,eax                                      |
00007FF721684F93 | 0F8E 413B0302                  | jle reliccardinal.7FF7236B8ADA                    |

Here we make a call to reliccardinal.7FF722DAAE10. If we step into reliccardinal.7FF722DAAE10, we can begin to see string references to SSL

00007FF722DAAE22 | BA D0000000                    | mov edx,D0                                        |
00007FF722DAAE27 | C74424 20 AA070000             | mov dword ptr ss:[rsp+20],7AA                     |
00007FF722DAAE2F | 4C:8D0D AAD11203               | lea r9,qword ptr ds:[7FF725ED7FE0]                | 00007FF725ED7FE0:"..\\ssl\\ssl_lib.c"
00007FF722DAAE36 | 8D48 DC                        | lea ecx,qword ptr ds:[rax-24]                     |
00007FF722DAAE39 | 44:8D42 3F                     | lea r8d,qword ptr ds:[rdx+3F]                     | rdx+3F:"89&callNum=124&connect_id=X&dataChecksum=-1262884654&lastCallTime=102723&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF722DAAE3D | E8 9EEDECFF                    | call reliccardinal.7FF722C79BE0                   |

This is a very large and complex function that is doing tons of work. We can step out of it and ignore it for now. Lets take a look at how the caller handles the result of this call

00007FF721684F8E | 48:63D0                        | movsxd rdx,eax                                    |
00007FF721684F91 | 85C0                           | test eax,eax                                      |
00007FF721684F93 | 0F8E 413B0302                  | jle reliccardinal.7FF7236B8ADA                    |

If the result is greater than zero, the function sets some values and returns. If the result is less than or equal to zero, then we jump to reliccardinal.7FF7236B8AB6. If we look around the area of reliccardinal.7FF7236B8AB6 , we find some more useful strings:

00007FF7236B8AFE | FF15 1C5A9E05                  | call qword ptr ds:[<&WSAGetLastError>]            |
00007FF7236B8B04 | 48:8B0E                        | mov rcx,qword ptr ds:[rsi]                        |
00007FF7236B8B07 | 48:8D15 5240A502               | lea rdx,qword ptr ds:[7FF72610CB60]               | 00007FF72610CB60:"SSL_write() returned SYSCALL, errno = %d"
00007FF7236B8B0E | 44:8BC0                        | mov r8d,eax                                       |
00007FF7236B8B11 | E8 AA33F901                    | call reliccardinal.7FF72564BEC0                   |

We see a debug logging reference to SSL_write, which is responsible for writing bytes to a TLS connection. We can conclude that the function at reliccardinal.7FF722DAAE10 is SSL_write. This also tells us that [RCX+0x8] is a pointer to the SSL structure. We can begin to give the unknown pointer passed to reliccardinal.7FF68F314EF8 some structure. As interesting as it is, the work is likely not needed. We can keep the parameter as a void pointer and ignore it in our hook. We can simply continue with the knowledge that this unknown parameter is some defined structure that is responsible for interacting with the OpenSSL library and holding request information.

Having established the definition of the request encryption function, and looking a bit more at the internals, we can begin creating the hook to get at the request data. That is the topic of the next post.

November 22, 2021

Reverse Engineering REST APIs: Egress – Walking the Call Stack (6/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 11:53 AM

Table of Contents:

From the previous post, we had set hooks on the send and recv functions. We verified that network traffic was going out and coming in through these functions, though the data was encrypted due to the use of TLS. The plan now is to work backwards in two steps: egress (send) and ingress (recv). We will begin by looking at the egress path in these next few posts. The general plan is to work backwards from the send function and find out where the plaintext data is located. To do this, we will set a breakpoint on the send function as before, and walk the call stack backwards until we’ve hit a location where plaintext data is visible.

The issue we found with the send function is that it is constantly being hit since it is the gateway for all egress traffic from the process. Setting a breakpoint on it and investigating the call stack can have a lot of false leads. We are only interested in reverse engineering the REST APIs being called, and not other things like traffic from game data, telemetry, etc. A simple way to do this is to force the game to make a REST API call to the server. Luckily, there is a straightforward way in-game to do this: we can refresh the multiplayer lobby list. We can set a breakpoint on send, refresh the lobby list, and when our breakpoint gets hit, it should be from the REST API call.

Lets begin: launch Age of Empires IV and attach to the RelicCardinal.exe executable. Set a breakpoint on send and refresh the multiplayer lobby list.

After refreshing, your breakpoint should be hit. It is hard to tell what is happening, but we can investigate the registers to try and get a better idea.

The four registers highlighted in green are the values of the arguments passed to send. RCX holds the socket, RDX a pointer to the data, R8 the size of the data buffer, and R9 any additional flags. A buffer with size 0x4B0 (1200) bytes seems like it could be for a REST call. We can take a look at the call stack that generated this call

There are a lot of functions here, but overall the call stack looks pretty straightforward and everything is in user code. The simplest approach here is to set a breakpoint on each of these functions and walk the call stack backwards until we (hopefully) see plaintext data.

After all of the breakpoints have been set, we can resume execution and begin walking the call stack. As each breakpoint gets hit, we can look around and see if any references to plaintext data show up. As luck would have it, we begin to see some plaintext data a few frames in.

00007FF690A4E137 | EB 43                          | jmp reliccardinal.7FF690A4E17C                    |
00007FF690A4E139 | 44:8B7424 40                   | mov r14d,dword ptr ss:[rsp+40]                    |
00007FF690A4E13E | 48:8B45 88                     | mov rax,qword ptr ss:[rbp-78]                     |
00007FF690A4E142 | 48:8B55 98                     | mov rdx,qword ptr ss:[rbp-68]                     |
00007FF690A4E146 | 4C:8BC8                        | mov r9,rax                                        |
00007FF690A4E149 | 48:8B4D 90                     | mov rcx,qword ptr ss:[rbp-70]                     | [rbp-70]:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=43&connect_id=X&dataChecksum=-1262884654&lastCallTime=27010&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF690A4E14D | 48:895424 20                   | mov qword ptr ss:[rsp+20],rdx                     |
00007FF690A4E152 | 4C:8BC1                        | mov r8,rcx                                        |
00007FF690A4E155 | 41:8BD6                        | mov edx,r14d                                      |
00007FF690A4E158 | 48:8983 A8160000               | mov qword ptr ds:[rbx+16A8],rax                   |
00007FF690A4E15F | 48:898B C0160000               | mov qword ptr ds:[rbx+16C0],rcx                   | [rbx+16C0]:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=43&connect_id=X&dataChecksum=-1262884654&lastCallTime=27010&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8
00007FF690A4E166 | 44:89B3 B0160000               | mov dword ptr ds:[rbx+16B0],r14d                  |
00007FF690A4E16D | 48:8983 B8160000               | mov qword ptr ds:[rbx+16B8],rax                   |
00007FF690A4E174 | 48:8BCB                        | mov rcx,rbx                                       |
00007FF690A4E177 | E8 54140000                    | call reliccardinal.7FF690A4F5D0                   |
00007FF690A4E17C | 48:8B8D B00E0000               | mov rcx,qword ptr ss:[rbp+EB0]                    |

We can see a query string for a GET request. From its name, this findAdvertisements call matches up with what we would expect when refreshing the lobby list. We see the plaintext request data being moved into offset 0x16C0 of the structure at RBX. This is a great find; we’ve successfully located a spot where the plaintext data is present. There is one issue however, but it is more a matter of convenience than anything: this function is rather large and complicated. There are also references in this function to SSL, i.e.

00007FF690A4E0FA | 48:8D05 C7561203               | lea rax,qword ptr ds:[7FF693B737C8]               | 00007FF693B737C8:"..\\ssl\\record\\rec_layer_s3.c"

Meaning that if we hook this function, we could be hooking a generic SSL function which gets called by other places that we are not interested in. This would mean having to write additional logic in our hook to differentiate where the source data is coming from. It’s certainly doable, but it is an annoyance. Instead of stopping at the first place where we found plaintext data, we can continue looking further down the call stack and see if we reach a more simple function that only gets called when we make a REST API call.

We can continue stepping until we reach 00007FF68F314E26 in the call stack, which is near the end of a smaller function

00007FF68F314DE7 | 48:8B8B 78100000               | mov rcx,qword ptr ds:[rbx+1078]                   | [rbx+1078]:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=87&connect_id=X&dataChecksum=-1262884654&lastCallTime=89191&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF68F314DEE | 4C:8BC7                        | mov r8,rdi                                        |
00007FF68F314DF1 | 49:8BD5                        | mov rdx,r13                                       | r13:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=87&connect_id=zpwke4tjyrbpa81sy752jao6xxvekd&dataChecksum=-1262884654&lastCallTime=89191&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF68F314DF4 | E8 2BB37701                    | call <JMP.&memmove>                               |
00007FF68F314DF9 | 4C:8BAB 78100000               | mov r13,qword ptr ds:[rbx+1078]                   | r13:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=87&connect_id=X&dataChecksum=-1262884654&lastCallTime=89191&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF68F314E00 | 4C:8BB424 88000000             | mov r14,qword ptr ss:[rsp+88]                     |
00007FF68F314E08 | 48:8D8424 88000000             | lea rax,qword ptr ss:[rsp+88]                     |
00007FF68F314E10 | 4C:8BCF                        | mov r9,rdi                                        |
00007FF68F314E13 | 4D:8BC5                        | mov r8,r13                                        | r13:"GET /game/advertisement/findAdvertisements?appBinaryChecksum=7989&callNum=87&connect_id=X&dataChecksum=-1262884654&lastCallTime=89191&matchType_id=0&modDLLChecksum=0&modDLLFile=INVALID&modName=INVALID&modVersion=INVALID&profile_ids=%5B1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%2C1%5D&race_ids=%5B1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%
00007FF68F314E16 | 48:894424 20                   | mov qword ptr ss:[rsp+20],rax                     |
00007FF68F314E1B | 49:8BD7                        | mov rdx,r15                                       |
00007FF68F314E1E | 49:8BCC                        | mov rcx,r12                                       |
00007FF68F314E21 | E8 D2000000                    | call reliccardinal.7FF68F314EF8                   |
00007FF68F314E26 | 898424 A0000000                | mov dword ptr ss:[rsp+A0],eax                     |
00007FF68F314E2D | 85C0                           | test eax,eax                                      |
00007FF68F314E2F | 75 67                          | jne reliccardinal.7FF68F314E98                    |

Here we can see the arguments getting set up for the call to reliccardinal.7FF68F314EF8. The query data is passed directly to this function. If we set a breakpoint here, we will also observe that it only gets triggered when a REST API call to the server gets made. This looks like as good a place as any to begin hooking in to. Since we see that reliccardinal.7FF68F314EF8 takes in the plaintext request data, we can set a hook on it to intercept the data and allow us to inspect and modify it. That will be the topic of the next few posts.

November 19, 2021

Reverse Engineering REST APIs: Hooking Winsock (5/12)

Filed under: Game Hacking,Programming,Reverse Engineering — admin @ 12:28 PM

Table of Contents:

Having identified some promising leads in the previous post, it is time to start the investigation. To begin with, the Xbox Live API functions HCHttpCallRequestSetUrl, HCHttpCallRequestSetHeader, HCHttpCallRequestSetRequestBodyBytes, and similar all look very promising. If they are indeed used for the client-server communication that we are interested in, then it should be a simple matter of hooking into them and getting a hold of the HTTP content.

We can begin by attaching x64dbg to the Age of Empires IV process (RelicCardinal.exe), and finding what functions reference these strings. Lets start with HCHttpCallRequestSetUrl. In this case, there is only one location:


00007FF64CC88673 | 48:C7C3 FFFFFFFF               | mov rbx,FFFFFFFFFFFFFFFF                          |
00007FF64CC8867A | 4C:8BC3                        | mov r8,rbx                                        |
00007FF64CC8867D | 0F1F00                         | nop dword ptr ds:[rax],eax                        |
00007FF64CC88680 | 49:FFC0                        | inc r8                                            |
00007FF64CC88683 | 42:803C07 00                   | cmp byte ptr ds:[rdi+r8],0                        |
00007FF64CC88688 | 75 F6                          | jne reliccardinal.7FF64CC88680                    |
00007FF64CC8868A | 49:8D4E 08                     | lea rcx,qword ptr ds:[r14+8]                      |
00007FF64CC8868E | 48:8BD7                        | mov rdx,rdi                                       |
00007FF64CC88691 | E8 9AEBFFFF                    | call reliccardinal.7FF64CC87230                   |
00007FF64CC88696 | 90                             | nop                                               |
00007FF64CC88697 | 4C:8BC3                        | mov r8,rbx                                        |
00007FF64CC8869A | 66:0F1F4400 00                 | nop word ptr ds:[rax+rax],ax                      |
00007FF64CC886A0 | 49:FFC0                        | inc r8                                            |
00007FF64CC886A3 | 42:803C06 00                   | cmp byte ptr ds:[rsi+r8],0                        |
00007FF64CC886A8 | 75 F6                          | jne reliccardinal.7FF64CC886A0                    |
00007FF64CC886AA | 49:8D4E 28                     | lea rcx,qword ptr ds:[r14+28]                     |
00007FF64CC886AE | 48:8BD6                        | mov rdx,rsi                                       |
00007FF64CC886B1 | E8 7AEBFFFF                    | call reliccardinal.7FF64CC87230                   |
00007FF64CC886B6 | 90                             | nop                                               |
00007FF64CC886B7 | 41:80BE 20010000 00            | cmp byte ptr ds:[r14+120],0                       |
00007FF64CC886BF | 74 2A                          | je reliccardinal.7FF64CC886EB                     |
00007FF64CC886C1 | 48:897424 28                   | mov qword ptr ss:[rsp+28],rsi                     | [rsp+28]:"­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
00007FF64CC886C6 | 48:897C24 20                   | mov qword ptr ss:[rsp+20],rdi                     |
00007FF64CC886CB | 4D:8B8E 18010000               | mov r9,qword ptr ds:[r14+118]                     |
00007FF64CC886D2 | 4C:8D05 0FAD3103               | lea r8,qword ptr ds:[7FF64FFA33E8]                | 00007FF64FFA33E8:"HCHttpCallRequestSetUrl [ID %llu]: method=%s url=%s"
00007FF64CC886D9 | BA 04000000                    | mov edx,4                                         |
00007FF64CC886DE | 48:8D0D DB51EF04               | lea rcx,qword ptr ds:[7FF651B7D8C0]               | 00007FF651B7D8C0:&"HTTPCLIENT"
00007FF64CC886E5 | E8 46ADFFFF                    | call reliccardinal.7FF64CC83430                   |

Lets begin by taking a look at the structure of these instructions. We can see that there are two blocks [00007FF64CC88673 to 00007FF64CC88691] and [00007FF64CC88697 to 00007FF64CC886B1], which perform very similar functionality: they load some parameters, and call reliccardinal.7FF64CC87230. To analyze this assembly listing, we will start bottom-up and look at the block beginning at 00007FF64CC88697. Why do it this way? Because it is closer to the instructions that print out information related to HCHttpCallRequestSetUrl.

Here, at 00007FF64CC886B1, we call reliccardinal.7FF64CC87230, and then check if [R12+0x120] is equal to 0. If it is not, we skip calling next set of instructions; otherwise we continue executing and call reliccardinal.7FF64CC83430. The format string provides a lot of information about the arguments of this function. We can deduce that [R14+0x118] holds the request ID, [RSP+0x20] holds the method, and [RSP+0x28] holds the URL. Following the x64 calling convention, and taking hints from the disassembly, we can deduce that the function call looks similar to


reliccardinal_7FF64CC83430((void *)0x7FF651B7D8C0, 4, "HCHttpCallRequestSetUrl [ID %llu]: method=%s url=%s",
    id, method, url);

This is a good start. We can now work backwards to learn more. If we look at the call to reliccardinal.7FF64CC87230 that occurs at 00007FF64CC886B1, we see that it takes three arguments: [R14+0x28], and the RSI and R8 registers. The RSI register was loaded into [RSP+28], the last argument of reliccardinal.7FF64CC83430, which we have determined is the request URL. The R8 register appears to be calculated based off of RSI; it is incremented until it reaches a place where [RSI+R8] is 0. Since RSI is a string containing the URL, then R8 must be calculating where the null terminator is, effectively calculating the length of the URL. The value of [R14+0x28] is still undetermined, but if we look higher in the disassembly, towards the beginning of the function, we see these instructions:

00007FF64CC885D9 | 49:8BF0                        | mov rsi,r8                                        |
00007FF64CC885DC | 48:8BFA                        | mov rdi,rdx                                       |
00007FF64CC885DF | 4C:8BF1                        | mov r14,rcx                                       |                                    

The R14 register gets assigned here to RCX, the first argument of this function we are reversing, and doesn’t get re-assigned at any other point in the function. Different offsets into R14 get referenced and passed as arguments to other functions, meaning that it is some kind of structure. We can piece together what it is by looking at the HCHttpCallRequestSetUrl documentation and discover that it is the HCCallHandle opaque pointer. We also see that RDI and RSI get assigned here as well to the second and third arguments respectively. Since we have the function definition from the documentation we can conclude that RDI holds the method and RSI holds the URL. This is consistent with what we found from looking deeper into the function disassembly.

This gives us enough information to see what this block is doing: it is taking the input URL and copying it to an internal buffer inside the HCCallHandle structure. We can apply this logic for the other, nearly identical, block beginning at 00007FF64CC88673 and see that it is doing the same thing for the HTTP method parameter.

Putting all of this together allows us to begin reconstructing the original code of this function. Taking what we know, we can translate the above assembly listing into something that looks like this:

...
WriteToBuffer(callHandle->MethodBuffer, method, strlen(method));
WriteToBuffer(callHandle->UrlBuffer, url, strlen(url));
if (callHandle->ShowDebugOutput) {
    DebugWrite((void *)0x7FF651B7D8C0, 4, "HCHttpCallRequestSetUrl [ID %llu]: method=%s,
        url=%s", callHandle->Id, callHandle->MethodBuffer, callHandle->UrlBuffer);
}
...

With the debugger still attached, we can set breakpoints on where the two writes happen. If the breakpoints get hit, we should be able to get the HTTP method and URL for the outgoing request. So lets do that: set the breakpoints, tab back into the game, and begin performing some actions. If you do that, you will unfortunately find out that the breakpoints never get hit. The game does not appear to be using this function. It is the same story for the other HCHttpCallX functions; the request logic must be happening somewhere else.

Having spent some time on a fruitless search, we can take another approach. Instead of trying to investigate library internals, we can look one level higher at the Windows API. The functions that we are interested in are the send and recv functions provided by the Windows sockets API. Barring some very atypical implementation, all networking functionality will go through these two functions; send will be called to send data out to a socket, and recv will be used to read data from a socket. Lets verify this by setting a breakpoint on send.

Set the breakpoint and perform some activity that would require getting data from the server, i.e. refresh the available game lobbies. If the breakpoint has been successfully set, it should have been hit.

This is a great sign: we have some point at which to begin really reverse engineering and stepping backwards from. We know that the HTTP data must have been constructed at some point prior to calling send. We can take a quick detour and come up with a plan of action. To start with, we can hook the send and recv APIs and dump out the outgoing and incoming buffers. To do this, we will be using Microsoft’s Detours library. We will write this in a DLL that will be injected into the Age of Empires IV process. From within this DLL, we will hook send and recv and redirect them to our hook functions. From these hook functions we will dump out the buffers to a console. To start with, we can write the DllMain function to do this:

__declspec(dllexport) BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, LPVOID reserved)
{
	static HookEngine hookEngine{};

	if (dwReason == DLL_PROCESS_ATTACH) {
		DisableThreadLibraryCalls(hModule);
		if (AllocConsole()) {
			(void)freopen("CONOUT$", "w", stdout);
			(void)freopen("CONOUT$", "w", stderr);
			SetConsoleTitle(L"Console");
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
			std::cerr << "DLL Loaded" << std::endl;
		}

		(void)hookEngine.Hook("Ws2_32.dll", "send", sendHook);
		(void)hookEngine.Hook("Ws2_32.dll", "recv", recvHook);
	}

	if (dwReason == DLL_PROCESS_DETACH) {
		(void)hookEngine.Unhook("Ws2_32.dll", "send");
		(void)hookEngine.Unhook("Ws2_32.dll", "recv");
	}

	return TRUE;
}

Upon attaching to the process, we will allocate a console that we will write the data buffers to. We then hook the send and recv functions in Ws2_32.dll. The HookEngine class is just a wrapper around the Detours API. The Hook function is implemented as follows:

bool HookEngine::Hook(std::string_view moduleName, std::string_view functionName, HookFncPtr hookAddress)
{
	if (IsHooked(moduleName, functionName)) {
		std::cerr << std::format("{}:{} is already hooked.", moduleName, functionName)
			<< std::endl;
		return false;
	}

	auto functionAddress{ GetFunctionAddress(moduleName, functionName) };
	if (functionAddress == nullptr) {
		std::cerr << std::format("Hook installation failed. Address for {}:{} is nullptr.", moduleName, functionName)
			<< std::endl;
		return false;
	}

	auto result{ Hook(functionAddress, hookAddress) };
	if (!result) {
		return false;
	}

	m_hookedFunctions[std::string{ moduleName }].push_back(std::make_pair(std::string{ functionName }, functionAddress));

	std::cerr << std::format("Hook installed on {}:{} successfully.", moduleName, functionName)
		<< std::endl;

	return true;
}

bool HookEngine::Hook(FncPtr originalAddress, HookFncPtr hookAddress)
{
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourAttach(&(PVOID&)originalAddress, (PVOID)hookAddress);
	auto result{ DetourTransactionCommit() };
	if (result != NO_ERROR) {
		std::cerr << std::format("Hook transaction failed with code {}.", result) << std::endl;
		return false;
	}

	m_hookToOriginal[hookAddress] = originalAddress;

	return true;
}

For more details on the implementation, you can view the source listing in the Github link provided in the table of contents at the top of this post.

We define the sendHook and recvHook functions, which will be called when the target process calls send and recv. These functions do nothing except print out the incoming and outgoing buffers in a nice format.

template <typename ReturnType, typename FunctionType>
ReturnType PassthroughHook(void *caller, SOCKET socket, char *buffer, int length, int flags)
{
	if (!IsIgnoredPacket(buffer, length)) {
		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{ (FunctionType)HookEngine::GetOriginalAddressFromHook(caller) };
	ReturnType result{};
	if (original != nullptr) {
		result = original(socket, buffer, length, flags);
	}

	return result;
}

int WSAAPI sendHook(SOCKET socket, const char* buffer, int length, int flags)
{
	return PassthroughHook<int, sendFnc>(sendHook, socket, const_cast<char*>(buffer), length, flags);
}

int WSAAPI recvHook(SOCKET socket, char* buffer, int length, int flags)
{
	return PassthroughHook<int, recvFnc>(recvHook, socket, buffer, length, flags);
}

We also define an IsIgnoredPacket function that prevents printing certain data buffers. This is to prevent our screen from being flooded with calls that happen constantly, i.e. heartbeats and other calls that we are not interested in. Building this DLL and injecting it into the process shows that the hooks are being called. We can see data coming in and going out from the process. However, when looking at it, we can’t really see anything useful.

There is some occasional information that is visible in plaintext, but it is hard to get the context of the data and what it matches up to.

This is a good start, but we have cast too wide of a net. By hooking send and recv, we are monitoring everything coming in and going out over the network. If the goal is to get at the REST APIs, we need to go further down in the code and isolate the logic responsible for them. That will be the topic of the next posts.

« Newer PostsOlder Posts »

Powered by WordPress