Reverse Engineering of Cossacks, Part Three: Thimbles in LAN

    In the yard, the end of 2016, finally, causing a storm of enthusiasm among the fans, the third part of the “Cossacks” came out ... But I was still haunted by the strange mistake in the network component of the first part. The strange thing was that when creating the game on the local network, only two people could normally launch the game. With three players, the load indicator grew painfully slowly, and starting with four, it remained at 0% altogether. Well, let's start the investigation!

    Error manifestation

    Symptoms of the problem are several. The “max ping” value displayed in the game room is too high and grows in proportion to the number of players. Although all players are connected to the same switch and regular icmp ping produces consistently less than 1 ms, delays up to 350 ms are displayed in the game room. If there are only two players in the room, then “max ping” is first ~ 90 ms, then drops per second to ~ 10 ms.

    The second symptom is the warning “no direct connection established with: player name ”. The three of us have a chance to get it somewhere around 50%, and if there are four players in the room, then it is shown constantly. Although this warning can be ignored by holding down the Ctrl key while clicking on “Start”, this indicates some “perception problem” of the connection quality from the side of the game.

    The third symptom is a slow download speed. When all players clicked "Start", the percentage indicator is displayed at the game host. It increases in steps of ~ 8%, reaches 100%, and only after that can the game begin. Given that this indicator primarily shows the progress of transferring a file of a randomly generated card from the host to the players, and the size of this file for a regular card is approximately 3.5 MB, then even 5 seconds of download on a gigabit LAN is a problem. And for the time it takes to “download” three players, you can copy the entire folder with the game.

    Start from the end

    With your permission, I will spare you the details of the beginning of this reverse and the problems that have arisen. I can only say that I was mistaken in thinking that it would be fun to disassemble the network component. Calling functions through pointers. Multithreading. Work with sendto () and recvfrom () simultaneously using DirectPlay. The lack of any intelligible documentation for this dinosaur, not to mention debugging information. My kung fu was obviously not enough here.

    So we will work as usual. Open everyone’s favorite, most beautiful program , press Alt + T and look for “no direct connection”. Find the region of memory containing the string, then through xrefwe go directly to the pointer, and then to the volumetric function of 32 kilobytes in size. Among other things, the elements of the interface of the game room are initialized in it, messages are processed, and lines are composed before being displayed. Let's call her LanLobby (). Below you can see the decision-making algorithm whether to show a warning to the player and deactivate the “Start” key:

    Note: the disassembler listings shown in the article were processed to improve readability.

    1. A small function called SomeIteration () is called. Looking ahead, I’ll say that in her body there is a call to a similar function, we will call it ImportantIteration (). If the latter returns zero at least once, then SomeIteration () immediately exits the loop and also returns zero. In this case, the transition does not occur and we run the risk of receiving a warning.

    2. Next, the status of the Ctrl button is checked. If it is clamped, the “Start” button will not be disabled.

    3. Deactivation of the "Start" button. The hStartButton variable is initialized above with the result of a function with the speaking name “addVideoButton”.

    4. The timer is checked for the passage of two seconds. Above the PreviousTick variable, the current GetTickCount () value is assigned each time the number of players in the room changes. If less than two seconds have passed since then, a transition is made and a warning is not displayed.

    5. In the end, the warning line is copied to the line intended for display on the screen as the status bar of the game room.

    Conclusion: After taking the player into the room, the game gives itself two seconds to establish a connection. Then, if ImportantIteration () still returns zero, a warning about the lack of a direct connection is displayed.

    Stumbling block

    At this stage of the reverse, I still did not understand what was happening in ImportantIteration (), and I decided to find the layout of the loading indicator line and continue to work from there. Earlier, in a futile attempt to analyze the “max ping” calculation algorithm, I noticed that all strings with numeric variables are first compiled via sprintf () and then placed on the target string. Given the format string syntax, look for the text “%%” and find a match in our LanLobby (). This is how the code fragment looks, deciding whether to show in the field of the download indicator a number with percentages or a check mark signaling the “readiness” of the game:

    It turns out that the result of the GetLoadPercentage () function, if it is less than 100, is transferred to the screen one by one. The second branch must be drawing a tick. I wonder what is so calculated in this function? GetLoadPercentage () consists of a loop that goes through the array with the data of the players and ... oops!

    And here ImportantIteration () solves the issue. Yes, we were not mistaken with the name. Given arithmetic, ImportantIteration () returns the player’s download status in the range from 0x00 to 0x0C, that is, there are only 12 steps on this “scale”. Now it’s clear why the percentage indicator increases in steps of ~ 8%.

    Now that we understand what ImportantIteration () should do, let's see where it is still used.

    In addition to the calls we know when checking connection quality and calculating interest, ImportantIteration () is called in two cases: when a warning about a poor connection is formulated - there it is used to compile a list of players for the status bar - and to check whether all players are ready. The latter is carried out in a function with a small cycle that goes through all the players and compares the degree of loading with 0x0C. If at least one player has less, then the cycle returns zero. We can safely assume that the result of the last function directly affects the activation of the Start button on the host and the ability to start the game.


    So, the simplest solution is to make corrections to the logic, depending on the result of ImportantIteration (). Fortunately, in all cases, the transitions are carried out with a positive result, that is, we only need to change all the transitions from conditional to unconditional. In the case of the so-called “Windows 7 version” of the dmcr.exe file, the entire patch can be described as follows:

      оффсет | было      | стало     | эффект
    0x00CEEA | 0x7D      | 0xEB      | игра всегда готова к старту
    0x098792 | 0x0F 0x8D | 0x90 0xE9 | у всех игроков всегда 100%
    0x09C389 | 0x0F 0x85 | 0x90 0xE9 | нет проверки соединения

    And since all the changes concern only the logic of the game room for games on the local network, and the network message itself takes place in parallel and does not depend on it, one can not be afraid of side effects. A file of a randomly created card is distributed over the network almost instantly, and the room allows you to start the game without any delay. Here you can wash your hands, but ...

    What is in the casket?

    After all, it is curious how the game room determines the loading status of players. Meet ImportantIteration (), the hero of today's article:

    Here you can notice a few interesting things:

    • One of the two parameters is passed through the ecx register.
    • All the necessary data is in memory next to each other, so why do we need to store their addresses? Instead of pointers Pointer_A and Pointer_B we will have Pointer_A and (Pointer_A + 4).
    • The memory region pointed to by the pointer parameter PlayersDataStruct is written in parallel threads and does not depend on what is happening in LanLobby ().

    In an attempt to understand what was going on in the indicated region of memory, something similar to an array of structures was discovered. Each element has a size of 0x84 bytes, and inside are stored, among other things, the name of the player, the name of the random card file used, the version of the game client, the ping value, several variables of the Boolean type, as well as several integer values ​​and / or pointers. All attempts to track records in this region are limited by memcpy () calls from other threads, and it is rather difficult to statically analyze it - xref produces 82 links, and besides initialization with the value 0x12345678, all of them are read. Those. the address of the structure is loaded into registers, the address of the desired element is calculated, and only after that reading, writing, calling other functions with a pointer as a variable, or all of the above immediately.

    In the end, I just set write tracking on a specific part of this structure, and in the body of ImportantIteration () I added a few conditional breakpoints. Actually, I turned off the debugger stop, and as a condition I specified a small IDC function that displays the contents of the register in the message window:

    Message("EDX: %08X\n", EDX), 0

    After that, I started the game and for a few seconds connected to the host on the local network. Then I stopped the debugger and, after looking at the tracking results and the contents of the message window (“Trace window” and “Output window”), I came to the following conclusion:

    The memory region from which data is extracted, among other things, and ImportantIteration (), is constantly changing. At the same time, some values ​​are first reset before new data is written. Probably somewhere in the processing of network messages before storing new information, this memory region is first reinitialized. And since we have DirectPlay and multithreading, these zeros may well be there just during the execution of our cycle, which leads to the behavior described at the beginning of the article.


    So, what morality can be drawn from this fable? You don’t need to be a Yakuza to call on Yakuza. It’s not always necessary to dig out the roots of a problem in order to solve it. By the way, in the process of reverse, a piece of code that was responsible for the “auto-loss” when minimizing the game for more than a couple of seconds caught my eye completely by accident. So now you can safely change the music during the game. True, I am not sure about the benefits of such a patch for the gaming community, as it may facilitate the manipulation of the game using third-party programs.

    In the end, I would like to return to the question of the timing of the game, raised in the first article . Then I did not fully understand the role of the functions QueryPerformanceFrequency () and QueryPerformanceCounter (). How correctly noticed AndreySmi1e , it did not look like they influenced timing. Now I can say for sure that these functions are used in the game room of the first “Cossacks” as a pseudo-random number generator to create a random map file and / or the name of this file, and the actual timing of the game is carried out exclusively through GetTickCount ().

    That's all, see you soon!


    Also popular now: