Table of Contents:
- Introduction (1/12)
- The Easy Way (2/12)
- Basics (3/12)
- Debugging (4/12)
- Hooking Winsock (5/12)
- Egress – Walking the Call Stack (6/12)
- Egress – Reversing the Request Encrypt Function (7/12)
- Egress – Monitoring (8/12)
- Ingress – Walking the Call Stack (9/12)
- Ingress – Reversing the Response Decrypt Function (10/12)
- Ingress – Monitoring (11/12)
- Conclusion (12/12)
- Source code
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!