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 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.