📜 ⬆️ ⬇️

How to earn points without even starting the game

image

One evening, while sitting at the computer, I came across one indie game called “Shoot First” (the game can be downloaded absolutely free from the author’s site, and for donating any size you will get a special version with two new weapons and another type levels). Its gameplay is pretty straightforward - the player needs to run around the floors in search of a passage to the next level, if necessary collecting various items (maps, keys, etc) and simultaneously killing the enemies who have met on his way. In general, a sort of action roguelike . Despite the seeming simplicity, the game hooked me pretty much, and I spent more than one hour trying to get as far as possible and earn as many points as possible.

Speaking of glasses. After the death of the character and entering the name, the game displays an online table of records:
')
image

Having played enough, I decided to figure out how it works and try to deceive the game, saying that I earned an unreal amount of points.

How was the process, and what came of it, read under the cut (carefully, a lot of screenshots ).

The first thing that comes to mind, probably, to any person who has ever been engaged in dishonest money in sign-player games, is ArtMoney . Well, why not?

We start the game, earn some “unusual” number of points, load the process into ArtMoney and look for this very value. After much torment, I never managed to find the integer value with the number of points I had collected, which, if I changed, I would achieve my goal.

Well, okay, let's go the other way.

Obviously, the game climbs into the network to get the record table, and interaction with the network in Windows, as you know, lies on the shoulders of WinSock , the implementation of which is in WS2_32.dll . We take WPE Pro into our hands (unlike, for example, Wireshark, it can intercept packages of a specific application, which is much more convenient in our case), indicate the process of our game, die and look at the result:

image

As you can see, the game sends to teknopants.com. GET requests of the form.

/games/shootfirst/score12.php?alltime=15&monthly=15&weekly=15&daily=15&name=% name% & score = % score% & data = Floor% 20 % floor%% 20% 5b % player% P% 5d & hash = % hash%

where % name% is the player's name, % score% is the number of points, % floor% is the floor on which the player was killed, % player% is the player's number (two people can play simultaneously with one computer - 1P and 2P respectively ) and % hash% - a hash, obviously, necessary to verify the correctness of the sent data.

Please note that the GET request simultaneously contains information about what data you need to get (alltime, weekly and daily parameters) and what data you need to add (name, score, data and hash parameters).

It is clear that you can’t just change the number of points earned in the sent GET request, for this we also need to generate a new hash. Solving the problem by conducting experiments is almost pointless, so it's time to take on another tool - this time OllyDbg .

But before loading the process into OllyDbg, let's check if our game is packed. Take DiE , open the executable file of the game and see the following picture:

image

It turns out that with high probability the game is not protected by anything.

Well, great. Then we run the test subject in OllyDbg and try to find a place where the game makes GET requests. In WinSock, there are two functions that are directly responsible for sending data - send and WSASend . Switch to the module of our executable file (Alt-E -> Shoot First% version% .exe) and look for them in the list of "Intermodular calls" (right-click on the CPU window -> Search For -> All intermodular calls). Oddly enough, but there is no one or the other functions. Two options come to mind at once - a developer could copy their code from WS2_32.dll directly into their application or simply call them from some other module. The second option is much easier to track, so let's start with it.

We look at the directory with the game for some additional dynamic libraries. We find one that hints by its name that our searches will be short-lived:

image

Switch to it (Alt-E -> plaidscores.dll) and also look for send and WSASend calls . There is only one:

image

Put a software breakpoint on it (left click -> F2), die (of course, in the game) and ... stop before calling the send function:

image

Arguments are visible on the stack, the most interesting of which is Data for us. If you look at what is located at this address (right click -> Follow in Dump), then we will see the GET request that is already familiar to us:

image

Thus, we realized that sending data to the server is carried out in the plaidscores.dll module. Obviously, the module Shoot First% version% .exe must somehow tell the dll some data (at least, these are all the same points, while the hash, for example, can be generated already in the dll). There are, of course, options for transferring data here, in general, a whole mass (files, registry, sockets, etc), but in most cases, developers simply call the exported function from the dll with the appropriate arguments. We look from where we were called (for this we need to open the call stack with Alt-K):

image

As you can see, for sending data, the exe-module calls us from the place allocated on the screenshot. We remove the breakpoint from the send function, jump to the call (right-click -> Show call) and set the software breakpoint with F2. We die again and look at the situation:

image

What do we see here?

First, the name of the exported function is psSubmit .
Secondly, the state of the stack at the time of its call.

Unfortunately, it is guaranteed to understand how many arguments are passed to the exported function, it is possible only if their names were decorated (if you wish, you can read about it, for example, here ). Well, let's check. Run Dependency Walker , open our dll and look at the list of exported functions:

image

Unfortunately, their names are not decorated. In this case, we will have to analyze the code before calling the psSubmit function in search of PUSHs. Most likely, all 4 PUSHs in the case block of the above screenshot are the arguments of our investigated function. Let's look at them again:

image

With the first and last arguments of the questions should not arise - this is the name of the player, the floor on which he died, and 1P / 2P. Most likely, one of the remaining arguments is our goal - points. To understand which one is specifically of them, let's earn some of them before dying (before that I died without scoring). Press F9, perform the task, die and stop at the same place, but with other data on the stack:

image

I scored 9 points, and the value of one of the arguments really changed - now in its place 0x40220000 is flaunted. On 9 in hex, it doesn't look like much, so let's do some more experiments:

Number of points in the game - the value of the argument
6 - 0x40180000
7 - 0x401C0000
8 - 0x40200000
9 - 0x40220000
10 - 0x40240000

As you can see, the values ​​are increasing unevenly, so that we are guaranteed to reverse the conversion right now. But at least let's check that when this value changes, before the psSubmit call , the game really thinks that we scored a different number of points, and sends fake data to the server. We die without earning points, stop before calling psSubmit and change (left click -> Ctrl-E) the value of the corresponding argument to, say, 0x40220000, i.e. 9 points. Press F9 and see that our fake value really went to the server.

Now we still have two unsolved problems:


The second point is not to say that the problem, but no one likes, when what he is studying is not explicable, right? However, let us dwell on the first paragraph for now.

Once plaidscores.dll forms a GET request with a “readable” number of points, it receives an “encoded” version as input, it knows how to do the transformation we need (in principle, the exe module knows it, since it can display the number points to the player). In this regard, we can now take up the study of the decoding algorithm, but what if there is a simpler way? We have forgotten that we have a hash that looks a lot like MD5. Recalling that WinAPI has a function for getting an MD5 hash (and some other types) for the data passed to it, we can assume that the game simply calls it to get this hash, so that we can understand what the game takes hash. If there is such a call, it should be in plaidscores.dll , because, as we have seen, only four arguments are passed to psSubmit , each of which is not very similar to the MD5 hash.

The function in question is called CryptGetHashParam (in fact, there are a number of functions that need to be called one by one, but everything leads to it), so let's look for it among the “Intermodular calls”. Unfortunately, there was no such function.

Well, nothing - again we stop before calling psSubmit , jump inside this function by pressing F7 and put a hardware breakpoint on the memory area with our glasses. To do this, look for the address where points are stored in “Memory Dump” (you can use Ctrl-G) -> right-click on the first byte -> Breakpoint -> Hardware, on access -> Dword. Press F9 and get to the following place:

image

Unlike software, hardware breakpoints stop at instructions that follow after performing actions of interest to us, so we look at what is located at 0x09F60195. Here you can see the FLD instruction:

Pushes the register stack. Extended-precision floating-point format


So that's it! It turns out that the value is not coded at all, but is merely represented as a floating point number! If you look at the register ST0, then we really see the number of our points:

image

Frankly, I did not expect this, because it is simply impossible to earn a non-integer number of points (at least, visually and according to the table of records).

For a final check of our assumptions, you can use some online service :

image

Moreover, as you can see, FLD refers not only to the value we are examining, but also to the last unknown argument. Therefore, it is an 8-byte floating point number.

Looking back, I understand that Art Money could help in this situation immediately, if I knew that it was not necessary to look for an integer value at all:

image

However, is this really all? It’s not very convenient to use this solution, so I decided to write a separate program that will send a GET request with user-defined data (to simplify the code, I removed some checks):

#include <boost/scope_exit.hpp> #define WIN32_LEAN_AND_MEAN #include <Windows.h> #include <cstdlib> #include <iostream> #include <sstream> typedef void(__cdecl *submit_proc_t)(const char*, double, const char*); int main() { HMODULE scores_dll = LoadLibraryA("PlaidScores.dll"); if (scores_dll == NULL) { std::cerr << "Unable to load DLL \n"; return EXIT_FAILURE; } BOOST_SCOPE_EXIT_ALL(scores_dll) { FreeLibrary(scores_dll); }; submit_proc_t submit_proc = (submit_proc_t)GetProcAddress(scores_dll, "psSubmit"); if (submit_proc == NULL) { std::cerr << "Unable to find submit procedure \n"; return EXIT_FAILURE; } std::cout << "Enter your name: "; std::string name; std::getline(std::cin, name); std::cout << "Enter scores count: "; int scores; std::cin >> scores; std::cout << "Enter floor: "; int floor; std::cin >> floor; std::cout << "Enter player number: "; int player_number; std::cin >> player_number; std::ostringstream osstr; osstr << "Floor " << floor << "[" << player_number << "P]"; submit_proc(name.c_str(), scores, osstr.str().c_str()); std::cout << "Done \n"; } 

We start, we look excitedly at the table of records, and ... Nothing happens.

At first glance, everything looks the same as in the case of calling plaidscores.dll from the game. What went wrong? Let's try to figure it out.

We load our executable file into OllyDbg, set the breakpoint on psSubmit and look at the stack:

image

Visually, everything looks exactly the same as in the case of the game. Maybe we made a mistake with the number of arguments? But before undertaking the analysis of PUSHs before calling psSubmit from the exe-module of the game, remember how the work with dynamic libraries in Windows usually happens. You have probably heard more than once that DllMain is a function that is rather limited by what you can do in it. However, very often there is a situation when the DLL needs to initialize some data at startup in order not to do it constantly when calling each exported function. In this regard, DLL developers often provide an exported function for initialization (and also often for deinitialization) in which they perform all the actions they need. Let's see if there is such a function in plaidScores.dll using Ctrl-N:

image

As you can see, it really is. We start the game in OllyDbg, set a bryak on psInit , look where we were called from and see that, most likely, it takes two arguments:

image

One of them is the link to which you must perform a GET request (http://teknopants.com/games/shootfirst/score12.php), and the other is the string “5hoo7first12”.

Based on the new data, we will slightly change the source code of our program:

 #include <boost/scope_exit.hpp> #define WIN32_LEAN_AND_MEAN #include <Windows.h> #include <cstdlib> #include <iostream> #include <sstream> typedef void(__cdecl *init_proc_t)(const char*, const char*); typedef void(__cdecl *submit_proc_t)(const char*, double, const char*); int main() { HMODULE scores_dll = LoadLibraryA("PlaidScores.dll"); if (scores_dll == NULL) { std::cerr << "Unable to load DLL \n"; return EXIT_FAILURE; } BOOST_SCOPE_EXIT_ALL(scores_dll) { FreeLibrary(scores_dll); }; init_proc_t init_proc = (init_proc_t)GetProcAddress(scores_dll, "psInit"); if (init_proc == NULL) { std::cerr << "Unable to find init procedure \n"; return EXIT_FAILURE; } submit_proc_t submit_proc = (submit_proc_t)GetProcAddress(scores_dll, "psSubmit"); if (submit_proc == NULL) { std::cerr << "Unable to find submit procedure \n"; return EXIT_FAILURE; } std::cout << "Enter your name: "; std::string name; std::getline(std::cin, name); std::cout << "Enter scores count: "; int scores; std::cin >> scores; std::cout << "Enter floor: "; int floor; std::cin >> floor; std::cout << "Enter player number: "; int player_number; std::cin >> player_number; std::ostringstream osstr; osstr << "Floor " << floor << "[" << player_number << "P]"; init_proc("http://teknopants.com/games/shootfirst/score12.php", "5hoo7first12"); submit_proc(name.c_str(), scores, osstr.str().c_str()); std::cout << "Done \n"; } 

We start and enjoy - this time the result appeared in the table of records.

How to find out what is the maximum number of points? There are two options - either the author performs the necessary checks directly in the dll, or on the server. In both cases, the easiest way is to search the module for strings with similar content, so we right-click on the CPU -> Search for -> All referenced text strings window and carefully go over the list. Your attention should attract the following lines:

image

We set breakpoints in the places where they are accessed, as well as on the call to the send function, we transfer a huge amount of points and see that the application makes a GET-request, but after executing it, it understands that something went wrong and really refers to the string with a description of the possible cause of the error. After that, you can conduct a series of experiments and finally find out that the maximum allowable value is 2 ^ 31 - 1. The results of the experiments can be seen in the screenshot:

image

By the way, the player number can be made more than two - for example, 999P works fine.

Afterword


I do not in any way agitate to break the game, falsify the results and in every way interfere with the normal gameplay (the pleasure of the game is in any case achieved by other things). With this article I just wanted to demonstrate one of the solutions to the problem that arose on my way. I already wrote to the author of the game.

I hope that the article seemed interesting to someone.

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


All Articles