OSU! リラックス(基本)

こんにちは、Habr! 記事Adventures in osuの翻訳を紹介します ゲームハッキング







少し前にOSUをプレイし始めました! そして私は彼女が好きだった。 時間が経つにつれて、私はこのゲームの内部をより深く掘り下げていきたいと思いました。







基本的なビートマップ分析



では、ビートマップをどのように分解するのでしょうか? 曲の名前から複雑な設定まですべてを確認できます。 (私たちは物事をシンプルに保ち、瞬間だけを分析し、オブジェクトとスライダーに関連するいくつかの値をヒットします。)







標準ゲームモードでは、サークルヒット、スライダー、カウンターの3種類のオブジェクトを扱います。 .osuファイル形式のドキュメントは、すべてのオブジェクトが次のコンポーネントを持っていることを示しています:X、Y、時間、タイプ。 それらはすべて構造に含まれます。







このセクションでは、各行を読み取って分割し、結果を保存するだけなので、あまり長く使いたくありません。







ゲーム時間を取得する



これを行うにはいくつかの異なる方法がありますが、最も簡単な方法はCheat Engineを使用することです。 あなたが私のように妄想している場合、この部分をオフラインで行うことができます。 少なくとも、OSUを終了してください!先に進む前に。







チートエンジンを開くことから始めます。 OSUなら! 開始されるまで、今すぐ開始します。 左上隅のアイコンをクリックしてプロセスのリストを開き、ここからOSU!.Exeを選択し、「デバッガーをプロセスにアタッチ」をクリックします。 OSUに戻る!..次に、音楽が再生されていないことを確認します。 メインメニューでこれを行うには、右上隅の停止アイコンをクリックします。







Cheat Engineに戻り、「Value」フィールドに0を入力して、最初のスキャンを実行します。 終了すると、100万件以上の結果が表示されます。 これをいくつかに減らします。 OSUに戻ります! もう一度音楽の再生を開始します。 Cheat Engineに戻り、スキャンタイプを「Increased value」に設定して、「Next Scan」をクリックします。 これにより、結果の数が大幅に減少します。 いくつかの結果が表示されるまで、「次のスキャン」ボタンをクリックし続けます。







ほぼ手に入れました。 今残っているのは、この値を動的に受け取ることだけです。 これが、以前にCheat Engineデバッガーを使用した理由です。 各アドレスを右クリックして、ドロップダウンメニューから[<>]を選択します。 それらのいくつかは私たちには適していないが、分解すると似たようなものが見つかるはずだ。







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
      
      





基本的な外部署名スキャナーをダウンロードしました。これは後の実装で使用します。







 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.
      
      





上記の署名は、安定(最新)リリースチャネルにのみ適用されることに注意してください。 シグニチャは、安定(フォールバック)、ベータ、およびカッティングエッジ(実験)の各チャネルで異なる可能性がありますが、それを見つけるプロセスは上記と同じです。







実装



ここで、OSUプロセスIDを見つける必要があります! 処理します。 これにはさまざまな方法がありますが、プロセスのリストを反復処理するには、CreateToolhelp32SnapshotとProcess32Nextを使用するのがおそらく最も簡単です。







 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; }
      
      





これでプロセス識別子が得られ、プロセス記述子を開くことができます。

メモリを読み込むだけなので、必要なアクセスフラグとしてPROCESS_VM_READを使用します。







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





これは退屈なもののほとんどでした。 ここで必要なのは、ゲーム時間のアドレスと、続行する前にキー入力を送信する方法です。 それらの最初の場合は、以前に作成した署名が必要です。







 > 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; };
      
      





これらのヘルパー関数の最後には、呼び出すときにキーを押すものが必要です。 繰り返しますが、これを実装する方法はいくつかありますが、keybd_eventが見つかりましたが、SendInputが最も簡単です。 keybd_eventは推奨されないため、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); }
      
      





残っているのは、削除されたオブジェクトを反復処理し、移動しながら入力を送信することです。 最初は、ビートマップの最初にいます。 今、私たちは本当に自分がどこにいるかを知るための時間を読むことができます。







 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; } }
      
      





AudioLeadIn時間を持つカードのチェックを必ず追加してください。







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





それが本当の楽しみの始まりです。 この部分は複雑だと思っていたかもしれませんが、実際のロジックは非常に単純です。 現在のオブジェクトの「開始時間」を待ち、キーを保持し、「終了時間」を待ってからリリースします。 キーを放した後、次のオブジェクトに進み、ビートマップの最後に到達するまで続行します。







 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)); }
      
      





開始から5ミリ秒を減算したことに注意してください。これは一種の魔法の数字であり、あなたの走行距離はそれとは異なる場合があります。 彼はすべてのボタンとスライダーを完全に押すことができませんでした。 また、ビートマップクラスのラップの最後に2ミリ秒を追加します。 サークルを保持する必要がないため、できるだけ早くリリースしたいと思います。 それらをあまりにも速く行かせると、プレスは無視される可能性があるため、さらに2ミリ秒が必要です。







さて、これでOSUをコンパイルしてテストする準備ができました!リラックス!








All Articles