📜 ⬆️ ⬇️

Reverse Engineering "Kazakov", the last part: the second breath



After several months of working on the source code of the Cossacks: War Again game, I can finally wash my hands and present the results of my labors . In this article I would like to share with you the experience of refactoring this extraordinary project, in particular, code curiosities. All lovers of necro programming is dedicated to ...

Start


The first and one of the most unpleasant obstacles was not in the code, but in the projects themselves. There are four of them:


And these guys have seen some code! Because of the circular dependency between the IChat.dll projects and the dmcr.exe, at least one lib file from the previous build is required for linking. Project files themselves are incorrectly converted in Visual Studio 2015: they contain links to libraries that are not displayed in project properties, absolute paths to files in the system of one of the developers, and other surprises. In the end, I was bored with dancing with tambourines, and I created all the projects anew, having learned in passing that there are extra files in the archive with outdated source code, which when included in the project leads to conflicts. Well, where could it be without linking features, for which it is necessary to exclude libc.lib.
')

We start


So, with the projects figured out, now you can get down to business. The compiler is happy for us and welcomes with a lot of errors C2065: undeclared identifier . We look at the code and see the following picture everywhere:

for (int i = 0; i < max1; i++) { /*  â„–1 */ } for (i = 0; i < max2; i++) { /* C2065 */ } // : for (int i = 0; i < max && arr[i] != value; i++); /*      */ if (i < max) { /*    .  C2065   */ } 

Of course, you could put / Zc: forScope- and forget about it, but we are not firemen and carpenters. Rule pens more than a hundred of these pieces of code, we continue.

The next obstacle was in the graphic element, more precisely in DirectDraw 7. It actively uses the system palette replacement mechanism. And if earlier it was common practice, then since Windows Vista such tricks no longer pass. The fact is that DWM with Windows Aero closely work with the palette and do not tolerate competition. As a result, many old games suffer from color distortion.

Not being a DirectX expert, I began to look for a ready-made solution and found it in the version of "Cossacks", published on Steam in 2010. In addition to the ddraw.dll library itself, there is an additional library mdraw.dll in the game folder, which exports the initialization function DirectDrawCreate (). To be honest, I don’t know what exactly the guys from GSC wrote in their DDemu DirectDraw Emulator library in 2008, but it does an excellent job with its task. Without thinking, I added the appropriate wrapper to Ddini.cpp and forgot about this issue.

Then there was the question of debugging a full-screen application. Here I was lucky again - the code provided a debug mode in which the game was launched in the corner of the screen in a frameless window with a fixed size. I only needed to bring it to mind, add a change of resolution, process the capture and return the cursor depending on whether the player is in the menu or in the active game and add the corresponding parameters at the start. Now it was possible to conveniently start the game in the debugger with the key / window.

Small retreat
The following are the weird, problematic and erroneous sections of the Cossacks: War Again game code that I encountered during my work. Please note that the purpose of this article in no way is to criticize or ridicule the developers of this game. I believe that “Cossacks: War Again” is an exceptional result of diligent robots and painstaking optimization of a small team of developers who have raised the bar for performance and scope of battles for RTS games very high. Thank you, GSC!

Fun arithmetic


One of my goals was to add settings for multiplayer games, for example, the ability to disable the diplomatic center and the market or limit the available ships in the shipyard. Expanding the interface of the game room and adding the necessary branches in the code, I increased the array PlayerInfo.UserParam [], in which these settings are stored, from seven to ten elements. But I couldn’t test new options in any way - when starting the game, the AI ​​started to control my peasants instead of their own and play for me, while its peasants stood motionless. It's fun, but it won't do that.

The reason for this behavior of the AI ​​was in the following trick with the ears when copying settings from the game host to the clipboard:

 //PlayerInfo PINFO[8]; //byte* BUFB = (byte*) ( BUF + 10 + 8 + 32 - 10 ); memcpy( BUFB, PINFO[HostID].MapName + 44 + 16, 16 ); 

And this is how the PlayerInfo structure is declared:

 #pragma pack(1) struct PlayerInfo { char name[32]; DPID1 PlayerID; byte NationID; byte ColorID; byte GroupID; char MapName[36 + 8]; int ProfileID; DWORD Game_GUID; byte UserParam[7]; byte Rank; word COMPINFO[8]; //… ( 12 ) } 

As you can see, by offset MapName + 60 is COMPINFO [8]. Accordingly, as the UserParam [7] array increases, the memcpy () call misses, and incorrect data about which players the AI ​​should play for gets into the buffer. The problem is solved by replacing offset mathematics with a direct call at PINFO [HostID] .COMPINFO.

As a result, I still decided not to touch UserParam [], but to add UserParam2 [3] array at the end of the structure, since the client version is stored in one of the last elements and any change of structure before it is fraught with incorrect definition of versions in the game lobby. And so players with version 1.35 will see that others have an updated version of the game.

What lessons can you learn from this?


Invisible Joe Define


Understanding the mechanics of displaying in-game text messages in order to increase the display time and the maximum number of messages on the screen, I came across an interesting constant :

 #define SIGNBYTE ' ' void ShowCharUNICODE( int x, int y, byte* strptr, lpRLCFont lpr ) { if (strptr[0] == SIGNBYTE) { /*  */ } else { /* ascii */ } } 

“So what's the big deal? - you ask - Just a space as a constant. " Well, firstly, a space would be an extremely strange choice to identify something in a line of text, and secondly, it is not a space at all. SIGNBYTE is defined as 0x7F, or the escape character DEL. And if your browser is sufficiently circumspect and at least shows that there is something between the quotes, then Visual Studio 2015 faithlessly draws '' , between which the cursor “stumbles” one character.

Please, if you use non-printable characters, specify them in the code in hexadecimal value, and not as a character.

Legal aspect


Every time I am surprised when administrator rights are required to start the game. And every time I think something like “well, how can you program games somehow.” But this time I had the code on my hands, and I collected it myself, and the UAC window still did not give me rest.

The answer was found quite by accident, when I thought that it would be nice to write in the properties of the executable file that this version of the game is not original and is not supported by the developers. Naturally, there was no manifesto in the project, but there was a resource file Script1.rc . What was my surprise when after changing the VS_VERSION_INFO block, the game no longer required administrator rights!

It turns out that Windows, starting with Windows Vista, uses a heuristic algorithm to identify applications that may require elevation of privileges. This feature is called “Installer Detection Technology” (see the article in the Windows IT center ), and it usually responds to keywords like install or setup. But in our case, the culprit was the CompanyName parameter - if it contains the string "-GSC- \ 0", then UAC wakes up and requires administrative rights.

How to protect your application from such heuristics on the part of Microsoft, existing and coming? No way. Today you are developing games, and tomorrow you are already on a par with Inno Setup and InstallShield.

Guerrilla sscanf ()


I came across this bug when after adding additional game settings when playing records of past games, a synchronization error occurred, i.e. the recording did not go along the course along which the game developed. At the same time, the error appeared, apparently by chance and for a long time I could not find the reason.

I'll skip the boring debugging details and go to the juice itself. The game settings, the map view and the data of the selected nations are transmitted through the file name of the random map, which has the format RN0 1A3C 12345 0KFH31CJ 4501326.m3d , where


And now let's look at the code segment that processes the game settings, reading the random card file name when loading the game record:

 int v1, v2, v3, ADD_PARAM; char ccc[32]; int z = sscanf( Name, "%s%x%x%x%d", ccc, &v1, &v2, &v3, &ADD_PARAM ); if ( z == 5 ) { /*    ADD_PARAM */ } 

The catch here is that the fourth variable indicates the type % x , while the range of characters in it goes beyond the hexadecimal system and extends to the letter K. If there are players in the game who have chosen nations with an index higher than F, then sprintf () will prematurely finish parsing and return 4. The parameters will not be interpreted, the AI ​​will have incorrect information about the game and it will make other decisions, which will lead to out of sync.

In addition to this, there is the fact that sprintf () is called exclusively for ADD_PARAM — the other variables are not used anywhere. The solution is relatively simple:

 int options = 0; int z = sscanf( Name, "%*s %*s %*s %*s %d", &options ); if ( 1 == z ) { /*    ADD_PARAM */ } 

The * flag indicates functions that the value should not be stored in a variable. By the way, see how I implemented the coding of 10 game settings in place of the same 7 digits here . “Why?” - you ask. But because changing the length of the line with the name of the map file at my own discretion seemed to me not a very good idea (see above in “Merry arithmetic”).

The most interesting thing for me here is the fact that the bug manifested itself only when compiled in MSVC 14 . It turns out that the implementation of the sscanf () function in the standard library over the years has become more stringent and will not continue to forgive such freedoms on the part of programmers.

Memo: Follow first the requirements of the documentation, and not the principle of "working code - the correct code."

Subtleties of language


Localization is a separate topic for any developer, but this is the first time I saw this:

 #define RUSSIAN #define _CRYPT_KEY_ 0x78CD #ifdef RUSSIAN #undef _CRYPT_KEY_ #define _CRYPT_KEY_ 0x4EBA #endif 

If this is not enough for you, I suggest to look under the spoiler and see what this “crypto-key” is for.

Do not do this. You are welcome.
 VOID CGSCarch::MemDecrypt( LPBYTE lpbDestination, DWORD dwSize ) { BYTE Key = (BYTE) ~( HIBYTE( _CRYPT_KEY_ ) ); isiDecryptMem( lpbDestination, dwSize, Key ); } void isiDecryptMem( LPBYTE lpbBuffer, DWORD dwSize, BYTE dbKey ) { _asm { mov ecx, dwSize mov ebx, lpbBuffer mov ah, dbKey next_byte: mov al, [ebx] not al xor al, ah mov [ebx], al inc ebx loop next_byte } } 


This is the simple way to screw up localization at the compilation stage. If, for example, the “English” dmcr.exe is slipped into the archive with resources from the Russian version, then all that remains of the game is the access violation error window. Because neither before nor after “isi memory decryption” the contents of the buffer are checked. But if we unpack the archive all.gsc, replace the files and pack it back, then the Russian interface will be waiting for us in the game.

Having looked at this XOR-orgy, I decided to limit myself to the English version, but with the support of the Cyrillic alphabet in the chat. Since all the text is drawn through the game's own fonts, I copied the mainfont.gp resource from the Russian version. It remains only to catch the characters that fall outside the ascii range, and correctly match the letter codes with the “frame index” of this file (the GP format is used everywhere in the game for storing graphics, including animation). Not the most elegant solution , but it works flawlessly, and on the server in a chat with players under version 1.35, too.

UDP without holes


Unfortunately, the original Cossacks did not implement the UDP hole punching mechanism , which would allow players to connect to the game rooms, even if their host is located on the other side of their provider’s NAT .

Fortunately, the comrade, known as [-RUS-] AlliGator, who started and maintains the server cossacks-server.net, allocated some of his time and we agreed on an additional protocol by which the game host will support a UDP connection to the server, and by which the server will be able to communicate the host's external UDP port to players who wish to connect to it.

All implementation details can be viewed in the UdpHolePuncher class. The connection is initialized when the game room is created by the host, after which it keeps in touch until the game starts, sending small packets. This is necessary because NAT can assign a different external port with each new UDP connection, and so the server probably knows that at this point in time the host is available because of NAT on the port from which the packets come.

Corresponding changes were made to the procedure for processing server commands and to the RoomInfo structure in the IChat.dll library. The following additional variables are supported when creating a game room:


After receiving this data, the game host opens an additional connection and maintains it. Those who wish to join receive an additional variable % CG_PORT when connected to a room. And only if it is not there, the constant DATA_PORT is used .

Although this whole mechanism has already been implemented in the game client, I haven’t managed to test it yet, because contact with the Alligator broke. Shortly before this, he laid out the source code of his server for open access - thank you for that! - so if someone is ready to raise the baton, I will be all for it.

Afterword


The article already came out longer than I had planned, so I will be brief. Although this project took a long time and significantly depleted my enthusiasm for reverse engineering and source code analysis, I am glad that I took it. I am glad that I wrote an article on Habr with a description of my first, assembler, crutch for Kazakov. I am glad that Max fsou11 came to the comments and laid out the source code of the game for free access. I am also grateful to the LCN community for valuable advice, explanations, and testing assistance.

References:

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


All Articles