RCE Endeavors 😅

November 29, 2021

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

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

Table of Contents:

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

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

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

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

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

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

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

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

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress