Reversing O2Jam Network
Recently, I have successfully released a major version of Mozart.Encore project with code name: CrossTime, which is to support one of the major O2Jam versions, particularly the most radical one: O2Jam X2.
In turn, I shared this in a couple of places on the internet, and people are showing interest in the reversing process. So today, on this occasion, I’d like to share my reversing journal about O2Jam Networking. Please bear in mind that my reversing skills consist solely of guesswork plus trial and error; what do you expect? I’m pretty new to reversing myself!
In this post, we will focus on the v3.10 client and I’d like to stick with static analysis as much as possible. Thanks to community efforts, some of the client inner working have already been discovered; such as how to start the game, and the fact that the game uses TCP instead of UDP. There are many other small and subtle details that I won’t mention here one by one. As trivial as they may sound, they’re still helpful, so we don’t have to figure them out by ourselves from scratch, which means I can focus on the network part. Nice!
Before reading further, I recommend you read Welcome to CXO2 if you are unfamiliar with O2Jam.
Starting the game
Seriously, before we start anything, we need to understand how to launch the game because it’s not like I can double-click the executable to run it. It’s definitely something you won’t see in many old games.
When the official service was still around, you had to log into the web portal via Internet Explorer, then click Start game, which would launch the game via ActiveX. The e-Games distribution included a game launcher which essentially baked an Internet Explorer window into the program; behind the scenes, it involve ActiveX to launch the game too.
Anyway, the ActiveX script would also pass an encoded base64 string along with a couple of pairs of game server IP and port. The decoded base64 string is essentially a random GUID encoded in UTF16BE and used as an opaque auth token. Keep in mind the decoded token is opaque, meaning the game will decode the base64 string, but it will not try to process this GUID, and thus any encoded base64 from a random UTF16BE string will work just fine to launch the game.
The launch argument will look like this:
OTwo.exe ADAAOQBEADUAQgBBADcAQwAtADUARQAyAEEALQA0ADYANQBBAC0AOQA4AEUARAAtADgAMgBDADYAOAA1AEEARgAxAEYAMQA4
www.download.e-games.web.id:1234 O2Jam 2 127.0.0.1 15010 127.0.0.1 15010
Let’s break it down:
OTwo.exeis the game client- The second parameter is the authentication code
- The third parameter is the FTP server
- The fourth parameter is presumably the service name, always equal to
O2Jam. In some newer versions, it is a web server URL instead. - The rest of the parameters are the number of servers, followed by the game server IP and port pairs.
If we decode the base64 string above, we will get the following string:
09D5BA7C-5E2A-465A-98ED-82C685AF1F18
The game will send this string to authenticate the player session and expect a response from the server. With all that said, let’s finally get started.
Intercepting the network request
The first thing that comes to my mind is to do this backward: let’s see what the actual data being sent looks like. There are a couple of reasons, but the idea is that locating where the network message is being sent to the server sounds generally faaaaar easier.
Another thing is that OTwo.exe is massive. I could easily see myself getting mixed up between graphics rendering, asset processing, input handling and so on. I could think of some alternatives, such as looking up the socket function from the import table. But in this case, I would prefer to see the packet data to get the full picture.
With all that said, let’s observe the network messages sent by the client. Fortunately, this should be easy even without a network monitoring program such as Wireshark. We can simply set up a TCP Server to listen on port 15010, or any port of our choice as we can adjust it from the launch argument above, which allows us to route the game’s network traffic into our dummy server app.
To make it even simpler, I asked ChatGPT to write the TCP server for me in C#. Very convenient indeed!
using System.Net;
using System.Net.Sockets;
// Set the listener IP address and port
var ipAddress = IPAddress.Parse("127.0.0.1");
var port = 15010;
// Create TcpListener
var tcpListener = new TcpListener(ipAddress, port);
try
{
// Start listening for incoming connection requests
tcpListener.Start();
Console.WriteLine($"Listening on {ipAddress}:{port}");
// Accept the pending client connection
var tcpClient = tcpListener.AcceptTcpClient();
Console.WriteLine("Connection accepted.");
Console.WriteLine();
// Get the network stream from the connected client
var networkStream = tcpClient.GetStream();
// Read data from the client as a byte array
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = networkStream.Read(buffer, 0, buffer.Length)) > 0)
{
Console.WriteLine("Received data as byte array:");
Print(buffer);
Console.WriteLine(); // Add a newline for better formatting
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}
finally
{
// Stop listening and close the TcpListener and TcpClient
tcpListener.Stop();
}
private static void Print(Memory<byte> memory)
{
var span = memory.Span;
const int bytesPerLine = 16;
string hexes = string.Empty;
string decoded = string.Empty;
for (int i = 0; i < span.Length; i += bytesPerLine)
{
var lineSpan = span.Slice(i, Math.Min(bytesPerLine, span.Length - i));
// Hex
var hex = string.Join(" ", lineSpan.ToArray().Select(b => b.ToString("X2")));
// ASCII
var ascii = new string(lineSpan.ToArray().Select(b => b >= 32 && b <= 126 ? (char)b : '.').ToArray());
hexes += $"{hex,-48}" + Environment.NewLine;
decoded += ascii + Environment.NewLine;
}
Console.WriteLine(hexes);
Console.WriteLine(decoded);
}
Alright! Let’s fire up the server and see what our client sends to us!
Listening on 127.0.0.1:15010
Connection accepted.
Received data as byte array:
29 00 E8 03 30 39 44 35 42 41 37 43 2D 35 45 32
41 2D 34 36 35 41 2D 39 38 45 44 2D 38 32 43 36
38 35 41 46 31 46 31 38 00
)...09D5BA7C-5E2
A-465A-98ED-82C6
85AF1F18.
Thankfully, as you can see above, the data seems to be simple. I made several attempts using different sets of input strings to see the variation of the first 4 bytes in the packet. For example:
Received data as byte array:
09 00 E8 03 54 45 53 54 00
)...TEST.
After a couple of seconds, the game shows an in-game error saying it encountered a network error or something along those lines (the wording is awkward, I know).
From here, I made the assumption that the payload looks like this:
int16 packet_sizeint16 opcodestring token(null-terminated)
In the above case, the opcode is 1000 (0x03E8).
Static analysis
This is a great start. We haven’t fired up a disassembler or debugger, yet we’ve been able to make a solid guess about the request payload. But now, we have to figure out what response the client expects in order to proceed, so this is where the main dish starts.
First, if my assumption above is correct, I believe the first 4 bytes are common across requests and responses. So we need to discover the opcodes used by both client and server. Let’s start with 1000, 1001, and maybe 1002; time to fire up our disassembler.
Looking up the immediate value of 1000 (0x03E8) gives me dozens of results and I don’t think it’s practical to verify every single one of them. In our case, we are more interested in other opcodes anyway, so let’s try our luck with 1001 (0x03E9).
1001 (0x03E9).This looks promising as it only returns 3 results. By the looks of it, sub_429610 handles 3 opcodes: 1001, 1003, and 1005. But it doesn’t call any function at all for 1001; it just sets one random global variable to 0. On the other hand, sub_456290 has this huuuge switch/case table that handles dozens of opcodes!
Following the 1001 branch led me to sub_456D20, which interestingly appears in the immediate value search above. After carefully looking at it, it contains auth token validation. This is it! This is the function where it processes the auth response from the server!
0045720E loc_45720E: ; CODE XREF: sub_456D20+70↑j
0045720E ; DATA XREF: jpt_456D90↓o
0045720E push ebx ; jumptable 00456D90 case -1
0045720F push offset aEitherLoginNam ; "Either login name or password is incorr"...
00457214 jmp loc_4572B4
00457219 ; ---------------------------------------------------------------------------
00457219
00457219 loc_457219: ; CODE XREF: sub_456D20+70↑j
00457219 ; DATA XREF: jpt_456D90↓o
00457219 push ebx ; jumptable 00456D90 case -2
0045721A push offset aUserIsNowBeing ; "User is now being connected to the Game"...
0045721F jmp loc_4572B4
00457224 ; ---------------------------------------------------------------------------
00457224
00457224 loc_457224: ; CODE XREF: sub_456D20+70↑j
00457224 ; DATA XREF: jpt_456D90↓o
00457224 push ebx ; jumptable 00456D90 case -5
00457225 push offset aYouHaveBeenBan ; "You have been banned! \nPlease enquire "...
0045722A jmp loc_4572B4
0045722F ; ---------------------------------------------------------------------------
0045722F
0045722F loc_45722F: ; CODE XREF: sub_456D20+70↑j
0045722F ; DATA XREF: jpt_456D90↓o
0045722F push ebx ; jumptable 00456D90 case -101
00457230 push offset aPleaseInquireO ; "(Please inquire of Administrator.) DB e"...
00457235 jmp short loc_4572B4
00457237 ; ---------------------------------------------------------------------------
00457237
00457237 loc_457237: ; CODE XREF: sub_456D20+70↑j
00457237 ; DATA XREF: jpt_456D90↓o
00457237 push offset aNetworkErrorHa ; jumptable 00456D90 case 3
0045723C jmp short loc_45727A
0045723E ; ---------------------------------------------------------------------------
0045723E
0045723E loc_45723E: ; CODE XREF: sub_456D20+70↑j
0045723E ; DATA XREF: jpt_456D90↓o
0045723E lea edx, [esp+21E4h+Buffer] ; jumptable 00456D90 cases 10,11,33
00457245 push offset aYouHaveInsuffi ; "You have insufficient points to play. P"...
0045724A push edx ; Buffer
0045724B call _sprintf
00457250 add esp, 8
00457253 jmp short loc_4572AB
00457255 ; ---------------------------------------------------------------------------
00457255
00457255 loc_457255: ; CODE XREF: sub_456D20+70↑j
00457255 ; DATA XREF: jpt_456D90↓o
00457255 lea ecx, [esp+21E4h+Buffer] ; jumptable 00456D90 case 17
0045725C push offset aYouHaveAlready ; "You have already connected another game"...
00457261 push ecx ; Buffer
00457262 call _sprintf
00457267 add esp, 8
0045726A lea edx, [esp+21E4h+Buffer]
00457271 push ebx
00457272 push edx
00457273 jmp short loc_4572B4
Let’s carefully trace back a little bit to see the entire jump table and find out where the value comes from. Looking back at the top of sub_456D20, there are 2 function calls. The first one seems to be a constructor of an object, but the other one looks more interesting.
00456D65 push 4
00456D67 push eax
00456D68 mov ecx, esi
00456D6A mov [esp+21ECh+var_4], ebx
00456D71 call sub_455900
00456D76 mov ecx, [esp+21E4h+var_21B8]
00456D7A lea eax, [ecx+65h] ; switch 135 cases
00456D7D cmp eax, 134
00456D82 ja def_456D90 ; jumptable 00456D90 default case, cases -100--6,-4,-3,1,2,4-9,12-16,19-32
00456D88 xor edx, edx
00456D8A mov dl, ds:byte_457320[eax]
00456D90 jmp ds:jpt_456D90[edx*4] ; switch jump
As you can see from the above, it seems eax holds the value that determines the switch/case jump. And the value seems to be coming from sub_455900.
sub_455900 looks like a copy function from some sort of container object; presumably the response payload class!
00455900 ; const void **__thiscall sub_455900(const void **this, void *, unsigned int)
00455900 sub_455900 proc near ; CODE XREF: sub_44C380+A↑p
00455900 ; sub_44C3B0+A↑p ...
00455900
00455900 arg_0 = dword ptr 4
00455900 arg_4 = dword ptr 8
00455900
00455900 mov edx, [esp+arg_4]
00455904 mov eax, ecx
00455906 mov ecx, edx
00455908 push ebx
00455909 push esi
0045590A mov esi, [eax+0Ch]
0045590D mov ebx, ecx
0045590F push edi
00455910 mov edi, [esp+0Ch+arg_0]
00455914 shr ecx, 2
00455917 rep movsd
00455919 mov ecx, ebx
0045591B and ecx, 3
0045591E rep movsb
00455920 mov ecx, [eax+0Ch]
00455923 pop edi
00455924 add ecx, edx
00455926 pop esi
00455927 mov [eax+0Ch], ecx
0045592A pop ebx
0045592B retn 8
0045592B sub_455900 endp
The function could look like this:
response->sub_455900(&eax, 4); // where 4 is the number of bytes to read
Before we start verifying our current findings, let’s rename the following symbols:
sub_456290→dispatch_response_opcodesub_456D20→process_auth_responsesub_455900→read_response_payload
This is great! We’ve made a lot of progress with static analysis alone. But we need to see if we got everything right. So let’s do that by triggering the -1 branch, which should yield the Either login name or password is incorrect. error:
0045720E loc_45720E: ; CODE XREF: sub_456D20+70↑j
0045720E ; DATA XREF: jpt_456D90↓o
0045720E push ebx ; jumptable 00456D90 case -1
0045720F push offset aEitherLoginNam ; "Either login name or password is"...
00457214 jmp loc_4572B4
Verifying analysis
Let’s start by modifying the C# code to return the following payload when we receive the 1000 opcode request:
08 00 E9 03 FF FF FF FF
Surprise, surprise! It works! 🎉
Let’s goo! But we need to make this actually work. Not to mention, we have yet to locate the request code; so I’m hoping that we will find a similar huge switch/case table for the request as well in the subsequent steps. So let’s try to make the auth validation pass.
Looking at branch 0, there are a lot of moving parts here and it would be extremely hard to understand what’s happening without debugging, especially with so little context. But I managed to get some details out of it:
- There is a check against a global that gets set when receiving another opcode (remember
sub_429610above? It gets set in1005) - There are a bunch of string initializations:
FM,DB,FD,LE,TH,TB,LD, andLM. They could be just random binaries, but they’re likely strings. - The most interesting part:
sub_4559A0. It calls ourread_response_payloadand seems to be related to the strings.
004559A0 sub_4559A0 proc near ; CODE XREF: sub_44C3A0+8↑p
004559A0 ; sub_4569B0+11F↓p ...
004559A0
004559A0 arg_0 = dword ptr 4
004559A0
004559A0 push esi
004559A1 mov esi, ecx
004559A3 push edi
004559A4 or ecx, 0FFFFFFFFh
004559A7 mov edi, [esi+0Ch]
004559AA xor eax, eax
004559AC repne scasb
004559AE mov eax, [esp+8+arg_0]
004559B2 not ecx
004559B4 push ecx
004559B5 push eax
004559B6 mov ecx, esi
004559B8 call read_response_payload
004559BD mov eax, esi
004559BF pop edi
004559C0 pop esi
004559C1 retn 4
004559C1 sub_4559A0 endp
Additionally, there’s a validation that checks whether the result of sub_4559A0 is null or not. Here, I’d like to make another attempt at brute-forcing, where I assume:
sub_4559A0is presumably a function to read a null-terminated string from the response payload. (Renamed toread_string_response_payload)- The result is compared against one of those 2-character strings
Let’s start verifying this idea by adjusting the C# code to return the following payload when we receive the 1000 opcode request:
0B 00 E9 03 00 00 00 00 46 4D 00
// ASCII: ........FM.
The game seems to be stuck, but we’ve made progress here: the game sent another request!
Received data as byte array:
04 00 EA 03
....
Okay, there are a bunch of things running through my head right now. For starters, I’m beginning to see the pattern here. The request with opcode 1000 is paired with the response with opcode 1001. Now the client sends 1002 (0x03EA), so I could assume that it expects 1003 next. Although, the server may send messages to the client without being prompted by the client. For example, when the room master updates the music selection, the server will need to broadcast that information to the rest of the room members. So incrementing the opcode by 1 shouldn’t always be the answer, as it might be occupied by such event messages.
Going back to our test, there are no execution branches out of these strange 2-character arrays. The game seems happy to accept any incoming string as long as it matches one of those weird codes, or we don’t send the string at all. However, if the server sends a string that is not in the defined set, we get a Billing Error code, very cryptic indeed.
Unfortunately, I don’t see xrefs for those strings. Figuring this out from the client alone might be impossible. They could be referenced via a pointer at runtime, in which case we’d need to use the debugger. But I think that’s very unlikely, and I don’t think it’s important right now.
Locating the request code
It’s probably already past due time to find the request code. We can try searching 1002 (0x03EA) to get a better result now.
1002 (0x03EA).Interesting! The value was hiding under our nose all along. Looking at the search results, the code looks extremely similar in all places, which more or less resembles something like this:
00456EC1 push ebx
00456EC2 lea eax, [esp+21E8h+var_2008]
00456EC9 push 1
00456ECB mov word ptr [ecx], 1002
00456ED0 mov edx, [esp+464]
00456ED7 lea ecx, [esp+484]
00456EDE mov word ptr [edx], 4
00456EE3 lea edx, [esp+460]
00456EEA mov [esp+21ECh+var_2014], ecx
00456EF1 mov ecx, dword_48C408
00456EF7 push edx
00456EF8 mov [esp+21F0h+var_2010], eax
00456EFF call sub_455C60
sub_455C60 doesn’t look like our read_response_payload; it’s much more complex, and it calls a function pointer which we would need to debug if we want to go further. But looking at the xrefs of this function, it strongly suggests that it is indeed closely related to the request, as I have found many calls around the opcodes handling outside the dispatch_response_opcode. I assume this is where the actual packet sending happens. I’d like to stay with static analysis for now; we can come back later with our debugger if needed. For now, I have renamed sub_455C60 to maybe_send_request_packet.
maybe_send_request_packet (sub_455C60).Anyway, looking back at the xrefs of maybe_send_request_packet once again, I found the code responsible for handling opcode 1000 (0x03E8) in sub_429480.
004294D2 xor edi, edi
004294D4 sub eax, 1000
004294D9 mov [esp+2030h+var_4], edi
004294E0 jz loc_42956B
; ...
0042956B loc_42956B: ; CODE XREF: sub_429480+60↑j
0042956B mov ecx, dword_48C3E4
00429571 call sub_45FF70
00429576 test eax, eax
00429578 jnz short loc_4295A9
0042957A mov ecx, dword_48C3D8
00429580 push edi
00429581 push offset aFailedInConnec ; "Failed in connecting to the server."
00429586 push edi
00429587 push 1
00429589 call sub_41EA20
0042958E mov ecx, esi
00429590 mov dword_49398C, edi
00429596 mov dword_49D2DC, edi
0042959C mov dword_49D2E0, edi
004295A2 call sub_429B00
004295A7 jmp short loc_4295F6
004295A9 ; ---------------------------------------------------------------------------
004295A9
004295A9 loc_4295A9: ; CODE XREF: sub_429480+F8↑j
004295A9 mov eax, [esp+2030h+var_2018]
004295AD mov dword_49398C, 3
004295B7 lea edx, [esp+2030h+var_2008]
004295BB push offset unk_48C44C
004295C0 mov word ptr [eax], 1000
004295C5 mov ecx, [esp+2034h+var_201C]
004295C9 lea eax, [esp+2034h+var_2008]
004295CD mov word ptr [ecx], 4
004295D2 lea ecx, [esp+2034h+var_2020]
004295D6 mov [esp+2034h+var_2010], edx
004295DA mov [esp+2034h+var_2014], eax
004295DE call sub_455970
sub_45FF70 appears to be a class function that checks one of the dword_48C3E4 fields. When the check fails, the game will likely show the error Failed in connecting to the server.. But loc_4295A9 is where the interesting thing happens: it calls sub_455970, and inside that, it calls sub_455930. Both look extremely similar to read_string_response_payload and read_response_payload respectively. I also noticed from the xrefs that sub_455930 is used similarly to read_response_payload, but for the opcodes outside the dispatch_response_opcode function.
This strongly suggests that sub_455930 and sub_455970 are write_request_payload and write_string_request_payload respectively! To verify this idea, let’s fire up the debugger to see if unk_48C44C contains our token!
My favorite toolkit for debugging is Cheat Engine. It’s a Swiss Army knife for reverse engineering, and I highly recommend adding it to your toolkit; particularly if you find x86dbg overwhelming. Anyway, in this scenario, we don’t even have to attach the debugger in Cheat Engine; we simply have to observe the memory, so no debugger is actually involved!
Bingo! We can conclude that this is the write function for the request payload. Unfortunately, this mean request code are fragmented unlike the response handler.
Rinse and repeat
We have successfully located both the request and response handlers. We can simply check the xrefs of read_response_payload or write_request_payload to pinpoint the request and response payload logic for each network message, plus we can look at the surrounding constants to associate them with an opcode. Let’s put it to the test by implementing the response message for opcode 1002 (0x03EA).
This will be significantly harder since we don’t have as much context as we had with the auth token stuff. On the other hand, as we observed before, the client does not include any information other than the packet size and the request opcode itself. So let’s start by looking at the 1003 branch of dispatch_response_opcode to get a better idea.
004573B0 push ecx
004573B1 push ebx
004573B2 push esi
004573B3 push edi
004573B4 mov edi, [esp+10h+arg_0]
004573B8 push 4
004573BA push offset dword_48C8E4
004573BF mov ecx, edi
004573C1 call read_response_payload
004573C6 mov eax, dword_48C8E4
004573CB xor ebx, ebx
004573CD test eax, eax
004573CF jle short loc_457437
004573D1 mov esi, offset word_48C8EA
004573D6
004573D6 loc_4573D6: ; CODE XREF: sub_4573B0+85↓j
004573D6 lea eax, [esp+10h+arg_0]
004573DA push 2
004573DC push eax
004573DD mov ecx, edi
004573DF call read_response_payload
004573E4 lea ecx, [esp+10h+var_2]
004573E8 push 2
004573EA push ecx
004573EB mov ecx, edi
004573ED call read_response_payload
004573F2 mov dx, word ptr [esp+10h+arg_0]
004573F7 mov ax, [esp+10h+var_2]
004573FC lea ecx, [esi+2]
004573FF push 4
00457401 push ecx
00457402 mov [esi-2], dx
00457406 mov ecx, edi
00457408 mov [esi], ax
0045740B call read_response_payload
00457410 lea edx, [esi+6]
00457413 push 4
00457415 push edx
00457416 mov ecx, edi
00457418 call read_response_payload
0045741D lea eax, [esi+0Ah]
00457420 push 1
00457422 push eax
00457423 mov ecx, edi
00457425 call read_response_payload
0045742A mov eax, dword_48C8E4
0045742F inc ebx
00457430 add esi, 10h
00457433 cmp ebx, eax
00457435 jl short loc_4573D6
It looks like it expects an array. The read_response_payload was used on dword_48C8E4, and then it’s used in the loop as the bound. In other words, it stores the number of items in the array. Each item in the array consists of 2 + 2 + 4 + 4 + 1 bytes (13 bytes).
We could try to find out which piece of code accesses the array at 48C8EA and carefully trace how it’s used, especially when it involves sprites, which would mean it’s being displayed rather than serving as a mere internal state. But the fact that it’s an array gives us enough context to make an educated guess and brute-force our way through.
You see, both 1000 and 1002 are triggered after I clicked one of the servers in the server selection. Apart from the authentication, the game is supposed to display a list of available channels to enter. So based on this assumption, the above payload should include:
- Channel number
- The number of active users in the channel
- The maximum capacity of the channel
This assumption is also based on one of the findings from the 9you private server: the channel listing could be configured to appear out of order, which means the channel number must be included somewhere! Turns out we’re not completely out of clues with this one.
So after multiple brute-force attempts (about 3 to 4 attempts), I was able to figure out the struct completely:
int32 page(0 means the channel is displayed on the first page; in the private server, this could be controlled by additional channel server instances)int32 channel_numberint16 max_usersint16 active_usersbool is_active(it won’t be displayed if it’sfalse)
Here’s a snippet of our dummy server app to build our dummy channel list:
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
writer.Write((short)0); // we write this later
writer.Write((short)1003);
writer.Write((int)7); // Say, we have 7 channels
// Put the first 5 channels normally
for (int i = 0; i < 5; i++)
{
writer.Write((short)0);
writer.Write((short)i); // Channel number start from 0 but presented with + 1
writer.Write((int)100); // I don't think we can go past 100
writer.Write((int)(i * 25));
writer.Write((byte)1); // true
}
// Let's have 2 stray channels out of order and 1 channel padding, one of them belong to different server id (next page)
writer.Write((short)1);
writer.Write((short)6);
writer.Write((int)100);
writer.Write((int)25);
writer.Write((byte)1);
writer.Write((short)0);
writer.Write((short)0);
writer.Write((int)0);
writer.Write((int)0);
writer.Write((byte)0); // padding to give 1 empty slot for the last channel
writer.Write((short)0);
writer.Write((short)5);
writer.Write((int)100);
writer.Write((int)25);
writer.Write((byte)1);
stream.Flush();
stream.Seek(0, SeekOrigin.Begin);
writer.Write((short)(stream.Length)); // write the packet size
stream.Flush();
var buffer = stream.ToArray();
Print(buffer);
networkStream.Write(buffer);
Which yields the following payload:
70 00 EB 03 08 00 00 00 00 00 00 00 64 00 00 00 00 00 00 00 01 00 00 01 00 64 00 00 00 19 00
00 00 01 00 00 02 00 64 00 00 00 32 00 00 00 01 00 00 03 00 64 00 00 00 4B 00 00 00 01 00 00
04 00 64 00 00 00 64 00 00 00 01 01 00 06 00 64 00 00 00 19 00 00 00 01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 05 00 64 00 00 00 19 00 00 00 01
And, the moment of truth 🥁
Jackpot! We made it! That’s a wrap for today.
What’s next?
That was a lot, yet I still haven’t brought out the Cheat Engine debugger into the picture. So on the next occasion, I’d like to cover a more complex scenario where debugging is necessary to reverse the network messages.
Although, it will be tricky because such scenarios often involve internal state that is not displayed, or at least not immediately observable after the response is received by the client. Not to mention it may require a deep understanding of how the game works in general; for example, it might involve how the game populates installed music files, or we may need to be familiar with the file format of certain assets.
But I’ll try my best to cover them when the time comes. Until then, see you around!