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