RCE Endeavors 😅

December 1, 2021

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!

November 30, 2021

Reverse Engineering REST APIs: Ingress – Reversing the Response Decrypt Function (10/12)

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

Table of Contents:

We last left off in the middle of a function where we had a pointer to where decrypted response data would be written in to. The plan was to take a closer look at this function and see what it does; if we can hook into it and manipulate the decrypted buffer then our mission of reverse engineering the request-response flow is complete. Fortunately, this function is pretty simple — the assembly listing has been reproduced below:

00007FF7BFB0FAC4 | 48:895C24 08                   | mov qword ptr ss:[rsp+8],rbx                      |
00007FF7BFB0FAC9 | 48:896C24 10                   | mov qword ptr ss:[rsp+10],rbp                     |
00007FF7BFB0FACE | 48:897424 18                   | mov qword ptr ss:[rsp+18],rsi                     |
00007FF7BFB0FAD3 | 57                             | push rdi                                          |
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                                        |
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]              |
00007FF7BFB0FAF8 | B8 FFFFFF7F                    | mov eax,7FFFFFFF                                  |
00007FF7BFB0FAFD | 48:3BD8                        | cmp rbx,rax                                       |
00007FF7BFB0FB00 | 48:8BD7                        | mov rdx,rdi                                       |
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                                    |
00007FF7BFB0FB15 | 85C0                           | test eax,eax                                      |
00007FF7BFB0FB17 | 7E 1C                          | jle reliccardinal.7FF7BFB0FB35                    |
00007FF7BFB0FB19 | 48:8BC7                        | mov rax,rdi                                       |
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]                     |
00007FF7BFB0FB30 | 49:8BE3                        | mov rsp,r11                                       |
00007FF7BFB0FB33 | 5F                             | pop rdi                                           |
00007FF7BFB0FB34 | C3                             | ret                                               |

We can begin by setting a breakpoint at the top of this function and stepping. After doing this, we can see that the buffer gets decrypted after the call to reliccardinal.7FF7C1699830 is executed, as shown below:

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

If the result of this call is less than or equal to zero, then we jump to reliccardinal.7FF7BFB0FB35. If we look around the instructions at this address, we notice some debug strings mentioning OpenSSL:

00007FF65657FBF4 | FF15 26E9AA05                  | call qword ptr ds:[<&WSAGetLastError>]            |
00007FF65657FBFA | 48:8B0E                        | mov rcx,qword ptr ds:[rsi]                        |
00007FF65657FBFD | 48:8D15 9CD0B102               | lea rdx,qword ptr ds:[7FF65909CCA0]               | 00007FF65909CCA0:"OpenSSL SSL_read: %s, errno %d"
00007FF65657FC04 | 44:8BC8                        | mov r9d,eax                                       |
00007FF65657FC07 | 4C:8BC3                        | mov r8,rbx                                        |
00007FF65657FC0A | E8 B1C20502                    | call reliccardinal.7FF6585DBEC0                   |

This logic is very similar to what we saw when we were encrypting the request data, though now we are on the read path instead of the write path. Whereas before we had a debug string referencing SSL_write, now we have one referencing SSL_read. Based on stepping through the code, the documentation of SSL_read, and the debug strings, we can make a reasonable assumption that reliccardinal.7FF7C1699830 is SSL_read. As in the request decryption code, [RCX+0x8] is passed as the first parameter, followed by a pointer to the decrypt buffer, and the buffer size. From the previous reverse engineering session regarding the request encryption flow, we found that [RCX+0x8] holds the SSL opaque pointer.

This function that we are looking at seems like a reasonable place to hook. The logic here is very similar to the encryption function that was hooked earlier in this series, and we can verify that this function only gets called when we have made a REST request. The last thing that we need to do is to define the function prototype. We can derive most of the prototype based on what we’ve seen already. To start with, we see four arguments to this function being moved to different registers at the top


00007FF6541AFADB | 48:63C2                        | movsxd rax,edx                                    |
00007FF6541AFADE | 49:8BD9                        | mov rbx,r9                                        |
00007FF6541AFAE1 | 49:8BF8                        | mov rdi,r8                                        |
00007FF6541AFAE4 | 48:8BF1                        | mov rsi,rcx                                       |

We can see these registers being passed a little lower to the call to SSL_read:

00007FF6541AFB00 | 48:8BD7                        | mov rdx,rdi                                       |
00007FF6541AFB03 | 0F47D8                         | cmova ebx,eax                                     |
00007FF6541AFB06 | 48:8B49 08                     | mov rcx,qword ptr ds:[rcx+8]                      |
00007FF6541AFB0A | 44:8BC3                        | mov r8d,ebx                                       |
00007FF6541AFB0D | E8 1E9DB801                    | call reliccardinal.7FF655D39830                   |

From this, we can deduce that RCX holds the structure that wraps the SSL and HTTP request info logic, R8 holds the pointer to the decrypt buffer, and R9 holds the size of the decrypt buffer. This might be unclear from the partial assembly listings, but if you start with the call to SSL_read and work backwards from where the arguments come from, it becomes more clearly visible. This gets us to identifying three arguments, but there are more.

At the top, we move the second argument into RAX. This argument is then used in two places:

00007FF6541AFAE7 | 48:8D2C40                      | lea rbp,qword ptr ds:[rax+rax*2]                  |
00007FF6541AFAEB | E8 0093A501                    | call reliccardinal.7FF655C08DF0                   |
00007FF6541AFAF0 | 48:8B8CEE E0020000             | mov rcx,qword ptr ds:[rsi+rbp*8+2E0]              |
...
00007FF6541AFB35 | 48:8B8CEE E0020000             | mov rcx,qword ptr ds:[rsi+rbp*8+2E0]              |
...

It is used as an index into the structure passed in the first parameter. From debugging, we notice that its value is always zero, and we can just leave this argument alone since we aren’t interested in reverse engineering the SSL/HTTP request wrapper internals.

The last argument is an out parameter. If we look through the path where SSL_read returns less than or equal to 0, we can see it being set:

00007FF6541AFB64 | 48:8B8424 50010000             | mov rax,qword ptr ss:[rsp+150]                    |
00007FF6541AFB6C | C700 51000000                  | mov dword ptr ds:[rax],51                         | 51:'Q'
...
00007FF65657FC0F | 48:8B8424 50010000             | mov rax,qword ptr ss:[rsp+150]                    |
00007FF65657FC17 | C700 38000000                  | mov dword ptr ds:[rax],38                         | 38:'8'

Given that this is on the error path, we will also choose to ignore it. Since we are placing a hook, we can just forward the result of this back to the caller without having to worry about performing any modifications on it. At this point we have all of the arguments to the function. We can define the prototype as

using GameDecryptPacketFnc = int (WINAPI*)(void* unknown, int alwaysZero, char* decryptBuffer, size_t decryptBufferSize, char* errorFlag);

We are now done reverse engineering the decryption routine. We’ve found that it is a suitable function to hook in order to get at the decrypted response buffer. From within our hook, we can choose to inspect or modify this buffer before returning it to the caller. The next post will quickly cover how to do this, and wrap up the series.

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.

« Newer PostsOlder Posts »

Powered by WordPress