RCE Endeavors 😅

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.

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.

Older Posts »

Powered by WordPress