RCE Endeavors 😅

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.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment


Powered by WordPress