“Kazakov 3” reverse engineering, part of the network: we create a local server
Recently, in a conversation with colleagues, we discussed various games of the RTS genre , and I wondered why the release of the third “Cossacks” passed me by. A couple of minutes and one search query later I
Well, if the mountain does not go to Mohammed ...
TL; DR: Server, instructions for use and source code are available on GitHub .
In order to understand the general structure of the protocol, the first four packets that are eavesdropped between the game client and the official server are enough. So we have:
- Network protocol built on top of the usual the TCP / the IP ,
- without encryption or obfuscation ,
- in which the numbers are transmitted in order from lowest to highest .
The packet header always takes exactly 14 bytes and contains the payload size (1), the command code (2) and two player identifiers for packet addressing (3.4). Let's take a simple
Here you can also see that the lines are preceded by their length (5). It is noteworthy that, depending on the specific command code, the data format is different and the size of the lines is indicated by one, then two, or even four bytes.
Consider a public notification about the creation of a new game room:
The header also begins with the size, command and sender (1,2,3), but the recipient identifier is missing (4). In this message, it means “send to everyone”, but for other teams it can also mean “to everyone in his room”. The first line (5) contains the name, password and information about the type of game and version of the client at the creator (host) of the room. To separate the values, the tab character \ t is used, it is 09h. Then follows a line with information about the room (6), which is required to display it in the list. It contains the status, the number of live players, computer opponents, closed slots and two more values. Here, the vertical bar plays the role of the separator. After it are followed by two constants of 4 bytes each
Anticipating questions arising from reading, I note that ...
- Yes, passwords to private rooms are transmitted to all players on the server.
(See also the article “ The Cossacks Have No Secrets ”)
- No, the host name of the creator of the room is not required for the game, because all packets are transmitted through the server.
Now let's move on to more interesting points.
Initially, I analyzed packets in Wireshark with tcp && data filters so that “empty” packets like ACK would not flash before my eyes . At some point, it turned out to me sideways: it turned out that Wireshark mistakenly accepts packets whose TCP payload starts with bytes 05h 00h for DCERPC protocol packets . In particular, this affected packets with a notification about a player entering the room, as they always contain exactly five bytes after the header. This leads to the fact that Wireshark marks the TCP load not as Data , but as [Malformed Packet: DCERPC] and hides the packet: The use of the newer tcp.payload filter would be correct in this case .
. It displays all TCP packets with a payload, regardless of how Wireshark interprets this load.
When reversing the network protocol, it was obvious that different people with different priorities worked on it at different times. Three types of lines passing variables can be distinguished:
- compact strings
The length of the string is indicated by one byte, the strings contain the names of variables, the fields are separated by vertical bars:
- intermediate strings
The length of the string is indicated by two bytes
(hereinafterreferred to as a number oftype Short ), the values are separated by a tab, no names:
- generous strings
Special strings, which are actually an array of strings with alternating variable names and their values. A kind of associative array in one-dimensional form. The length of each (sic!) Of the lines in it is indicated by a number of type Int . A detailed description of the format can be found in the comments on the server source code.Example
Numbers indicating lengths of lines are marked in red,
yellow - separators.
The last example is taken from a package that regulates the transit of the host role to one of the players when the creator of the room leaves the game. As I understand it, this functionality was further developed after the release of the game. Obviously, then the question of data transfer efficiency was no longer as acute as at the time of writing the network protocol in the original engine.
At the end of many packets, the payload ends with four or six zero bytes. But in certain packages (in particular, with the command codes 0xc8 and 0x19d ), data sometimes pops up unexpectedly among them. I just could not understand where they came from and why they were needed, until in one of these packages I found in them a fragment of one of the messages in the lobby chat.
Apparently, the official server does not always zero out the buffer into which the response packet is written before sending, and the remaining bytes may be lost in the closing bytes. Fortunately, in the game client itself this does not lead to errors. But something still says something about the pace of development and the level of quality control.
You know less - a stronger server
... or " the worse the better ." After gaining a critical mass of knowledge about the protocol used, the amount of source code stopped growing and began to melt. The interfaces were simplified, and the data that the server was forced to save (for example, to transfer the state of the lobby to new arrivals), ceased to be analyzed and began to be stored as strings. It is not necessary to know the origin and destination of each byte in the packet; just understand how and to whom to redirect it.
The server responds to most requests with data received from other clients earlier, or provokes the desired client with a special package and redirects the response to the requestor. Where possible, I omitted optional strings, replacing them with a null byte. First of all, this concerns the data on the rating of players, the number of points and victories.
In some cases, too wide packet relaying even generated client-side errors. In particular, I noticed this on the
Get in line
Of particular difficulty for me was the aspect of launching and synchronizing the game. Having dealt with packet relaying at boot, I noticed that during the game all clients send packets with the 0x4b0 command code . At the same time, the number of the creator of the room is in one field of the identifier, and the second field is empty. But if you approach logically and understand this as
Instead, the server itself must monitor the source of the packet and check whether it is a host or an ordinary player. In the first case, the packet is sent to all players in the room except the host itself, in the
By the way, that is why when changing the host during the game, a warning is displayed stating that your orders may be
Contrary to public opinion, the client still has the functions necessary for a full game on the local network. When analyzing network traffic, I noticed that among the many TCP packets, the creator of the room also sends out notifications about the number of players and the status of the room via UDP .
In addition, when disassembling the client, I came across a function that is called when exceptions occur and displays the error text. Going through her calls and analyzing the text transmitted to her, you can determine the real names of those procedures in which exceptions arise. A little surprised at the result, I played around with strings ,
and here is what I saw:
There are two options: either the developers have such humor, or they universally used the acronym LAN for everything related to multiplayer games.
C ++, Asio and unscathed legs
Since I initially set myself the goal of writing a cross-platform server and expanding my knowledge of network programming, for the implementation I chose the C ++ language and the Asio library . The latter also allowed me to abandon multithreading and related features of data access in favor of asynchrony and simpler code. As a basis, I took the source code of one of the examples in the library repository .
The most interesting aspect of development for me was the problem of the availability of the data buffer during asynchronous sending of packets. At the same time, I tried to minimize the number of allocations and copying data in memory. In addition to everything, the server should have a rather large buffer for receiving packets, as the size of the TCP payload when transmitting map data before the start of the game can exceed 800 kilobytes.
As a result, I implemented the process of reading, creating and sending packages as follows:
- When connecting a new client, the server creates an object of the Session class containing a sufficiently large (1 MiB) buffer of the std :: vector type
( hereinafter - Buffer).
- After the asynchronous read operation is completed, this address is transferred to this buffer to the main packet processing function. The next read operation will be initiated only after the completion of this function, guaranteeing the safety of data in the buffer for processing time.
- At the beginning of processing, an object of the Packet class is created , which provides an interface for reading and serializing data. Through it, the server response is written all in the same buffer of the Session object , which is assigned to the sender of the packet.
- After all operations on the package are completed, the send function allocates a buffer using std :: make_shared
. At the same time, the iterator of the same buffer in Session is passed to the Buffer constructor , given the exact size of the response packet ( Packet monitors this during recording ). Those. in one operation, we first allocate enough memory for the packet and the control block of the pointer (and at a time ), then copy exactly the number of bytes from the large buffer that should be sent to it.
- The new buffer is passed using the resulting pointer of type std :: shared_ptr
( hereinafter referred to as BufPtr) to the Session objects of all those clients to whom the packet should be sent. There it is placed in local queues like std :: deque . Each copy of the index that is in the queue of any of the clients increases the reference counter. After that, packet processing ends, and the initial Session buffer is ready to receive the next packet.
- After the asynchronous recording (sending) operation of the packet is completed, the pointer is deleted from the local Session queue of the destination client, lowering the reference counter. As soon as the packet is sent to all clients in which it was in the queue, the counter will be reset to zero and the smart pointer will free up memory on its own.
No matter how you judge, the addition of lambda expressions, various containers and smart pointers to the C ++ language standard significantly reduced the complexity of designing such systems; when writing the server, not a single leg was shot.
Shutter speed test
Having finished work on the server and having tested the basic functions, I decided to check the stability of the server and synchronization after the host transit after a long time. To do this, I created a room on the local network with three players and five complex AI . For the test, I chose the maximum parameters regarding the map size, number of deposits, population and non-aggression time. I started the game and after a couple of minutes I stopped the game process on the host computer in the task manager to simulate an unexpected shutdown. Then the two remaining clients were left to themselves all night.
Experienced players have probably already guessed what happened next. In short, the server passed the test, but the
I hope you were interested in following the process of copying the black box of the official server. I also hope that the developers of the game will take pity on the players and finally add the possibility of a multi-player game on the local network,
If you have questions or suggestions regarding the reverse or source code of the