📜 ⬆️ ⬇️

Reverse engineering and slowdown "Kazakov"



In the famous game "Cossacks: Again War" there is a bug that reduces the pleasure of a network game to zero: The inhuman speed of the gameplay on modern computers. At the same time, the change in game speed in the settings, which works perfectly in the single player mode, does not affect what is happening in the game over the network. This question is discussed on many forums, but the most popular tips are:

  1. Artificially load the processor core on which the game is running
  2. Run the game in a virtual machine with limited resources
  3. Play not on the local network, but on the Internet - there are more delays

The first two options lead to the fact that the game is slow, but with jerks. The sound quality also drops. The third option is no comments at all.
')

Getting started


First, look for the value of the speed setting using the Cheat Engine . This should be either a certain multiplier, directly proportional to the position of the slider in the settings menu, or an inversely proportional interval. This memory cell is quickly located:



This interval is 0 at maximum speed, and the comfortable for me game speed corresponds to the interval 20. Changing the value during the game immediately affects the speed of the game process. Well, let's see what we have in a network game. Load, change the value in the Cheat Engine, and ... nothing. Only the position of the slider in the settings changes. Okay, let's see where this interval is processed. Cheat Engine shows only two adjacent addresses at which the cell is being read:



Let's look at the listing of the built-in disassembler:



Offhand, you can say that the interval is doubled and compared with something, and if it is something less than twice the interval, then the transition is made somewhere back. Great, we are launching the Tatar program, our favorite for such cases, and we see such a picture at the very end of one long function:

After some digging we find out the following:


We are looking for an error


Put a breakpoint somewhere in the comparison subprocess with an interval. We start a single game and immediately fly to the debugger. We start the game over the network - the breakpoint does not work. Moreover, if you put a breakpoint at the very beginning of the function, then it always works. It turns out that it is in a multiplayer game that the interval check is deliberately not performed. The matter remains for the small: Find the responsible for this branching in the function and change it so that the branch with the required sub-procedure loc_4D1ABF is always executed.

We will go from the bottom up. First, we set a breakpoint in the sub-procedure loc_4D1A9C. Bingo! In a single-player game, the word_611B60 variable is always equal to 1, so the jump condition is not fulfilled and control is transferred first to loc_4D1AAA, and from there to our sub-procedure. When playing online, the word_611B60 variable is always 2, which leads to a jump forward immediately to loc_4D1AE6 and to the end of the function. To make the game always transfer control to the branch with the sub-procedure loc_4D1ABF, it is enough to replace the comparison instruction cmp edx, 2 with cmp edx, 3 . Like two bytes on asphalt!


Not so simple


Now the speed setting works in the network game. Unfortunately, the tar didn’t do without a spoon: Over time, one of the players begins to greatly increase the speed of scrolling and the rate of water animation. After some time, the effect disappears and appears in another player. In this case, the speed of the rest of the game processes for both is the same and corresponds to the one set in the settings.

I did not manage to find out the reason for this behavior, but a strong suspicion arose at the loc_4D1AAA sub-procedure that caused ProcessMessages. Apparently for the game on the network, this challenge was not needed. Perhaps it was because of the above described strange behavior that a circumvention of this sub-procedure was made? In any case, we will try to exclude it from the branch, leaving only the loc_4D1ABF subprocedure useful to us.

We get a calculator


So what we need to do:

  1. Replace the jump with the condition in the sub-procedure loc_4D1A9C with a short direct jump to the sub- procedure loc_4D1ABF
  2. Change the reverse jump offset in the sub-procedure loc_4D1ABF to close it on yourself and not fall into loc_4D1AAA
  3. Remove the second jump instruction to loc_4D1AAA , located in the block immediately after the sub- procedure loc_4D1ABF

With the first point, everything is clear: the operating code of the short jump is EB , and the offset we need is determined by subtracting the address following the byte jump instruction from the address of the beginning of the sub-procedure loc_4D1A9C.

The third point is even simpler: Replace the jump instruction with two nop 's.

The second point requires the calculation of the offset for a short jump back. Fortunately, I stumbled upon an article that clearly describes the algorithm of this action, namely: Subtract the destination address from the address following the byte jump instruction, then subtract 1h , then convert the number to a binary form (byte size), invert and re-convert to hexadecimal the system. The resulting number is the offset we need.

Well, gentlemen. Patch!





Result


Open the modified file in the same beloved program and see the following picture:



The block containing the ProcessMessages call will never be executed. After we turned off the test for multiplayer game in the sub-procedure loc_4D1A9C, the control passes to our sub-procedure with a call to GetTickCount and a comparison with the interval. If the difference is less than the interval, then the subprocedure jumps back to the beginning of itself until the interval is met.

Now the game behaves as it should. The speed of the game corresponds to the lowest speed among players, synchronization is not disturbed. The scrolling speed is also configurable.

Afterword


Since this is my first experience with reverse engineering and working with assembler, this is most likely not the most elegant solution. The root of the problem lies in using the functions QueryPerformanceFrequency and QueryPerformanceCounter, on which the timing of the game is based. These functions are called once when creating a new game, setting the tone for all subsequent calculations with GetTickCount . Unfortunately, I did not manage to influence this part of the program properly, because I was too lazy, and some strange arithmetic was going on around the challenge of the above mentioned functions .

UPD: With a careful analysis, it quickly becomes clear that the QPF and QPC functions are misused . Their results are summarized, and the QPC is subsequently not invoked anywhere else. The variable storing the result is subsequently used before calling functions that work with strings. So QPF and QPC are most likely used here only as PRNG in the process of creating a random map and / or map file name.

Links



UPD: At the request of VRV , the version of “Cossacks” was also patched, available through Steam. The discussion he created is on the LCN forum .

The patcher for various versions of the Kazakov executable file is available here .

Source: https://habr.com/ru/post/277067/


All Articles