📜 ⬆️ ⬇️

OSU! Relax (basics)

Hi, Habr! I present to your attention the translation of the article Adventures in osu! game hacking .


Not so long ago I started playing OSU! and I liked her. Over time, I wanted to dig a little in the guts of this game.


Basic beatmap analysis


So, how are we going to parse the beatmap? We can make out everything from the name of the song to the difficulty settings. (We will keep things simple and only analyze points in time, hit objects and some values ​​related to the slider.)


In the standard game mode, we deal with three types of objects: a circle of hit, a slider and a counter. The documentation for the .osu file format indicates that all objects have the following components: X, Y, time, type. All of them will be included in our structure.


I don’t want to dwell on this section for too long, as it’s just reading each line, separating it and saving the results.


Getting playing time


There are several different ways to do this, but the simplest is with the Cheat Engine. If you are paranoid like me, you can do this part offline, after all, there have been many known cases of automatic bans related to using the Cheat Engine. At the very least, make sure you exit your OSU! Before proceeding.


Start by opening the Cheat Engine. If OSU! not yet running, run it now. Click the icon in the upper left corner to open the process list, from here select OSU! .Exe and click “Attach debugger to process”. Go back to OSU! .. Now make sure that no music is playing. You can do this in the main menu by clicking on the stop icon in the upper right corner.


Now go back to the Cheat Engine, enter 0 in the “Value” field and perform the first scan. As soon as it is completed, you will see more than a million results. We will reduce it to a few. Go back to OSU! and start playing music again. Now go back to Cheat Engine, set the scan type to “Increased value” and click “Next scan”. This will significantly reduce the number of results. Continue pressing the “Next Scan” button until you are left with a few results.


We almost got it. All that remains now is to dynamically receive this value. That's why we used the Cheat Engine debugger before. Right-click on each address and select <> from the drop-down menu. Some of them do not suit us, but you have to find one that looks similar when disassembling.


13654FA8 - DB 5D E8 - fistp dword ptr [ebp-18] 13654FAB - 8B 45 E8 - mov eax, [ebp-18] 13654FAE - A3 BC5D7705 - mov [05775DBC], eax 13654FB3 - 8B 35 94382104 - mov esi, [04213894] 13654FB9 - 85 F6 -  esi, esi 

I downloaded a basic external signature scanner, which we will use later in our implementation.


 DB 5D E8 8B 45 E8 A3 — Regular or 'IDA-style' signature. \ XDB \ X5D \ X 8 \ x8B \ x45 \ X 8 \ XA3 — Code-style signature.  - — Code-style mask. 

Please note that the above signature applies only to the channel Stable (Latest) release. Signatures are likely to differ in Stable (Fallback), Beta and Cutting Edge (Experimental) channels, but the search process will be the same as above.


Implementation


Now we need to find the OSU process ID! and process it. There are many different ways to do this, but it's probably easiest to use CreateToolhelp32Snapshot, as well as Process32Next to iterate through the process list.


 inline const DWORD get_process_id() { DWORD process_id = NULL; HANDLE process_list = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); PROCESSENTRY32 entry = {0}; entry.dwSize = sizeof PROCESSENTRY32; if (Process32First(process_list, &entry)) { while (Process32Next(process_list, &entry)) { if (_wcsicmp(entry.szExeFile, L«osu!.exe») == 0) { process_id = entry.th32ProcessID; } } } > CloseHandle(process_list); return process_id; }; game_process_id = get_process_id(); if (!game_process_id) { return EXIT_FAILURE; } 

Now we have the process ID, we can open the process handle.
Since we only need to read its memory, we will use PROCESS_VM_READ as the desired access flag.


 game_process = OpenProcess (PROCESS_VM_READ, false, game_process_id); if (! game_process) { return false; } 

It was most of the boring stuff. Now we only need the address of the time of the game and the way to send the key entries before we can proceed. For the first of them, we need signatures that we made earlier.


 > inline const DWORD find_time_address() { // scan process memory for array of bytes. DWORD time_ptr = FindPattern(game_process, PBYTE(TIME_SIGNATURE)) + 7; DWORD time_address = NULL; if (!ReadProcessMemory(game_process, LPCVOID(time_ptr), &time_address, sizeof DWORD, nullptr)) { return false; } return time_address; }; inline const int32_t get_elapsed_time() { // read and return the elapsed time in the current beatmap. int32_t current_time = NULL; if (!ReadProcessMemory(game_process, LPCVOID(time_address), ¤t_time, sizeof int32_t, nullptr)) { return false; } return current_time; }; 

For the last of these auxiliary functions, we need something that will press a key when we call it. Again, there are several ways to do this, but I found keybd_event, but SendInput will be the easiest. Since keybd_event is deprecated, we will use SendInput,


 inline void set_key_pressed(char key, bool pressed) { INPUT key_press = {0}; key_press.type = INPUT_KEYBOARD; key_press.ki.wVk = VkKeyScanEx(key, GetKeyboardLayout(NULL)) & 0xFF; key_press.ki.awScan = 0; key_press.ki.dwExtraInfo = 0; key_press.ki.dwFlags = (pressed? 0: KEYEVENTF_KEYUP); SendInput(1, &key_press, sizeof INPUT); } 

All that is left is to loop through the remote objects and send the inputs as you go. Initially we are at the beginning of the beatmap. Now we can read the time to find out where we really are.


 size_t current_object = 0; int32_t time = get_elapsed_time(); for (size_t i = 0; i < active_beatmap.hitobjects.size(); i++) { if (active_beatmap.hitobjects.at(i).start_time > time) { current_object = i; break; } } 

Be sure to add a check for cards with AudioLeadIn time.


 while (current_object == 0 && get_elapsed_time() < active_beatmap.hitobjects.begin()->start_time) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } 

This is where the real fun begins. You might have expected this part to be difficult, but the logic here is actually quite straightforward. We wait for the 'start time' of the current object, hold the key, wait for the 'end time', and then release it. After we release the key, we move on to the next object and continue until we reach the end of the beatmap.


 hitobject& object = active_beatmap.hitobjects.at(current_object); while (current_object < active_beatmap.hitobjects.size()) { static bool key_down = false; time = get_elapsed_time(); // hold key if (time >= (object.start_time — 5) && !key_down) { set_key_pressed('z', true); key_down = true; continue; } // release key if (time > object.end_time && key_down) { set_key_pressed('z', false); key_down = false; current_object++; object = active_beatmap.hitobjects.at(current_object); } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } 

Note that I have deducted five milliseconds since the start, this is a kind of magic number, and your mileage may differ from it. He could not push all the buttons and sliders perfectly without it. I also add two milliseconds to the end of the circle in the beatmap class. Since the circles do not need to be held, we want to release them as soon as possible. If we release them too quickly, the click can be ignored, so an additional 2 ms is needed.


Well, now we are ready to compile and test OSU! Relax!


')

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


All Articles