Visualisasi waktu kelahiran Roshan

Artikel ini membahas intersepsi fungsi API grafis menggunakan contoh DirectX 9 untuk x64 sehubungan dengan permainan Dota 2 .

Ini akan dijelaskan secara rinci bagaimana menyusup ke dalam proses game, bagaimana mengubah aliran eksekusi, deskripsi singkat dari logika yang diterapkan diberikan. Pada akhirnya, kita akan berbicara tentang fitur rendering lain yang disediakan engine.



Penafian: Penulis tidak bertanggung jawab atas penggunaan pengetahuan yang Anda peroleh dalam artikel ini atau kerusakan yang diakibatkan oleh penggunaannya. Semua informasi yang disajikan di sini hanya untuk tujuan pendidikan. Terutama bagi perusahaan yang mengembangkan MOBA untuk membantu mereka berurusan dengan curang. Dan, tentu saja, penulis artikelnya adalah seorang pengemudi bot, seorang penipu, dan dia selalu begitu.
Kalimat terakhir layak dijelaskan - saya untuk kompetisi yang adil. Saya menggunakan cheat hanya sebagai minat olahraga, meningkatkan keterampilan membalikkan, mempelajari karya anti-menipu, dan hanya di luar kompetisi peringkat.

1. Pendahuluan


Artikel ini direncanakan sebagai seri pertama dan memberikan gambaran tentang bagaimana Anda dapat menggunakan API grafis untuk keperluan Anda sendiri, menjelaskan fungsionalitas yang diperlukan untuk memahami bagian selanjutnya. Saya berencana untuk mencurahkan artikel kedua untuk mencari pointer ke daftar entitas di Sumber 2 (juga menggunakan Dota 2 sebagai contoh) dan menggunakannya bersama dengan Source2Gen untuk menulis logika "tambahan" (sesuatu seperti ini kemungkinan besar akan menampilkan "peretasan peta" (periksa perhatian pada kutipan, apa yang dipertaruhkan dapat dilihat dalam video), atau otomatisasi artikel pertama). Artikel ketiga direncanakan dalam bentuk penulisan driver, berkomunikasi dengannya (IOCTL), menggunakannya untuk memintas perlindungan VAC (sesuatu yang mirip dengan ini ).

2. Mengapa saya membutuhkannya?


Saya membutuhkan penggunaan API grafis untuk men-debug bot saya secara visual, yang saya tulis untuk Dota 2 (informasi visual yang real-time sangat nyaman). Saya seorang mahasiswa pascasarjana dan saya terlibat dalam rekonstruksi kepala 3D dan morphing menggunakan gambar dan kamera mendalam - topik ini cukup menarik, tetapi bukan yang favorit saya. Karena saya telah melakukan ini untuk tahun kelima (dimulai dengan program master), saya mengerti satu hal - ya, saya telah mempelajari bidang ini dengan cukup baik, saya dengan mudah mempelajari artikel dengan metode dan pendekatan, dan mengimplementasikannya. Tapi itu saja, saya sendiri hanya bisa mengoptimalkan algoritma yang dipelajari berikutnya, membandingkannya dengan yang sudah dipelajari dan diimplementasikan dan memutuskan apakah akan menggunakannya dalam tugas tertentu. Ini adalah akhir dari optimasi, tidak mungkin untuk menghasilkan sesuatu yang baru, yang sangat penting untuk sekolah pascasarjana (kebaruan studi). Saya mulai berpikir - selagi ada waktu, Anda bisa menemukan topik baru. Anda sudah perlu memahami topik dengan baik (pada level saat ini) atau Anda dapat dengan cepat menariknya.

Pada saat yang sama, saya bekerja di game dev, dan ini mungkin yang paling menarik dari apa yang bisa dilakukan oleh seorang programmer (pendapat pribadi) dan sangat tertarik dengan topik AI, bot. Pada saat itu, ada dua topik yang saya ketahui dengan cukup baik - kemudian saya sedang membangun mesh navigasi dinamis (client-server) dan mempelajari bagian jaringan penembak dinamis. Sebuah topik dengan navigator dinamis tidak langsung cocok - saya melakukan ini selama jam kerja, saya harus meminta izin untuk penggunaannya dalam ijazah dari manajemen, selain itu, topik kebaruan terbuka - saya juga mempelajari dan menerapkan pendekatan dengan baik oleh artikel, tetapi ini bukan hal baru. Topik dengan bagian jaringan dari penembak dinamis (saya berencana menggunakannya untuk interaksi dalam realitas virtual) sekali lagi memecah kedua fakta bahwa saya melakukannya selama jam kerja, dan tentang hal baru, Anda dapat membaca serangkaian artikel dari Pixonic, di mana penulis sendiri mengatakan bahwa topik tersebut ini menarik, hanya pendekatan yang ditemukan 30 tahun yang lalu dan belum banyak berubah.

Sekitar waktu ini, OpenAI merilis bot mereka. Ini tentu bukan 5 oleh 5 , tapi itu mengagumkan! Saya tidak bisa membuang pikiran untuk mencoba membuat bot dan pertama-tama saya mulai berpikir tentang bagaimana menggunakannya sebagai disertasi, tentang hal-hal baru, dan bagaimana menyajikannya kepada seorang pemimpin. Dengan kebaruan dalam hal ini, semuanya jauh lebih baik - pastinya memungkinkan untuk menemukan sesuatu untuk dua topik sebelumnya, tetapi tampaknya bot membuat saya berpikir, berpegang teguh, mengembangkan dan mencari ide-ide yang jauh lebih kuat. Jadi, saya memutuskan untuk membuat bot 1 lawan 1 (pertarungan pada pertengahan, seperti OpenAI), menyajikannya kepada pemimpin, memberi tahu betapa kerennya itu, berapa banyak pendekatan yang berbeda, matematika, dan yang paling penting, yang baru.

Hal paling penting yang dibutuhkan bot pada tahap pertama adalah pengetahuan tentang lingkungannya - saya bermaksud mengambil keadaan dunia dari memori permainan dan menghabiskan tahap pertama mencari pointer ke Daftar Entitas dan integrasi dengan gagasan doa Dog2 Source2Gen - hal ini menghasilkan struktur mesin Source2, yang dibutuhkan dari sirkuit. Gagasan utama dan prasyarat untuk munculnya skema adalah replikasi keadaan antara klien dan server, tetapi tampaknya para pengembang sangat menyukai gagasan itu dan mereka mendistribusikannya secara lebih luas, saya sarankan Anda baca di sini .

Saya memiliki pengalaman rekayasa terbalik: Saya membuat cheat untuk Silent Storm, membuat generator kunci (yang paling menarik adalah untuk Black & White) - apa keygen dapat dibaca dari DrMefistO di sini , eksekusi combo di Cabal Online (semuanya rumit oleh kenyataan bahwa game ini dilindungi oleh Game Guard , lindungi dari ring0 (di bawah driver dalam mode kernel), menyembunyikan prosesnya (yang setidaknya tidak membuatnya mudah untuk menyusup) - detail lebih lanjut bisa dibaca di sini ).
Oleh karena itu, saya mengalami perkembangan di area ini, bot mendapatkan akses ke lingkungan untuk waktu yang direncanakan. Sungguh menakjubkan betapa banyak informasi yang direplikasi oleh server bunker melalui delta ke klien, misalnya, klien memiliki informasi tentang teleporter, kesehatan, dan perubahannya di antara agen (kecuali Roshan, ia tidak meniru) - semua ini ada dalam kabut perang. Meskipun saya menemui beberapa kesulitan, inilah yang akan saya bicarakan di artikel selanjutnya.
Jika Anda memiliki pertanyaan mengapa saya tidak menggunakan Dota Bot Scripting , saya akan menjawab dengan kutipan dari dokumentasi:
API dibatasi sedemikian rupa sehingga skrip tidak dapat menipu - unit dalam FoW tidak dapat ditanyakan, perintah tidak dapat dikeluarkan untuk unit skrip tidak mengontrol, dll.
Seri artikel ini ditujukan untuk pemula yang tertarik dengan topik reverse engineering.

3. Mengapa saya menulis tentang ini


Akibatnya, saya menghadapi banyak masalah dalam implementasi bot dari ml, yang saya habiskan cukup waktu untuk memahami bahwa dua tahun sebelum akhir pelatihan saya tidak bisa melampaui pengetahuan dan pengalaman saya dalam topik saat ini. Di Dota 2 saya tidak bermain dari rilis adat Dota Catur Otomatis, saya sekarang menghabiskan waktu luang saya di diploma dan membalikkan Apex Legend (strukturnya sangat mirip dengan Dota 2, seperti yang terlihat bagi saya). Dengan demikian, satu-satunya manfaat dari pekerjaan yang dilakukan adalah publikasi artikel teknis tentang topik ini.

4. Dota 2


Saya berencana untuk menunjukkan prinsip-prinsip ini pada gim yang sebenarnya - Dota 2. Gim ini menggunakan anti- cheat Valve Anti Cheat . Saya sangat suka Valve sebagai perusahaan: produk yang sangat keren, direktur, sikap terhadap pemain, Steam, Source Engine 2, ... VAC. VAC bekerja dari mode pengguna (ring3), ia tidak memindai semuanya dan tidak berbahaya dibandingkan dengan anti-cheat lainnya (semua yang dilakukan esea (khususnya anti-cheat mereka) membuat semua keinginan untuk menggunakan platform ini hilang). Saya yakin bahwa VAC melakukan tugasnya dengan cara yang sangat hemat - tidak memantau dari mode kernel, tidak melarang perangkat keras (hanya akun), tidak memasukkan tanda air ke tangkapan layar - berkat sikap Valve terhadap pemain, mereka tidak memasang antivirus penuh untuk Anda, seperti yang mereka lakukan Game Guard, BattlEye, Warden, dan lainnya, karena semua ini diretas dan sebagai tambahan menghabiskan sumber daya prosesor yang bisa diambil oleh game (bahkan jika ini dilakukan secara berkala), ada positif palsu (terutama untuk pemain di laptop). Apakah tidak ada retasan dinding, aimbot, retasan cepat, ESP di PUBG, Apex, Fortnite?

Sebenarnya tentang Dota 2. Permainan berjalan pada frekuensi 40Hz (25 ms), klien menginterpolasi kondisi permainan, prediksi input tidak digunakan - jika Anda memiliki kelambatan, permainan - penting bahkan bukan permainan, unit yang dikendalikan - benar-benar dibekukan. Server permainan mekanik menukar pesan terenkripsi dengan klien melalui RUDP (UDP andal), klien pada dasarnya mengirim input (jika Anda menjadi tuan rumah lobi, perintah dapat dikirim), server mengirimkan replika dunia permainan dan tim. Navigasi dilakukan pada kisi 3D, masing-masing sel memiliki jenis paten sendiri. Gerakan dilakukan menggunakan navigasi dan fisika (ketidakmungkinan melewati celah pengocok, kogi clokverka, dll.).

Keadaan dunia dengan semua entitas ada dalam memori dalam bentuk paling murni tanpa enkripsi - Anda dapat mempelajari memori game menggunakan Cheat Engine. Kebingungan tidak berlaku untuk string dan kode.

DirectX9, DirectX11, Vulkan, OpenGL tersedia dari API grafis.


5. Pernyataan masalah


Dalam permainan Dota 2 ada "kuno" netral, pembunuhan yang memberikan hadiah yang baik: pengalaman, emas, kemampuan untuk memutar kembali cooldown keterampilan dan benda, Aegis (kehidupan kedua), namanya Roshan. Mendapatkan Aegis dapat mengubah permainan secara fundamental atau memberi keuntungan yang lebih besar kepada sisi yang lebih kuat, masing-masing, para pemain mencoba mengingat / mencatat waktu kematiannya untuk merencanakan kapan harus berkumpul dan menyerangnya, atau berada di dekatnya untuk perlindungannya. Sepuluh pemain diberitahu tentang kematian Roshan, terlepas dari apakah ia disembunyikan dalam kabut perang. Waktu respawn adalah wajib delapan menit, setelah itu Roshan dapat muncul secara acak dalam interval tiga menit.

Tugasnya adalah sebagai berikut : untuk memberi pemain informasi tentang keadaan Roshan saat ini (hidup-hidup, ressurect_base-menghidupkan kembali waktu dasar, ressurect_extra-menghidupkan kembali waktu tambahan).


Gambar 1 - Kondisi untuk transisi antara negara dan tindakan selama transisi

Untuk kondisi di mana Roshan mati, perlihatkan waktu akhir masa tinggal di negara ini. Transisi dari keadaan hidup ke ressurect_base harus dilakukan oleh pemain dalam mode manual dengan tombol. Dalam hal deteksi / kematian Roshan dalam keadaan ressurect_extra (misalnya, tim musuh diam-diam menyelinap ke sarang dan membunuhnya), transisi ke keadaan hidup / ressurect_base juga dilakukan secara manual dengan tombol. Status Roshan (dan akhir zaman berada dalam keadaan kebangkitan) harus ditunjukkan dalam bentuk teks, input yang diperlukan (membunuh dan mengganggu keadaan ressurect_extra) harus disediakan dengan tombol.


Gambar 2 - Elemen antarmuka - label, tombol dan kanvas

Ini adalah satu-satunya tugas yang bisa saya buat sehingga pekerjaan dengan memori permainan tidak diperlukan dan setidaknya ada nilai untuk pemain - bahkan untuk mendapatkan karakteristik dasar, seperti kesehatan, mana, dan posisi entitas, Anda harus menemukannya terlebih dahulu membantu Cheat Engine dalam memori gim, yang perlu dijelaskan tambahan untuk waktu yang agak lama, atau dengan bantuan Source2Gen, yang akan dibahas dalam artikel selanjutnya. Pernyataan masalah memaksa pemain untuk mengikuti Roshan, mengalihkan banyak tindakan kepadanya, yang agak merepotkan - tetapi akan ada sesuatu yang bisa diandalkan di bagian kedua.

Kami akan menulis injected.dll kami, yang akan berisi logika bisnis berbasis MVC dan mengimplementasikannya dalam proses Dota 2. Dll akan menggunakan perpustakaan silk_way.lib kami, yang akan berisi logika perangkap untuk mengubah aliran eksekusi, logger, pemindai memori, dan struktur data .

6. Injector


Buat proyek C ++ kosong, panggil NativeInjector. Kode utama ada di fungsi Suntikan.

void Inject(string & dllPath, string & processName) { DWORD processId = GetProcessIdentificator(processName); if (processId == NULL) throw invalid_argument("Process dont existed"); HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, processId); HMODULE hModule = GetModuleHandle("kernel32.dll"); FARPROC address = GetProcAddress(hModule, "LoadLibraryA"); int payloadSize = sizeof(char) * dllPath.length() + 1; LPVOID allocAddress = VirtualAllocEx( hProcess, NULL, payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); SIZE_T written; bool writeResult = WriteProcessMemory(hProcess, allocAddress, dllPath.c_str(), payloadSize, & written); DWORD treadId; CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) address, allocAddress, 0, & treadId); CloseHandle(hProcess); } 

Fungsi mendapatkan jalur dan nama proses, mencari Id-nya dengan nama proses menggunakan GetProcessIdentificator.

fungsi GetProcessIdentificator
 DWORD GetProcessIdentificator(string & processName) { PROCESSENTRY32 processEntry; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); DWORD processId = NULL; if (Process32First(snapshot, & processEntry)) { while (Process32Next(snapshot, & processEntry)) { if (!_stricmp(processEntry.szExeFile, processName.c_str())) { processId = processEntry.th32ProcessID; break; } } } CloseHandle(snapshot); return processId; } 


Singkatnya, GetProcessIdentificator berjalan melalui semua proses yang berjalan dan mencari proses dengan nama yang sesuai.


Gambar 3 - keadaan awal proses

Selanjutnya, implementasi langsung perpustakaan dengan membuat aliran jarak jauh.

Penjelasan terperinci dari fungsi Suntikan
Berdasarkan Id yang ditemukan, proses dibuka menggunakan fungsi OpenProcess dengan hak untuk membuat utas, menerima informasi proses, menulis dan membaca kemampuan. Fungsi GetModuleHandle mengambil modul library kernel32, ini dilakukan untuk mendapatkan alamat fungsi LoadLibraryA yang terkandung di dalamnya oleh fungsi GetProcAddress. Tujuan LoadLibrary adalah untuk memuat injected.dll kami ke dalam proses yang ditentukan. Artinya, kita perlu memanggil LoadLibrary dari proses yang menarik bagi kita ("Dota2.exe"), untuk ini kita membuat thread baru dari jarak jauh menggunakan CreateRemoteThread. Sebagai penunjuk ke fungsi dari mana utas baru dimulai, kami meneruskan alamat fungsi LoadLibraryA. Jika Anda melihat tanda tangan dari fungsi LoadLibraryA, maka itu memerlukan jalur ke pustaka yang dimuat sebagai argumen - HMODULE LoadLibraryA (LPCSTR lpLibFileName). Kami memberikan argumen ini sebagai berikut: CreateRemoteThread dalam parameter setelah alamat fungsi mulai mengambil pointer ke parameternya, kami membentuk pointer ke lpLibFileName dengan menulis nilai ke memori proses menggunakan fungsi WriteProcessMemory (setelah mengalokasikan memori menggunakan VirtualAllocEx).


Gambar 4 - Membuat aliran jarak jauh

Pastikan untuk menutup penangan proses di akhir dengan fungsi CloseHandle, Anda juga dapat membebaskan memori yang dialokasikan. Injektor kami sudah siap dan menunggu kami untuk menulis logika bisnis di injected.dll dengan perpustakaan silk_way.lib.


Gambar 5 - Menyelesaikan implementasi perpustakaan

Untuk pemahaman yang lebih baik tentang prinsip ini, Anda dapat menonton video . Sebagai kesimpulan, saya akan mengatakan bahwa ini adalah pendekatan yang lebih aman dengan implementasi kode secara langsung di utas utama proses.

7. Silk Way


Mari kita mulai mengimplementasikan silk_way.lib, perpustakaan statis yang berisi struktur data, logger, pemindai memori, dan perangkap. Bahkan, saya mengambil sebagian kecil dari pekerjaan saya, sesuatu yang dapat dijelaskan paling sederhana, yang tidak terlalu terikat dengan yang lain, tetapi pada saat yang sama menyelesaikan masalah.

7.1. Struktur data.


Secara singkat tentang struktur data: Vektor - daftar klasik, waktu penyisipan dan penghapusan O (N), pencarian O (N), memori O (N); Antrian - antrian bundar, waktu penyisipan dan penghapusan O (1), tidak ada pencarian, memori O (N); RBTree - pohon merah-hitam, waktu penyisipan dan penghapusan O (logN), pencarian O (logN), memori O (N). Saya lebih suka hash yang digunakan untuk mengimplementasikan kamus dalam C # dan Python, pohon merah-hitam yang menggunakan perpustakaan standar C ++. Alasannya adalah bahwa hash lebih sulit untuk diimplementasikan lebih benar daripada pohon (kira-kira setiap setengah tahun saya menemukan dan mencoba varietas hash), dan biasanya hash mengambil lebih banyak memori (meskipun bekerja lebih cepat). Struktur ini digunakan untuk membuat koleksi baik dalam logika bisnis maupun perangkap.

Saya mencoba untuk tidak menggunakan struktur dari pustaka standar dan mengimplementasikannya sendiri, khususnya itu tidak masalah dalam kasus kami, tetapi penting jika dll Anda di-debug atau perakitannya dalam kondisi yang jelas (ini lebih mungkin untuk menipu komersial, yang kami kutuk ) Saya menyarankan Anda untuk menulis semua struktur sendiri, ini memberi Anda lebih banyak peluang.
Sebagai contoh, jika Anda membuat game dan tidak ingin “anak sekolah” memindainya menggunakan Cheat Engine, Anda dapat membuat pembungkus untuk tipe primitif dan menyimpan nilai terenkripsi dalam memori. Sebenarnya, ini bukan penyelamatan, tetapi bisa menyingkirkan sebagian dari mereka yang mencoba membaca dan mengubah memori permainan.

7.2. Logger


Output yang diimplementasikan ke konsol dan menulis ke file. Antarmuka:

 class ILogger { protected: ILogger(const char * _path) { path = path; } public: virtual ~ILogger() {} virtual void Log(const char * format, ...) = 0; protected: const char * path; }; 

Implementasi untuk output ke file:

 class MemoryLogger: public ILogger { public: MemoryLogger(const char * _path): ILogger(_path) { fopen_s( & fptr, _path, "w+"); } ~MemoryLogger() { fclose(fptr); } void Log(const char * format, ...) { char log[MAX_LOG_SIZE]; log[MAX_LOG_SIZE - 1] = 0; va_list args; va_start(args, format); vsprintf_s(log, MAX_LOG_SIZE, format, args); va_end(args); fprintf(fptr, log); } protected: FILE * fptr; }; 

Implementasi untuk output ke konsol adalah sama. Jika kita ingin menggunakan pencatatan, kita perlu mendefinisikan antarmuka ILogger *, mendeklarasikan pencatat yang diperlukan, memanggil fungsi Log dengan format yang diperlukan, misalnya:

 ILogger* logger = new MemoryLogger(filename); logger->Log("(%llu)%s: %d\n", GetCurrentThreadId(), "EnumerateThread result", result); 

7.3. Pemindai


Pemindai terlibat dalam fakta bahwa ia menampilkan nilai memori yang ditunjuk oleh pointer yang ditransfer dan membandingkannya dengan sampel dalam memori. Perbandingan fungsional dengan pola akan dipertimbangkan nanti.

Antarmuka:

 class IScanner { protected: IScanner() {} public: virtual ~IScanner() {} virtual void PrintMemory(const char * title, unsigned char * memPointer, int size) = 0; }; 

Implementasi file header:

 class FileScanner : public IScanner { public: FileScanner(const char* _path) : IScanner() { fopen_s(&fptr, _path, "w+"); } ~FileScanner() { fclose(fptr); } void PrintMemory(const char* title, unsigned char* memPointer, int size); protected: FILE* fptr; }; 

Implementasi file sumber:

 void FileScanner::PrintMemory(const char* title, unsigned char* memPointer, int size) { fprintf(fptr, "%s:\n", title); for (int i = 0; i < size; i++) fprintf(fptr, "%x ", (int)(*(memPointer + i))); fprintf(fptr, "\n", title); } 

Untuk menggunakannya, Anda perlu mendefinisikan antarmuka IScanner *, mendeklarasikan pemindai yang diinginkan dan memanggil fungsi PrintMemory, di mana Anda dapat mengatur judul, penunjuk dan panjangnya, misalnya:

 IScanner* scan = new ConsoleScanner(); scan->PrintMemory("source orig", (unsigned char*)source, 30); 

7.4. Perangkap


Bagian paling menarik dari perpustakaan silk_way.lib. Hook digunakan untuk mengubah alur eksekusi program. Buat proyek yang dapat dieksekusi yang disebut Sandbox.

Kelas Device akan menjadi boneka kami untuk menyelidiki operasi jebakan.
 class Unknown { protected: Unknown() {} public: ~Unknown() {} virtual HRESULT QueryInterface() = 0; virtual ULONG AddRef(void) = 0; virtual ULONG Release(void) = 0; }; class Device : public Unknown { public: Device() : Unknown() {} ~Device() {} virtual HRESULT QueryInterface() { return 0; } virtual ULONG AddRef(void) { return 0; } virtual ULONG Release(void) { return 0; } virtual int Present() { cout << "Present()" << " " << i << endl; return i; } virtual void EndScene(int j) { cout << "EndScene()" << " " << i << " " << j << endl; } void Dispose() { cout << "Dispose()" << " " << i << endl; } public: int i; }; 


Kelas Perangkat diwarisi dari antarmuka IUnknown, tugas kami adalah untuk mencegat panggilan fungsi Present dan EndScene dari setiap instance Perangkat, dan memanggil fungsi asli di penerima. Kami tidak tahu tempat dalam kode di mana dan mengapa fungsi-fungsi ini dipanggil, di mana utas.

Melihat fungsi Present dan EndScene, kita melihat bahwa mereka adalah virtual. Fungsi virtual diperlukan untuk mengesampingkan perilaku kelas induk. Fungsi virtual, serta yang non-virtual, adalah penunjuk ke memori tempat opcodes dan nilai argumen ditulis. Karena fungsi virtual berbeda antara ahli waris dan orang tua, mereka memiliki pointer yang berbeda (ini adalah fungsi yang sama sekali berbeda) dan disimpan dalam Tabel Metode Virtual (VMT). Tabel ini disimpan dalam memori dan merupakan penunjuk ke penunjuk kelas, kami menemukannya untuk Perangkat:

 Device* device = new Device(); unsigned long long vmt = **(unsigned long long**)&device; 

VMT menyimpan pointer ke fungsi virtual, jika kita ingin mewarisi dari Device, ahli waris akan mengandung VMT-nya. VMT menyimpan pointer fungsi secara berurutan dengan langkah yang sama dengan ukuran pointer (untuk x86 itu adalah 4 byte, untuk x64 adalah 8), sesuai dengan urutan di mana fungsi tersebut didefinisikan di kelas. Temukan pointer ke fungsi Present dan EndScene, yang terletak di tempat ketiga dan keempat:

 typedef int (*pPresent)(Device*); typedef void (*pEndScene)(Device*, int j); pPresent ptrPresent = nullptr; pEndScene ptrEndScene = nullptr; int main() { //declare Device and find pointer vmt ptrPresent = (pPresent)(*(unsigned long long*)(vmt + 8 * 3)); ptrEndScene = (pEndScene)(*(unsigned long long*)(vmt + 8 * 4)); } 

Penting juga bahwa pointer ke metode kelas harus berisi argumen pertama sebagai referensi ke instance kelas. Dalam C ++, C #, ini disembunyikan dari kami, dan kompiler tahu tentang hal itu - dalam diri Python secara eksplisit ditunjukkan oleh parameter pertama dalam metode kelas. Lebih lanjut tentang konvensi pemanggilan di sini , Anda perlu mencari panggilan ini.

Pertimbangkan instruksi e9 ff 3a fd ff - di sini e9 adalah opcode (dengan JMP mnemonik) yang memberitahu prosesor untuk mengubah pointer ke instruksi (EIP untuk x86, RIP untuk x64), lompat dari alamat saat ini ke FFFD3AFF (4294785791). Perlu juga dicatat bahwa dalam nomor memori disimpan "sebaliknya." Fungsinya memiliki prolog dan epilog dan disimpan di bagian .code. Mari kita lihat apa yang disimpan dengan pointer ke fungsi Present menggunakan pemindai:

 IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30); 

Di konsol kita melihat:

 Present: 48 89 4c 24 8 48 83 ec 28 48 8d 15 40 4a 0 0 48 8b d 71 47 0 0 e8 64 10 0 0 48 8d 

Untuk memahami set kode ini, Anda dapat melihat tabel , atau menggunakan disassembler yang tersedia. Kami akan mengambil disassembler yang sudah jadi - hde (mesin hacker disassembler). Anda juga dapat melihat distorm dan batu penjuru untuk perbandingan. Lewati sebuah penunjuk ke suatu fungsi ke disassembler mana saja dan ia akan mengatakan opcode apa yang digunakannya, nilai-nilai argumen, dan sebagainya.

7.4.1 Kait Kode Sandi


Sekarang kita siap untuk langsung menuju perangkap. Kita akan melihat pada Opcode Hook dan Hardware Breakpoint. Jebakan paling umum yang saya sarankan untuk diterapkan dan dijelajahi.

Mungkin jebakan yang paling umum digunakan dan sederhana adalah Opcode Hook (dalam artikel daftar jebakan, ini disebut Byte patching) - perhatikan bahwa itu mudah dikenali oleh anti-cheat ketika disalahgunakan (tanpa memahami bagaimana cara anti-cheat bekerja, tanpa mengetahui area dan bagian memori yang dipindai) saat ini dan hal-hal lain larangan tidak akan memperlambat untuk menunggu). Ketika digunakan dengan terampil, ini adalah jebakan yang hebat, cepat dan mudah dimengerti.
Jika saat membaca artikel Anda sedang bermain kode secara bersamaan dan berada dalam mode Debug, beralihlah ke Rilis - ini penting.

Jadi, izinkan saya mengingatkan Anda, kami perlu mencegat pelaksanaan fungsi Present dan EndScene.
Kami menerapkan pencegat - fungsi di mana kami ingin mentransfer kontrol:

 int PresentHook(Device* device) { cout << "PresentHook" << endl; return 1; } void EndSceneHook(Device* device, int j) { cout << "EndSceneHook" << " " << j << endl; } 

Mari kita pikirkan tentang abstraksi yang kita butuhkan. Kami membutuhkan antarmuka yang memungkinkan kami untuk mengatur jebakan, menghapusnya, dan memberikan informasi tentangnya. Informasi tentang jebakan harus berisi penunjuk ke fungsi yang dicegat, fungsi penerima dan loncatan (fakta bahwa kami mencegat fungsi tidak berarti bahwa itu tidak lagi diperlukan, kami juga ingin dapat menggunakannya - loncatan akan membantu untuk memanggil fungsi yang dicegat asli).

 #pragma pack(push, 1) struct HookRecord { HookRecord() { reservationLen = 0; sourceReservation = new void*[RESERV_SIZE](); } ~HookRecord() { reservationLen = 0; delete[] sourceReservation; } void* source; void* destination; void* pTrampoline; int reservationLen; void* sourceReservation; }; #pragma pack(pop) class IHook { protected: IHook() {} public: virtual ~IHook() {} virtual void SetExceptionHandler( PVECTORED_EXCEPTION_HANDLER pVecExcHandler) = 0; virtual int SetHook(void* source, void* destination) = 0; virtual int UnsetHook(void* source) = 0; virtual silk_data::Vector<HookRecord*>* GetInfo() = 0; virtual HookRecord* GetRecordBySource(void* source) = 0; }; 

Antarmuka IHook memberi kita kemampuan seperti itu. Kami ingin bahwa ketika instance dari kelas Perangkat memanggil fungsi Present dan EndScene (yaitu, pointer RIP pergi ke alamat ini), fungsi PresentHook dan EndSceneHook kami dijalankan sesuai dengan itu.

Bayangkan secara visual bagaimana fungsi yang dicegat, penerima dan loncatan berada di memori (bagian .code) pada saat ketika kontrol memasuki fungsi yang dicegat:


Gambar 6 - Keadaan awal memori, eksekusi masuk ke fungsi dicegat

Sekarang kami ingin RIP (panah merah) untuk beralih dari sumber ke awal tujuan. Bagaimana cara melakukannya? Seperti yang telah dinyatakan di atas, memori sumber berisi opcode yang akan dijalankan prosesor saat eksekusi mencapai sumber. Intinya, kita perlu melompat dari satu bagian ke bagian lain, mengarahkan ulang pointer RIP. Seperti yang mungkin sudah Anda duga, ada opcode yang memungkinkan Anda untuk mentransfer kontrol dari alamat saat ini ke yang diinginkan, mnemonik JMP ini disebut.

Anda dapat melompat langsung ke alamat yang diinginkan, atau relatif ke alamat saat ini, lompatan ini dapat ditemukan di masing-masing plat - ff dan e9. Buat struktur untuk instruksi ini:

 #pragma pack(push, 1) // 32-bit relative jump. typedef struct { unsigned char opcode; unsigned int delta; } JMP_REL; // 64-bit absolute jump. typedef struct { unsigned char opcode1; unsigned char opcode2; unsigned int dummy; unsigned long long address; } JMP_ABS; #pragma pack(pop) 

Instruksi lompatan relatif lebih pendek, tetapi ada batasan - unsigned int mengatakan bahwa Anda dapat melompat dalam 4.294.967.295, yang tidak cukup untuk x64.
Dengan demikian, alamat fungsi tujuan penerima tujuan dapat dengan mudah melebihi nilai ini dan berada di luar int yang tidak ditandatangani, yang sangat mungkin untuk proses x64 (untuk x86 semuanya jauh lebih sederhana dan Anda dapat membatasi diri pada lompatan yang relatif relatif ini untuk menerapkan Kait Kode Sandi). Lompatan langsung membutuhkan 14 byte, sebagai perbandingan, lompatan relatif hanya 5 (kami mengemas struktur, perhatikan paket #pragma (push, 1)).

Kita perlu menulis ulang nilai pada sumber ke salah satu dari instruksi hop ini.
Sebelum Anda menangkap suatu fungsi, Anda harus mempelajarinya - cara termudah untuk melakukannya adalah dengan debugger (saya akan menunjukkan kepada Anda bagaimana melakukannya dengan x64dbg nanti), atau dengan disassembler. Untuk Present, kami sudah menghasilkan 30 byte dari awalnya, instruksi 48 89 4c 24 8 menempati 5 byte.
Mari kita menerapkan lompatan relatif. Saya lebih suka opsi ini karena panjangnya instruksi. Idenya adalah ini: kita mengganti 5 byte pertama dari fungsi asli, mempertahankan byte yang diubah, menggantinya dengan lompatan relatif ke alamat instruksi, yang terletak di dalam int unsigned.


Gambar 7 - Sumber 5 byte dari fungsi sumber digantikan oleh lompatan relatif

Apa yang memberi kita lompatan ke memori yang dialokasikan (wilayah ungu), bagaimana kita membawa diri kita lebih dekat untuk mentransfer kontrol ke tujuan dengan tindakan ini? Dalam memori yang dialokasikan oleh kami, ada lompatan langsung, yang akan memindahkan RIP ke tujuan.


Gambar 8 - Mengalihkan RIP ke fungsi penerima

Masih mencari cara memanggil fungsi yang tertangkap. Kita perlu menjalankan instruksi yang macet dan memulai eksekusi dari bagian sumber yang tidak tersentuh. Kami melanjutkan sebagai berikut - simpan instruksi yang rusak ke awal trampolin, ingat berapa banyak byte yang rusak dan lompat langsung ke sumber + corruptLen, ke instruksi "sehat".

Eksekusi instruksi yang disimpan terhapus oleh lompatan relatif:


Gambar 9 - Menggunakan springboard untuk memanggil fungsi yang dicegat

Eksekusi instruksi lebih lanjut yang tidak memengaruhi mashing:


Gambar 10 - Kelanjutan dari pelaksanaan instruksi dari fungsi yang dicegat

Kode menerapkan ide yang dijelaskan di atas
 int OpcodeHook::SetHook(void* source, void* destination) { auto record = new HookRecord(); record->source = source; record->destination = destination; info->PushBack(record); JMP_ABS pattern = {0xFF, 0x25, 0x00000000, // JMP[RIP + 6] empty 0x0000000000000000 }; // absolute address pattern.address = (ULONG_PTR)source; int currentLen = 0; int redLine = sizeof(JMP_REL); while (currentLen < redLine) { hde64s context; const void* pSource = (void*)((unsigned char*)source + currentLen); hde64_disasm(pSource, &context); memcpy((unsigned char*)record->sourceReservation + currentLen, pSource, context.len); record->reservationLen += context.len; currentLen += context.len; } int trampolineMemorySize = 2 * sizeof(JMP_ABS) + record->reservationLen; record->pTrampoline = AllocateMemory(source, trampolineMemorySize); pattern.address = (unsigned long long)(unsigned char*)source + record->reservationLen; memcpy((unsigned char*)record->pTrampoline, record->sourceReservation, record->reservationLen); int offset = record->reservationLen; memcpy((unsigned char*)record->pTrampoline + offset, &pattern, sizeof(JMP_ABS)); pattern.address = (ULONG_PTR)destination; ULONG_PTR relay = (ULONG_PTR)record->pTrampoline + sizeof(pattern) + record->reservationLen; memcpy((void*)relay, &pattern, sizeof(pattern)); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); JMP_REL* pJmpRelPattern = (JMP_REL*)source; pJmpRelPattern->opcode = 0xE9; pJmpRelPattern->delta = (unsigned int)((LPBYTE)relay - ((LPBYTE)source + sizeof(JMP_REL))); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); return SUCCESS_CODE; } 


Penjelasan fungsi SetHook
Catatan dibuat yang menyimpan informasi tentang perangkap, setelah catatan ditambahkan ke koleksi. Instruksi dirayapi dari awal alamat sumber sampai instruksi lompatan relatif dapat sepenuhnya dimasukkan (5 byte), instruksi yang macet disalin ke reservasi, dan panjangnya diingat.
Poin yang sangat penting adalah bahwa kita perlu mengalokasikan memori untuk loncatan dan relay, di mana kita akan menyimpan instruksi untuk mengarahkan aliran dari sumber ke tujuan dan alamat untuk memori ini harus berada dalam batas-batas yang dapat dilompati oleh lompatan relatif (tidak ditandai) int).

Fungsionalitas ini mengimplementasikan fungsi AllocateMemory.

 void* OpcodeHook::AllocateMemory(void* origin, int size) { const unsigned int MEMORY_RANGE = 0x40000000; SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); ULONG_PTR minAddr = (ULONG_PTR)sysInfo.lpMinimumApplicationAddress; ULONG_PTR maxAddr = (ULONG_PTR)sysInfo.lpMaximumApplicationAddress; ULONG_PTR castedOrigin = (ULONG_PTR)origin; ULONG_PTR minDesired = castedOrigin - MEMORY_RANGE; if (minDesired > minAddr && minDesired < castedOrigin) minAddr = minDesired; int test = sizeof(ULONG_PTR); ULONG_PTR maxDesired = castedOrigin + MEMORY_RANGE - size; if (maxDesired < maxAddr && maxDesired > castedOrigin) maxAddr = maxDesired; DWORD granularity = sysInfo.dwAllocationGranularity; ULONG_PTR freeMemory = 0; ULONG_PTR ptr = castedOrigin; while (ptr >= minAddr) { ptr = FindPrev(ptr, minAddr, granularity, size); if (ptr == 0) break; LPVOID pAlloc = VirtualAlloc((LPVOID)ptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pAlloc != 0) return pAlloc; } while (ptr < maxAddr) { ptr = FindNext(ptr, maxAddr, granularity, size); if (ptr == 0) break; LPVOID pAlloc = VirtualAlloc((LPVOID)ptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pAlloc != 0) return pAlloc; } return NULL; } 

Idenya sederhana - kita akan beralih dari memori, mulai dari alamat tertentu (dalam kasus kami, pointer ke sumber) ke atas dan ke bawah hingga kami menemukan bagian yang sesuai dengan ukuran bebas.

Kembali ke fungsi SetHook. Salin byte yang aus dari sumber ke memori yang dialokasikan dan segera masukkan lompatan langsung ke sumber + rusak untuk melanjutkan eksekusi dengan instruksi yang tidak rusak.

Selanjutnya adalah pemasangan pointer relai, yang bertanggung jawab untuk mengarahkan utas eksekusi ke tujuan dengan langsung melompat ke alamat penerima. Pada akhirnya, kami mengubah sumbernya - kami menetapkan hak akses tulis ke tempat memori di mana fungsi tersebut berada dan mengganti 5 byte pertama dengan lompatan relatif yang mengarah ke alamat relai.

Kami memasang jebakan, tetapi itu juga harus bisa dibersihkan. Melanggar - bukan membangun, idenya sederhana - kami akan mengembalikan byte yang lusuh dari sumber, menghapus catatan tentang jebakan dari koleksi, dan membebaskan memori yang dialokasikan:

 int OpcodeHook::UnsetHook(void* source) { auto record = GetRecordBySource(source); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(source, record->sourceReservation, record->reservationLen); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); info->Erase(record); FreeMemory(record); return SUCCESS_CODE; } 

Menguji kerja. Segera ganti penerima kami sehingga mereka dapat memanggil fungsi yang dicegat menggunakan loncatan:

 int PresentHook(Device* device) { auto record = hook->GetRecordBySource(ptrPresent); pPresent pTrampoline = (pPresent)record->pTrampoline; auto result = pTrampoline(device); cout << "PresentHook" << endl; return result; } void EndSceneHook(Device* device, int j) { auto record = hook->GetRecordBySource(ptrEndScene); pEndScene pTrampoline = (pEndScene)record->pTrampoline; pTrampoline(device, 2); cout << "EndSceneHook" << " " << j << endl; } 

Kami menguji apakah kami melakukan semuanya dengan benar, apakah memori mengalir, apakah semuanya dijalankan dengan benar.
 int main() { while (true) { Device* device = new Device(); device->i = 3; unsigned long long vmt = **(unsigned long long**)&device; ptrPresent = (pPresent)(*(unsigned long long*)(vmt + 8 * 3)); ptrEndScene = (pEndScene)(*(unsigned long long*)(vmt + 8 * 4)); IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30); hook = new OpcodeHook(); hook->SetHook(ptrPresent, &PresentHook); hook->SetHook(ptrEndScene, &EndSceneHook); device->Present(); device->EndScene(7); device->Present(); device->EndScene(7); device->i = 5; ptrPresent(device); ptrEndScene(device, 9); hook->UnsetHook(ptrPresent); hook->UnsetHook(ptrEndScene); ptrPresent(device); ptrEndScene(device, 7); delete hook; delete device; } } 


Itu bekerja.Anda juga dapat memeriksa di x64dgb.

Ingat, pada awalnya saya meminta Anda untuk bekerja di rilis rilis? Sekarang pergi ke Debug dan jalankan program. Program macet ... Perangkap menyala, tetapi upaya untuk memanggil loncatan menimbulkan pengecualian, yang mengatakan bahwa alamat di mana kita memanggil loncatan sama sekali tidak untuk dieksekusi. Apa yang kami lewatkan? Apa masalah build Debug? Kami mulai dan melihat opcode dari fungsi Present:

 Present: e9 f4 36 0 0 e9 df 8d 0 0 e9 aa b0 0 0 e9 75 3e 0 0 e9 80 38 0 0 e9 da 81 0 0 

Saat berjalan di x64dbg, Anda dapat melihat yang berikut ini. Gambar 11 - Instruksi pembuatan Debug Di Debug, opcode telah berubah, sekarang kompiler menambahkan lompatan relatif e9 f4 36 0. Semua fungsi dibungkus dalam lompatan, termasuk main dan titik masuk ke mainCRTStartup. Opcode lain, well, ok, itu harus disalin ke batu loncatan, ketika batu loncatan dipanggil, lompatan relatif ini harus dipanggil, lalu lompatan langsung ke bagian sumber yang tidak rusak. Di sini menjadi jelas bahwa semuanya dilakukan seperti yang telah kami terapkan, hanya lompatan relatif ke itu dan relatif, bahwa pelaksanaannya dari alamat yang berbeda, sumber dan trampolin, mengekspos RIP ke nilai yang sama sekali berbeda.








Dalam pengalaman saya yang sederhana, penerapan lompatan relatif mencakup 99% penggunaan. Ada beberapa opcode lagi yang harus ditangani secara terpisah. Ingatlah bahwa sebelum memasang jebakan pada suatu fungsi, Anda tidak boleh terlalu malas dan mempelajarinya. Saya tidak akan mengganggu Anda dan menambahkan fungsionalitas ke versi 100 persen (sekali lagi, dalam pengalaman saya yang sederhana), jika Anda membutuhkannya atau tertarik, Anda dapat melihat bagaimana perpustakaan tersebut diatur dan secara khusus apa kasus-kasus lain yang mereka periksa - akan mudah dilakukan jika Anda tahu tentang apa ini.

Lompatan relatif memang cukup umum, jadi saya mengusulkan untuk menerapkannya. Lompatan relatif terdiri dari opcode e9 dan nilai yang harus Anda lompat ke relatif ke alamat saat ini. Dengan demikian, Anda bisa mencari tahu di mana harus melompat, dan melompat ke sana dari batu loncatan dengan lompatan langsung. Bahkan jika kita bertemu lompatan relatif baru di sana, itu sudah dari alamat yang benar.

Implementasi pemasangan jebakan dengan memperhitungkan lompatan relatif
 int OpcodeHook::SetHook(void* source, void* destination) { auto record = new HookRecord(); record->source = source; record->destination = destination; info->PushBack(record); JMP_ABS pattern = {0xFF, 0x25, 0x00000000, // JMP[RIP + 6] empty 0x0000000000000000 }; // address pattern.address = (ULONG_PTR)source; int currentLen = 0; bool isJmpOpcode = false; int redLine = sizeof(JMP_REL); while (currentLen < redLine && !isJmpOpcode) { hde64s context; const void* pSource = (void*)((unsigned char*)source + currentLen); hde64_disasm(pSource, &context); if (context.opcode == 0xE9) { ULONG_PTR ripPtr = (ULONG_PTR)pSource + context.len + (INT32)context.imm.imm32; pattern.address = ripPtr; isJmpOpcode = true; } memcpy((unsigned char*)record->sourceReservation + currentLen, pSource, context.len); record->reservationLen += context.len; currentLen += context.len; } int trampolineMemorySize = isJmpOpcode ? 2 * sizeof(JMP_ABS) : 2 * sizeof(JMP_ABS) + record->reservationLen; record->pTrampoline = AllocateMemory(source, trampolineMemorySize); if (!isJmpOpcode) { pattern.address = (unsigned long long)(unsigned char*)source + record->reservationLen; memcpy((unsigned char*)record->pTrampoline, record->sourceReservation, record->reservationLen); } int offset = isJmpOpcode ? 0 : record->reservationLen; memcpy((unsigned char*)record->pTrampoline + offset, &pattern, sizeof(JMP_ABS)); pattern.address = (ULONG_PTR)destination; ULONG_PTR relay = (ULONG_PTR)record->pTrampoline + sizeof(pattern) + record->reservationLen; memcpy((void*)relay, &pattern, sizeof(pattern)); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); JMP_REL* pJmpRelPattern = (JMP_REL*)source; pJmpRelPattern->opcode = 0xE9; pJmpRelPattern->delta = (unsigned int)((LPBYTE)relay - ((LPBYTE)source + sizeof(JMP_REL))); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); return SUCCESS_CODE; } 


Jika disassembler mengembalikan informasi bahwa opcode dari perintah ini adalah e9, kami menghitung alamat untuk melompat ke (ULONG_PTR ripPtr = (ULONG_PTR) pSource + context.len + (INT32) context.imm.imm32), dan menuliskan alamat tersebut ke dalam papan loncatan sebagai nilai argumen lompat langsung.

Saya juga mencatat bahwa dalam lingkungan multi-utas, suatu situasi mungkin muncul ketika, pada saat memasang / melepas kait, salah satu utas dapat mulai menjalankan fungsi yang kami tangkap - sebagai hasilnya, prosesnya akan jatuh. Bagian dari cara menangani ini akan dijelaskan dalam Hardware Breakpoint.

Jika Anda memerlukan alat yang terbukti, Anda ingin memastikan bahwa jebakan Anda akan berfungsi, Anda tidak memiliki ide sendiri dan Anda tidak ingin mempelajari prolog fungsi - gunakan solusi yang sudah jadi, misalnya, Microsoft menawarkan Detour library sendiri. Saya tidak menggunakan perpustakaan seperti itu dan menggunakan solusi buatan sendiri karena sejumlah alasan, oleh karena itu saya tidak bisa memberi saran sesuatu, saya hanya bisa memberi nama perpustakaan yang saya pelajari untuk menemukan sesuatu yang baru dan menggunakannya: PolyHook , MinHook , EasyHook (terutama jika Anda membutuhkan kait di C #).

7.4.2. Breakpoint perangkat keras


Opcode Hook adalah perangkap sederhana dan cepat, tetapi bukan yang paling efisien. Anti-cheat dapat dengan mudah melacak perubahan pada memori, tetapi Opcode Hook dapat digunakan sehubungan dengan anti-cheat itu sendiri atau mencegat panggilan sistem (misalnya, NtSetInformationThread) yang digunakannya. Hardware Breakpoint adalah jebakan yang tidak mengubah memori proses. Saya melihat utas di forum menanyakan apakah VAC mengikuti jebakan ini - jawabannya biasanya beragam. Secara pribadi, VAC tidak melarang saya untuk menggunakannya dan tidak mengatur ulang register (itu kurang dari enam bulan yang lalu, mungkin ada sesuatu yang berubah).
, , VAC DR /, - , . HWBP , - , , , DR0-DR7 .
HWBP menggunakan register prosesor khusus untuk mengganggu eksekusi thread. Jika konteks aliran berisi register DR0-DR7 diatur dengan cara tertentu dan RIP pergi ke salah satu dari empat alamat yang disimpan dalam DR0-DR3, pengecualian dilemparkan yang dapat ditangkap, dengan jenis pengecualian dan keadaan konteks, tentukan di mana alamat kontrol melempar pengecualian dan menyimpulkan - jebakan atau tidak. Keterbatasan yang signifikan dari pendekatan ini adalah bahwa Anda hanya dapat menggunakan empat fungsi pada satu waktu dan mengaturnya secara terpisah untuk setiap utas, yang menyebabkan ketidaknyamanan jika jebakan diatur dan yang baru dibuat / utas lama diciptakan kembali, yang menyebabkan jebakan. Ini bukan hambatan khusus dan diatur oleh intersepsi fungsi BaseThreadInitThunk, pembatasan penggunaan 4 jebakan tidak benar-benar mengganggu saya secara pribadi.Jika jumlah kait sangat penting bagi Anda, lihat pendekatan PageGuard.

Jadi, tugasnya sama - kita berada di kotak pasir (proyek Sandbox), perlu untuk mencegat metode kelas Device Present dan EndScene di mana untuk memanggil metode asli. Kami sudah memiliki antarmuka siap pakai untuk perangkap - IHook, mari kita berurusan dengan pekerjaan breakpoint "besi".

Prinsipnya adalah ini: ada empat register DR0-DR3 "yang berfungsi" yang alamatnya dapat ditulis, tergantung pada pengaturan register kontrol DR7 ketika mencoba untuk menulis, membaca atau mengeksekusi pada alamat yang ditentukan, akan terjadi pengecualian dengan tipe EXCEPTION_SINGLE_STEP yang akan diproses, yang harus diproses dalam penangan yang terdaftar sebelumnya. . Anda dapat menggunakan pengendali SEH dan VEH - kami akan menggunakan yang terakhir, karena memiliki prioritas yang lebih tinggi.

Kami menyadari ide ini:

 int HardwareBPHook::SetHook(void* source, void* destination, HANDLE* hThread, int* reg) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return ERROR_GET_CONTEXT; *(&context.Dr0 + *reg) = (unsigned long long)source; context.Dr7 |= 1ULL << (2 * (*reg)); context.Dr7 |= HW_EXECUTE << ((*reg) * 4 + 16); context.Dr7 |= HW_LENGTH << ((*reg) * 4 + 18); if (!SetThreadContext(*hThread, &context)) return ERROR_SET_CONTEXT; return SUCCESS_CODE; } 

Apa yang terjadi dalam kode
, , DR7. .

Secara lebih terperinci tentang apa itu DR6 dan DR7, serta pendekatan PageGuard, saya dapat menyarankan Gray Hat Python: Pemrograman Python untuk Peretas dan Pembuat Mesin Balik. Singkatnya, DR7 mengaktifkan / menonaktifkan penggunaan register "berfungsi" - bahkan jika salah satu register DR0-DR3 berisi alamat, tetapi pada DR7 bendera register yang sesuai dinonaktifkan, breakpoint tidak akan berfungsi. DR7 juga menetapkan jenis pekerjaan dengan alamat di mana perlu untuk melemparkan pengecualian - apakah alamat itu dibaca, apakah catatan itu dibuat atau alamat digunakan untuk menjalankan instruksi (kami tertarik pada opsi terakhir).

Menghapus jebakan juga cukup sederhana dan dilakukan melalui register kontrol DR7.

 int HardwareBPHook::UnsetHook(void* source, HANDLE* hThread) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return ERROR_GET_CONTEXT; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if ((unsigned long long)source == *(&context.Dr0 + i)) { info->GetItem(i)->source = 0; *(&context.Dr0 + i) = 0; context.Dr7 &= ~(1ULL << (2 * i)); context.Dr7 &= ~(3 << (i * 4 + 16)); context.Dr7 &= ~(3 << (i * 4 + 18)); break; } } if (!SetThreadContext(*hThread, &context)) return ERROR_SET_CONTEXT; return SUCCESS_CODE; } 

Masih berurusan dengan utas - jebakan harus ditetapkan untuk utas yang memanggil fungsi yang dicegat. Kami tidak akan repot tentang ini.

Kami memasang jebakan untuk semua utas proses.
 int HardwareBPHook::SetHook(void* source, void* destination) { THREADENTRY32 te32; HANDLE hThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThread == INVALID_HANDLE_VALUE) return ERROR_ENUM_THREAD_START; te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThread, &te32)) { CloseHandle(hThread); return ERROR_ENUM_THREAD_START; } DWORD dwOwnerPID = GetCurrentProcessId(); bool isRegDefined = false; int freeReg = -1; Freeze(); do { if (te32.th32OwnerProcessID == dwOwnerPID) { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (!isRegDefined) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(openThread, &context)) return ERROR_GET_CONTEXT; freeReg = GetFreeReg(&context.Dr7); if (freeReg == -1) return ERROR_GET_FREE_REG; isRegDefined = true; } SetHook(source, destination, &openThread, &freeReg); CloseHandle(openThread); } } while (Thread32Next(hThread, &te32)); CloseHandle(hThread); Unfreeze(); auto record = info->GetItem(freeReg); record->source = source; record->destination = destination; record->pTrampoline = source; return SUCCESS_CODE; } 


Kode di atas melewati semua proses yang terlihat dan mencari proses saat ini. Dalam proses yang ditemukan untuk utas berikutnya, kami mendapatkan penangan arus, menemukan salah satu dari empat register gratis dan memasang perangkap. Perlu memperhatikan fungsi Freeze dan Unfreeze - inilah yang disebut Opcode Hook tentang multithreading - mereka benar-benar menghentikan eksekusi utas proses ini (kecuali yang sekarang) sehingga tidak ada situasi ketika salah satu utas memasuki fungsi yang dicegat.

Melindungi utas dari memanggil fungsi kait
 int IHook::Freeze() { THREADENTRY32 te32; HANDLE hThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThread == INVALID_HANDLE_VALUE) return ERROR_ENUM_THREAD_START; te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThread, &te32)) { CloseHandle(hThread); return ERROR_ENUM_THREAD_START; } DWORD dwOwnerPID = GetCurrentProcessId(); do { if (te32.th32OwnerProcessID == dwOwnerPID && te32.th32ThreadID != GetCurrentThreadId()) { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (openThread != NULL) { SuspendThread(openThread); CloseHandle(openThread); } } } while (Thread32Next(hThread, &te32)); CloseHandle(hThread); return SUCCESS_CODE; } int IHook::Unfreeze() { // equal { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (openThread != NULL) { ResumeThread(openThread); CloseHandle(openThread); } } // equal return 0; } 


Hal serupa perlu diterapkan dalam fungsi melepas jebakan.

Tetap menambahkan handler pengecualian VEH. Menambah dan menghapus dilakukan oleh fungsi AddVectoredExceptionHandler dan RemoveVectoredExceptionHandler dari aliran apa pun.

 void HardwareBPHook::SetExceptionHandler(PVECTORED_EXCEPTION_HANDLER pVecExcHandler) { pException = AddVectoredExceptionHandler(1, pVecExcHandler); } ~HardwareBPHook() { info->Clear(); delete info; RemoveVectoredExceptionHandler(pException); } 

Pawang harus memeriksa jenis pengecualian (EXCEPTION_SINGLE_STEP diperlukan), memeriksa korespondensi alamat di mana pengecualian terjadi dengan apa yang ada dalam register dan, jika alamat tersebut ditemukan, mengatur ulang pointer RIP ke alamat penerima. Keadaan tumpukan dipertahankan, sehingga saat eksekusi penerima lebih lanjut, semua parameter pada tumpukan akan tetap utuh.

Kami menerapkan handler yang dijelaskan di kotak pasir:

 LONG OnExceptionHandler( EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_EXECUTION; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long)hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Rip = (unsigned long long)hook->GetInfo()->GetItem(i)->destination; break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

Secara teori, semuanya sudah siap, kami menjalankan program, menunggu pekerjaan yang sama persis dengan OpcodeHook.
Ini tidak terjadi, program kami macet - lebih tepatnya, ia terus-menerus masuk ke PresentHook dan pada saat loncatan harus dipanggil, fungsinya dipanggil lagi. Faktanya adalah bahwa breakpoint "besi" belum hilang, karena ketika Anda memanggil batu loncatan (yang, dalam kasus breakpoint "besi", menunjukkan fungsi asli), kami kembali alarm alamat yang sama dan menimbulkan pengecualian. Solusinya adalah sebagai berikut: kami akan menghapus breakpoint ketika ditemukan di handler untuk utas tertentu, dan pada waktu yang tepat kami akan mengaturnya lagi. Tempat pembaruan akan memilih saat ketika fungsi penerima berakhir.

Ini diimplementasikan sebagai berikut - dalam handler, bersama dengan menghapus breakpoint, perintah yang tertunda ditambahkan, artinya memperbarui breakpoint dalam aliran yang ditentukan. Perintah berjalan di akhir fungsi penerima.

 IDeferredCommands* hookCommands; int PresentHook(Device* device) { auto record = hook->GetRecordBySource(ptrPresent); pPresent pTrampoline = (pPresent)record->pTrampoline; auto result = pTrampoline(device); cout << "PresentHook" << endl; hookCommands->Run(); return result; } void EndSceneHook(Device* device, int j) { auto record = hook->GetRecordBySource(ptrEndScene); pEndScene pTrampoline = (pEndScene)record->pTrampoline; pTrampoline(device, 2); cout << "EndSceneHook" << " " << j << endl; hookCommands->Run(); } LONG OnExceptionHandler(EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_EXECUTION; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long)hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Dr7 &= ~(1ULL << (2 * i)); exceptionPointers->ContextRecord->Rip = (unsigned long long)hook->GetInfo()->GetItem(i)->destination; IDeferredCommand* cmd = new SetD7Command(hook, GetCurrentThreadId(), i); hookCommands->Enqueue(cmd); break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

Implementasi Perintah Tertunda
 namespace silk_way { class IDeferredCommand { protected: IDeferredCommand(silk_way::IHook* _hook) { hook = _hook; } public: virtual ~IDeferredCommand() { hook = nullptr; } virtual void Run() = 0; protected: silk_way::IHook* hook; }; class SetD7Command : public IDeferredCommand { public: SetD7Command(silk_way::IHook* _hook, unsigned long long _threadId, int _reg) : IDeferredCommand(_hook) { threadId = _threadId; reg = _reg; } void Run() { HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadId); if (hThread != NULL) { bool res = SetD7(&hThread); CloseHandle(hThread); } } private: bool SetD7(HANDLE* hThread) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return false; *(&context.Dr0 + reg) = (unsigned long long)hook->GetInfo()->GetItem(reg)->source; context.Dr7 |= 1ULL << (2 * reg); if (!SetThreadContext(*hThread, &context)) return false; return true; } private: unsigned long long threadId; int reg; }; class IDeferredCommands : public silk_data::Queue<IDeferredCommand*>, public IDeferredCommand { protected: IDeferredCommands() : Queue(), IDeferredCommand(nullptr) {} public: virtual ~IDeferredCommands() {} }; } 


Bayangkan secara visual karya breakpoint "besi".


Gambar 12 - Keadaan awal

Kami mengatur jebakan, menambahkan handler VEH, menunggu kontrol untuk mencapai fungsi sumber:


Gambar 13 - Tahap persiapan untuk intersepsi

Pengecualian dilemparkan, pawang dipanggil yang mengarahkan ulang RIP ke penerima dan mengatur ulang breakpoint:


Gambar 14 - Mengarahkan ulang thread eksekusi pada fungsi penerima

Pada perangkap topik ini dapat diselesaikan, perpustakaan statis silk_way.lib siap. Dari pengalaman saya sendiri, saya dapat mengatakan bahwa saya sering menggunakan OpcodeHook, VMT Hook, Forced Exception Hook (mungkin perangkap yang paling “wasir”), HardwareBreakpoint dan PageGuard (ketika waktu eksekusi tidak kritis, penyadapan satu kali).

8. Arsitektur logika


Basis logika disajikan dalam bentuk MVC (model-view-controller). Semua entitas inti mewarisi dari antarmuka ISilkObject.

8.1. Model


Saat mengembangkan bot di perpustakaan, saya pertama kali mengimplementasikan ECS (Anda dapat membaca tentang pendekatan ini di sini dan di sini ). Ketika saya menyadari bahwa meluncurkan bot dengan pemain sungguhan adalah tugas yang agak panjang, saya menulis simulasi di mana ml perpustakaan diuji (dengan kisi tiga dimensi untuk navigasi (Dota 2 hanya menggunakan kisi 3D untuk navigasi) dan fisika 2D yang disederhanakan untuk blok tubuh). Ketika kebutuhan untuk simulasi menghilang dan saya menemukan cara dan apa yang harus dicatat, informasi apa yang harus dikumpulkan selama pertempuran, ECS tidak lagi diperlukan dan model hanya mulai mengandung kamus komponen (untuk mewakili sesuatu seperti orang-orang dari SkyForge, bagian “Avatar dan massa), yang berisi pembungkus struktur dari Source2Gen. Untuk artikel ini, saya tidak mentransfer implementasi ini untuk menyederhanakan materi. Model berisi Skema, di mana uraiannya disimpan (titik ini disederhanakan dan dalam implementasi ini model tidak dibuat sesuai dengan skema, skema hanya menggambarkannya (menyimpan nilai yang telah ditentukan yang dapat di-hardcode) - ini dapat dibandingkan dengan menyimpan konten game dalam xml / json )

Secara skematis, perangkat model dapat direpresentasikan sebagai berikut: Gambar 15 - Representasi skematis Implementasi Model dalam kode:






 template <class S> SILK_OBJ(IModel) { ACCESSOR(IIdentity, Id) ACCESSOR(S, Schema) public: IModel(IIdentity * id, ISchema * schema) { Id = id; Schema = dynamic_cast<S*>(schema); components = new silk_data::RBTree<SILK_STRING*, IComponent>( new StringCompareStrategy()); } ~IModel() { delete Id; Schema = nullptr; components->Clear(); delete components; } template <class T> T* Get(SILK_STRING * key) { return (T*)components->Find(key); } private: silk_data::RBTree<SILK_STRING*, IComponent>* components; }; 

Skema ini mencakup deskripsi model tertentu dan berisi konteks yang dapat digunakan model tersebut.

 class IModelSchema : public BaseSchema { ACCESSOR(ModelContext, Context) public: IModelSchema(const char* type, const char* name, IContext* context) : BaseSchema(type, name) { Context = dynamic_cast<ModelContext*>(context); } ~IModelSchema() { Context = nullptr; } }; class ModelContext : public SilkContext { ACCESSOR(ILogger, Logger) ACCESSOR(IChrono, Clock) ACCESSOR(GigaFactory, Factory) ACCESSOR(IGameModel*, Model) public: ModelContext(SILK_GUID* guid, ILogger* logger, IChrono* clock, GigaFactory* factory, IGameModel** model) : SilkContext(guid) { Logger = logger; Clock = clock; Factory = factory; Model = model; } ~ModelContext() { Logger = nullptr; Clock = nullptr; Factory = nullptr; Model = nullptr; } }; 

Koleksi model dan koleksi skema
 template <class T, class S> class IModelCollection : public silk_data::Vector<T*>, public IModel<S> { protected: IModelCollection(IIdentity* id, ISchema* schema) : Vector(), IModel(id, schema) { auto factory = Schema->GetContext()->GetFactory(); auto guid = Schema->GetContext()->GetGuid(); foreach (Schema->Length()) { auto itemSchema = Schema->GetItem(i); auto item = factory->Build<T>(itemSchema->GetType()->GetValue(), guid->Get(), itemSchema); PushBack(item); } } public: ~IModelCollection() { Clear(); } T* GetByName(const char* name) { foreach (Length()) if (GetItem(i)->GetSchema()->CheckName(name)) return GetItem(i); return nullptr; } }; 


Jadi misalnya, antarmuka dan implementasi model yang menyimpan status Roshan terlihat seperti
 DEFINE_IMODEL(IRoshanStatusModel, IRoshanStatusSchema) { VIRTUAL_COMPONENT(IStatesModel, States) public: virtual void Resolve() = 0; protected: IRoshanStatusModel(IIdentity * id, ISchema * schema) : IModel(id, schema) {} }; DEFINE_MODEL(RoshanStatusModel, IRoshanStatusModel) { COMPONENT(IStatesModel, States) public : RoshanStatusModel(IIdentity * id, ISchema* schema) : IRoshanStatusModel( id, schema) { auto factory = Schema->GetContext()->GetFactory(); auto guid = Schema -> GetContext() -> GetGuid(); auto statesSchema = Schema -> GetStates(); States = factory->Build<IStatesModel>( statesSchema->GetType()->GetValue(), guid->Get(), statesSchema); } ~RoshanStatusModel() { delete States; } void Resolve() { auto currentStateSchema = States->GetCurrent()->GetSchema(); Schema->GetContext()->GetLogger()->Log("RESOLVE\n"); foreach (currentStateSchema->GetTransitions()->Length()) { auto transition = currentStateSchema->GetTransitions()->GetItem(i); if (transition->GetRequirement()->Check()) { transition->GetAction()->Make(); States->SetCurrent(States->GetByName( transition->GetTo()->GetValue())); break; } } } }; 


8.2. Lihat, Lihat Status dan Kontroler


Tidak ada banyak yang bisa dikatakan tentang Presentation, Presentation State dan Controller, implementasinya mirip dengan Models. Mereka juga terdiri dari skema dan konteks. Untuk memecahkan masalah untuk Lihat, Kanvas, ViewCollection, Label dan Tombol diimplementasikan, untuk dua terakhir, negara yang sesuai dengan negara di mana Roshan berada juga diimplementasikan.

Tampilan Skematik

16 —

Representasi skematis dari Negara Tampilan

17 —

8.3. Pabrik


Objek dibuat menggunakan pabrik. Pabrik menggunakan tipe antarmuka sebagai kunci, menerjemahkannya ke dalam string menggunakan typeid (T) .raw_name (). Secara umum, melakukan itu buruk, mengapa dan bagaimana membaca dengan benar di Andrei Alexandrescu, Modern C ++ Design: Generic Programming. Implementasi Pabrik:

 class SilkFactory { public: SilkFactory() { items = new silk_data::RBTree<SILK_STRING*, IImplementator>( new StringCompareStrategy()); } ~SilkFactory() { items->Clear(); delete items; } template <class... Args> ISILK_WAY_OBJECT* Build(const char* type, Args... args) { auto key = new SILK_STRING(type); auto impl = items->Find(key)->payload; return impl->Build(args...); } void Register(const char* type, IImplementator* impl) { auto key = new SILK_STRING(type); items->Insert(*items->MakeNode(key, impl)); } protected: silk_data::RBTree<SILK_STRING*, IImplementator>* items; }; class GigaFactory { public: GigaFactory() { items = new silk_data::RBTree<SILK_STRING*, SilkFactory>( new StringCompareStrategy()); } ~GigaFactory() { items->Clear(); delete items; } template <class T, class... Args> T* Build(const char* concreteType, Args... args) { auto key = new SILK_STRING(typeid(T).raw_name()); auto factory = items->Find(key)->payload; return (T*)factory->Build(concreteType, args...); } template <class T> void Register(SilkFactory* factory) { auto key = new SILK_STRING(typeid(T).raw_name()); items->Insert(*items->MakeNode(key, factory)); } protected: silk_data::RBTree<SILK_STRING*, SilkFactory>* items; }; 

Sebelum menggunakan pabrik untuk membangun objek, Anda harus mendaftar.
Contoh Pendaftaran Model
 void ModelRegistrator::Register( GigaFactory* factory) { auto requirement = new SilkFactory(); requirement->Register("true", new SchemaImplementator<TrueRequirement>); requirement->Register("false", new SchemaImplementator<FalseRequirement>); requirement->Register("roshan_killed", new SchemaImplementator<RoshanKilledRequirement>); requirement->Register("roshan_alive_manual", new SchemaImplementator<RoshanAliveManualRequirement>); requirement->Register("time", new SchemaImplementator<TimeRequirement>); requirement->Register("roshan_state", new SchemaImplementator<RoshanStateRequirement>); factory->Register<IRequirement>(requirement); auto action = new SilkFactory(); action->Register("action", new SchemaImplementator<EmptyAction>); action->Register("set_current_time", new SchemaImplementator<SetCurrentTimeAction>); factory->Register<IAction>(action); auto transition = new SilkFactory(); transition->Register("transition", new SchemaImplementator<TransitionSchema>); factory->Register<ITransitionSchema>(transition); auto transitions = new SilkFactory(); transitions->Register("transitions", new SchemaImplementator<TransitionsSchema>); factory->Register<ITransitionsSchema>(transitions); auto stateSchema = new SilkFactory(); stateSchema->Register("state", new SchemaImplementator<StateSchema>); factory->Register<IStateSchema>(stateSchema); auto statesSchema = new SilkFactory(); statesSchema->Register("states", new SchemaImplementator<StatesSchema>); factory->Register<IStatesSchema>(statesSchema); auto roshanStatusSchema = new SilkFactory(); roshanStatusSchema->Register("roshan_status", new SchemaImplementator<RoshanStatusSchema>); factory->Register<IRoshanStatusSchema>(roshanStatusSchema); auto triggerSchema = new SilkFactory(); triggerSchema->Register("trigger", new SchemaImplementator<TriggerSchema>); factory->Register<ITriggerSchema>(triggerSchema); auto triggersSchema = new SilkFactory(); triggersSchema->Register("triggers", new SchemaImplementator<TriggersSchema>); factory->Register<ITriggersSchema>(triggersSchema); auto resourceSchema = new SilkFactory(); resourceSchema->Register("resource", new SchemaImplementator<ResourceSchema>); factory->Register<IResourceSchema>(resourceSchema); auto resourcesSchema = new SilkFactory(); resourcesSchema->Register("resources", new SchemaImplementator<ResourcesSchema>); factory->Register<IResourcesSchema>(resourcesSchema); auto gameSchema = new SilkFactory(); gameSchema->Register("game", new SchemaImplementator<GameSchema>); factory->Register<IGameSchema>(gameSchema); auto gameModel = new SilkFactory(); gameModel->Register("game", new ConcreteImplementator<GameModel>); factory->Register<IGameModel>(gameModel); auto resources = new SilkFactory(); resources->Register("resources", new ConcreteImplementator<ResourceCollection>); factory->Register<IResourceCollection>(resources); auto resource = new SilkFactory(); resource->Register("resource", new ConcreteImplementator<Resource>); factory->Register<IResource>(resource); auto triggers = new SilkFactory(); triggers->Register("triggers", new ConcreteImplementator<TriggerCollection>); factory->Register<ITriggerCollection>(triggers); auto trigger = new SilkFactory(); trigger->Register("trigger", new ConcreteImplementator<Trigger>); factory->Register<ITrigger>(trigger); auto roshanStatus = new SilkFactory(); roshanStatus->Register("roshan_status", new ConcreteImplementator<RoshanStatusModel>); factory->Register<IRoshanStatusModel>(roshanStatus); auto states = new SilkFactory(); states->Register("states", new ConcreteImplementator<StatesModel>); factory->Register<IStatesModel>(states); auto state = new SilkFactory(); state->Register("state", new ConcreteImplementator<StateModel>); factory->Register<IStateModel>(state); } 


Skema dapat diisi dengan cara apa pun - Anda dapat menggunakan json, Anda dapat langsung dalam kode.
Opsi untuk mengisi skema untuk Model di json
 { "game": { "roshan_status": { "states": [ { "name": "alive", "transitions": [ { "from": "alive", "to": "ressurect_base", "requirement": { "typename": "roshan_killed", "action": { "typename": "set_current_time", "resource": "roshan_killed_ts" } } } ] }, { "name": "ressurect_base", "transitions": [ { "from": "ressurect_base", "to": "ressurect_extra", "requirement": { "typename": "time", "resource": "roshan_killed_ts", "offset": 480 }, "action": { "typename": "action" } } ] }, { "name": "ressurect_extra", "transitions": [ { "from": "ressurect_extra", "to": "alive", "requirement": { "typename": "time", "resource": "roshan_killed_ts", "offset": 660 }, "action": { "typename": "action" } }, { "from": "ressurect_extra", "to": "alive", "requirement": { "typename": "roshan_alive_manual" }, "action": { "typename": "action" } } ] } ] }, "triggers": { "roshan_killed": {}, "roshan_alive_manual": {} }, "resources": { "roshan_killed_ts": {} } } } 


Opsi untuk mengisi skema untuk pengiriman kode
 void GameController::InitViewSchema(ICanvasSchema** schema) { *schema = factory->Build<ICanvasSchema>("canvas_d9", "canvas_d9", "canvas_d9", viewContext); IViewCollectionSchema* elements = factory->Build<IViewCollectionSchema>( "elements", "elements", "elements", viewContext); (*schema)->SetElements(elements); ILabelSchema* labelSchema = factory->Build<ILabelSchema>( "label_d9", "label_d9", "roshan_status_label", viewContext); labelSchema->SetRecLeft(new SILK_INT(30)); labelSchema->SetRecTop(new SILK_INT(100)); labelSchema->SetRecRight(new SILK_INT(230)); labelSchema->SetRecDown(new SILK_INT(250)); labelSchema->SetColorR(new SILK_FLOAT(1.0f)); labelSchema->SetColorG(new SILK_FLOAT(1.0f)); labelSchema->SetColorB(new SILK_FLOAT(1.0f)); labelSchema->SetColorA(new SILK_FLOAT(1.0f)); labelSchema->SetText(new SILK_STRING("Roshan status: alive\0")); elements->PushBack((IViewSchema*&)labelSchema); IButtonSchema* buttonSchema = factory->Build<IButtonSchema>( "button_d9", "button_d9", "roshan_kill_button", viewContext); ILabelSchema* buttonLabelSchema = factory->Build<ILabelSchema>( "label_d9", "label_d9", "button_text", viewContext); buttonLabelSchema->SetRecLeft(new SILK_INT(30)); buttonLabelSchema->SetRecTop(new SILK_INT(115)); buttonLabelSchema->SetRecRight(new SILK_INT(110)); buttonLabelSchema->SetRecDown(new SILK_INT(130)); buttonLabelSchema->SetColorR(new SILK_FLOAT(1.0f)); buttonLabelSchema->SetColorG(new SILK_FLOAT(0.0f)); buttonLabelSchema->SetColorB(new SILK_FLOAT(0.0f)); buttonLabelSchema->SetColorA(new SILK_FLOAT(1.0f)); buttonLabelSchema->SetText(new SILK_STRING("Kill Roshan\0")); buttonSchema->SetLabel(buttonLabelSchema); buttonSchema->SetBorderColorR(new SILK_INT(0)); buttonSchema->SetBorderColorG(new SILK_INT(0)); buttonSchema->SetBorderColorB(new SILK_INT(0)); buttonSchema->SetBorderColorA(new SILK_INT(70)); buttonSchema->SetFillColorR(new SILK_INT(255)); buttonSchema->SetFillColorG(new SILK_INT(119)); buttonSchema->SetFillColorB(new SILK_INT(0)); buttonSchema->SetFillColorA(new SILK_INT(150)); buttonSchema->SetPushColorR(new SILK_INT(0)); buttonSchema->SetPushColorG(new SILK_INT(0)); buttonSchema->SetPushColorB(new SILK_INT(0)); buttonSchema->SetPushColorA(new SILK_INT(70)); buttonSchema->SetBorder(new SILK_FLOAT(5)); elements->PushBack((IViewSchema*&)buttonSchema); } 


8.4. Acara


Pandangan mempelajari tentang perubahan dalam Model melalui peristiwa. Anda bisa mendapatkan umpan balik dalam metode kelas dan fungsi biasa.

 #define VIRTUAL_EVENT(e) public: virtual IEvent* Get##e() = 0; #define EVENT(e) private: IEvent* e; public: IEvent* Get##e() { return e; } const int MAX_EVENT_CALLBACKS = 1024; class IEventArgs {}; class ICallback { public: virtual void Invoke(IEventArgs* args) = 0; }; template <class A> class Callback : public ICallback { typedef void (*f)(A*); public: Callback(f _pFunc) { ptr = _pFunc; } ~Callback() { delete ptr; } void Invoke(IEventArgs* args) { ptr((A*)args); } private: f ptr = nullptr; }; template <typename T, class A> class MemberCallback : public ICallback { typedef void (T::*f)(A*); public: MemberCallback(f _pFunc, T* _obj) { ptr = _pFunc; obj = _obj; } ~MemberCallback() { delete ptr; obj = nullptr; } void Invoke(IEventArgs* args) { (obj->*(ptr))((A*)args); } private: f ptr = nullptr; T* obj; }; class IEvent { public: virtual void Invoke(IEventArgs* args) = 0; virtual void Add(ICallback* callback) = 0; virtual bool Remove(ICallback* callback) = 0; virtual ~IEvent() {} }; 

Jika suatu objek ingin melaporkan peristiwa yang terjadi di dalamnya, Anda perlu menambahkan IEvent * untuk setiap peristiwa. Objek lain yang tertarik pada peristiwa yang terjadi di dalam objek ini harus membuat ICallback * dan meneruskannya di dalam IEvent * (berlangganan acara tersebut).
Contoh langganan yang terjadi di controller
 void Attach() { statesChangedCallback = new MemberCallback<GameController, IEventArgs>( &GameController::OnStatesChanged, this); Model->GetRoshanStatus()->GetStates()->GetCurrentChanged()->Add( statesChangedCallback); buttonClickedCallback = new MemberCallback<GameController, IEventArgs>( &GameController::OnKillRoshanClicked, this); killButton->GetClickedEvent()->Add(buttonClickedCallback); } 


Contoh mendeklarasikan suatu peristiwa di dalam kelas - dengan setiap klik jam (memanggil metode Tick), sebuah acara StruckEvent dimunculkan
 class IChrono { VIRTUAL_EVENT(Struck) public: virtual void Tick() = 0; virtual long long GetStamp() = 0; virtual long long GetDiffS(long long ts) = 0; }; class Chrono : public IChrono { EVENT(Struck) public: Chrono() { start = time(0); Struck = new Event(); } ~Chrono() { delete Struck; } void Tick() { auto cur = clock(); worked += cur - savepoint; bool isStriking = savepoint < cur; savepoint = cur; if (isStriking) Struck->Invoke(nullptr); } long long GetStamp() { return start * CLOCKS_PER_SEC + worked; } long long GetDiffS(long long ts) { return (GetStamp() - ts) / CLOCKS_PER_SEC; } private: long long worked = 0; time_t start; time_t savepoint; }; 


Tipe primitif dasar (SILK_INT, SILT_FLOAT, SILK_STRING, ...) diimplementasikan di Core.h.

9. DirectX 9


DirectX 9 adalah salah satu API grafik yang didukung oleh Dota 2. Perangkat adalah kelas yang diwarisi dari IUnknown dan berisi fungsi virtual. Dengan demikian, setelah menerima pointer ke tabel metode virtual, kita bisa mendapatkan pointer ke fungsi yang kita butuhkan. Fungsi kelas non-virtual tidak termasuk dalam tabel dan berada di segmen .code, karena mereka adalah satu-satunya yang tidak bisa diganti. By the way, di OpenGL dan Vulkan, fungsi mencegat perangkat jauh lebih mudah, karena mereka tidak virtual dan Anda bisa mendapatkan pointer menggunakan GetProcAddress (). Arsitektur DirectX 11 lebih kompleks dari 9, tetapi tidak banyak.

Untuk mencegat metode kelas virtual (dan juga metode non-virtual), kita memerlukan instance kelas ini, instance apa saja. Dengan menggunakan instance, kita mendapatkan tabel metode virtual dan mendapatkan pointer yang diperlukan untuk fungsi. Cara termudah untuk menemukan instance kelas adalah dengan membuatnya sendiri.

Untuk melakukan ini, kita perlu membuat objek dengan antarmuka IDirect3D9 menggunakan fungsi Direct3DCreate9, dan kita akan membuat perangkat menggunakan objek ini dengan memanggil metode CreateDevice. Kita dapat memanggil fungsi-fungsi ini secara langsung dari perpustakaan DirectX, tetapi untuk mengkonsolidasikan materi, kita akan memanggil mereka melalui pointer. Seperti yang dapat dilihat dari d3d9.h, Direct3DCreate9 adalah fungsi reguler dan sebuah pointer ke sana dapat diperoleh melalui GetProcAddress (seperti yang kami lakukan di NativeInjector untuk mendapatkan pointer ke LoadLibrary).


Gambar 18 - Deskripsi CreateDevice di d3d9.h

Buat instance IDirect3D9:
 typedef IDirect3D9* (WINAPI *SILK_Direct3DCreate9) (UINT SDKVersion); //IDirect3D9* pD3D = Direct3DCreate9(D3D_SDK_VERSION); SILK_Direct3DCreate9 Silk_Direct3DCreate9 = (SILK_Direct3DCreate9)GetProcAddress(GetModuleHandle("d3d9.dll"), "Direct3DCreate9"); IDirect3D9* pD3D = Silk_Direct3DCreate9(D3D_SDK_VERSION); 

Menggunakan IDirect3D9, kita dapat membuat perangkat dengan memanggil pD3D-> CreateDevice (...). Untuk mendapatkan pointer ke fungsi yang diperlukan dari VMT, kita perlu mencari tahu prosedur untuk menentukan metode ini. Gambar 19 - Pencarian indeks untuk metode CreateDevice dari antarmuka IDirect3D9 Dapatkan indeks ke - 16. Selain CreateDevice, kami juga memerlukan metode Release dan GetAdapterDisplayMode.






Kami menerapkan pembuatan perangkat dalam kode
 typedef HRESULT(WINAPI *SILK_GetAdapterDisplayMode)(IDirect3D9* direct3D9, UINT Adapter, D3DDISPLAYMODE* pMode); typedef HRESULT(WINAPI *SILK_CreateDevice)(IDirect3D9* direct3D9, UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface); typedef ULONG(WINAPI *SILK_Release)(IDirect3D9* direct3D9); const int RELEASE_INDEX = 2; const int GET_ADAPTER_DISPLAY_MODE_INDEX = 8; const int CREATE_DEVICE_INDEX = 16; BOOL CreateSearchDevice(IDirect3D9** d3d, IDirect3DDevice9** device) { if (!d3d || !device) return FALSE; *d3d = NULL; *device = NULL; //IDirect3D9* pD3D = Direct3DCreate9(D3D_SDK_VERSION); SILK_Direct3DCreate9 Silk_Direct3DCreate9 = (SILK_Direct3DCreate9)GetProcAddress(GetModuleHandle("d3d9.dll"), "Direct3DCreate9"); IDirect3D9* pD3D = Silk_Direct3DCreate9(D3D_SDK_VERSION); if (!pD3D) return FALSE; D3DDISPLAYMODE displayMode; int pointerSize = sizeof(unsigned long long); unsigned long long vmt = **(unsigned long long **)&pD3D; SILK_GetAdapterDisplayMode pGetAdapderDisplayMode = (SILK_GetAdapterDisplayMode)((*(unsigned long long *) (vmt + pointerSize * GET_ADAPTER_DISPLAY_MODE_INDEX))); pGetAdapderDisplayMode(pD3D, D3DADAPTER_DEFAULT, &displayMode); //pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &displayMode); HWND hWindow = GetDesktopWindow(); D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp, sizeof(pp)); pp.Windowed = TRUE; pp.hDeviceWindow = hWindow; pp.BackBufferCount = 0; pp.BackBufferWidth = 0; pp.BackBufferHeight = 0; pp.BackBufferFormat = displayMode.Format; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; IDirect3DDevice9* pDevice = NULL; SILK_CreateDevice pCreateDevice = (SILK_CreateDevice) ((*(unsigned long long *)(vmt + pointerSize * CREATE_DEVICE_INDEX))); if(SUCCEEDED(pCreateDevice(pD3D, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_DISABLE_DRIVER_MANAGEMENT, &pp, &pDevice))) { //if (SUCCEEDED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_DISABLE_DRIVER_MANAGEMENT, &pp, &pDevice))) { if (pDevice != NULL) { *d3d = pD3D; *device = pDevice; } } BOOL result = (*d3d != NULL); if (result == FALSE) if (pD3D) { SILK_Release pRelease= (SILK_Release)((*(unsigned long long *)(vmt + pointerSize * RELEASE_INDEX))); pRelease(pD3D); //pD3D->Release(); } return result; } 


Nah, kami menciptakan perangkat DirectX 9, sekarang kami harus memahami fungsi apa yang digunakan untuk membuat adegan, apa yang perlu kami sadap. Kita perlu menjawab pertanyaan: "Bagaimana DirectX 9 menunjukkan adegan itu kepada kita?" Fungsi Present digunakan untuk menampilkan adegan . Ada baiknya juga memperkenalkan konsep-konsep seperti buffer depan (buffer yang menyimpan apa yang ditampilkan (aksi jangka panjang) di layar), back buffer - berisi apa yang siap untuk ditampilkan dan sedang bersiap untuk menjadi buffer depan, rantai swap - sebenarnya satu set buffer yang membalik dari depan ke belakang (DirectX 9 hanya memiliki 1 rantai swap). Sebelum memanggil Present, beberapa fungsi BeginScene dan EndScene dipanggil , tempat Anda dapat memodifikasi buffer kembali.

Mari kita sela dua fungsi (pada kenyataannya, untuk mengeksekusi logika bisnis, satu sudah cukup bagi kita): EndScene dan Present. Untuk melakukan ini, lihat lokasi fungsi-fungsi ini di kelas IDirect3DDevice9 Gambar 20 - Mendeklarasikan antarmuka IDirect3DDevice9 Deklarasikan pointer dengan tanda tangan fungsi berikut:






 typedef HRESULT(*VirtualOverloadPresent)(IDirect3DDevice9* pd3dDevice, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion); VirtualOverloadPresent oOverload = NULL; typedef HRESULT(*VirtualOverloadEndScene)(IDirect3DDevice9* pd3dDevice); VirtualOverloadEndScene oOverloadEndScene = NULL; const int PRESENT_INDEX = 17; const int END_SCENE_INDEX = 42; 

Kami akan segera mengumumkan jebakan dengan penangan kesalahan, karena HardwareBreakpoint sebenarnya adalah satu-satunya opsi intersepsi aman kami yang diimplementasikan yang tidak melacak VAC (Anda juga dapat menguji dengan Opcode Hook, tetapi akun Anda kemungkinan besar akan terbang dalam larangan):

 silk_way::IDeferredCommands* deferredCommands; silk_way::IHook* hook; LONG OnExceptionHandler(EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_EXIT_UNWIND; for (int i = 0; i < silk_way::DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long) hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Dr7 &= ~(1ULL << (2 * i)); exceptionPointers->ContextRecord->Rip = (unsigned long long) hook->GetInfo()->GetItem(i)->destination; silk_way::IDeferredCommand* cmd = new silk_way::SetD7Command(hook, GetCurrentThreadId(), i); deferredCommands->Enqueue(cmd); break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

Suara fungsi yang ditunjuk dari salah satu dari dua perangkap kami:

 BOOL HookDevice(IDirect3DDevice9* pDevice) { unsigned long long vmt = **(unsigned long long **)&pDevice; int pointerSize = sizeof(unsigned long long); VirtualOverloadPresent pointerPresent= (VirtualOverloadPresent) ((*(unsigned long long *)(vmt + pointerSize * PRESENT_INDEX))); VirtualOverloadEndScene pointerEndScene = (VirtualOverloadEndScene) ((*(unsigned long long *)(vmt + pointerSize * END_SCENE_INDEX))); oOverload = pointerPresent; oOverloadEndScene = pointerEndScene; deferredCommands = new silk_way::DeferredCommands(); //hook = new silk_way::HardwareBPHook(); hook = new silk_way::OpcodeHook(); hook->SetExceptionHandler(OnExceptionHandler); hook->SetHook(pointerPresent, &PresentHook); hook->SetHook(pointerEndScene, &EndSceneHook); return TRUE; } 

Penerima fungsi:

 HRESULT WINAPI PresentHook(IDirect3DDevice9* pd3dDevice, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion) { Capture(pd3dDevice); auto record = hook->GetRecordBySource(oOverload); VirtualOverloadPresent pTrampoline = (VirtualOverloadPresent) record->pTrampoline; auto result = pTrampoline(pd3dDevice, pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion); deferredCommands->Run(); return result; } HRESULT WINAPI EndSceneHook(IDirect3DDevice9* pd3dDevice) { if (controller == nullptr) { controller = new GameController(); controller->SetDevice(pd3dDevice); } controller->Update(); auto record = hook->GetRecordBySource(oOverloadEndScene); VirtualOverloadEndScene pTrampoline = (VirtualOverloadEndScene) record->pTrampoline; auto result = pTrampoline(pd3dDevice); deferredCommands->Run(); return result; } 

Di Present, setiap panggilan mengambil tangkapan layar dari buffer kartu video (untuk verifikasi) menggunakan fungsi Capture
 VOID WINAPI Capture(IDirect3DDevice9* pd3dDevice) { IDirect3DSurface9 *renderTarget = NULL; IDirect3DSurface9 *destTarget = NULL; HRESULT res1 = pd3dDevice->GetRenderTarget(0, &renderTarget); D3DSURFACE_DESC descr; HRESULT res2 = renderTarget->GetDesc(&descr); HRESULT res3 = pd3dDevice->CreateOffscreenPlainSurface( descr.Width, descr.Height, /*D3DFMT_A8R8G8B8*/descr.Format, D3DPOOL_SYSTEMMEM, &destTarget, NULL); HRESULT res4 = pd3dDevice->GetRenderTargetData(renderTarget, destTarget); D3DLOCKED_RECT lockedRect; ZeroMemory(&lockedRect, sizeof(lockedRect)); if (destTarget == NULL) return; HRESULT res5 = destTarget->LockRect(&lockedRect, NULL, D3DLOCK_READONLY); HRESULT res7 = destTarget->UnlockRect(); HRESULT res6 = D3DXSaveSurfaceToFile(screenshootPath, D3DXIFF_BMP, destTarget, NULL, NULL); renderTarget->Release(); destTarget->Release(); } 


EndScene menciptakan pengontrol logika bisnis. Setelah pembuatan, pembaruan kontroler disebut, di mana semua logika diperbarui.

Saya perhatikan bahwa sekarang kami telah mengimplementasikan pekerjaan dengan DirectX 9. Jika kami ingin membuat semacam mod, cheat, dll., Keempat API harus didukung. Ini dibenarkan jika gudang senjata telah memiliki perpustakaan favorit Anda, kosong untuk UI, jika tidak Anda dapat menggunakan cara lain - fungsi yang menggunakan mesin untuk membuat permainan.

Perlu juga dikatakan bahwa panggilan pembaruan logika dari EndScene () bukan pilihan terbaik - Anda dapat menemukan panggilan periodik ke fungsi mesin atau logika panggilan di aliran Anda. Namun, jika Anda puas dengan panggilan dari EndScene, lebih baik melakukan ini dengan lockstep.

Sekarang kami telah mengimplementasikan semua yang kami rencanakan.

Rekomendasi Pengujian
DirectX SDK , , DirectX 9 DirectX 11. DirectX 11, - SDK, ( , ) , , DXUT, , — , FPS .


21 — DirectX SDK StateManager.exe

Sekarang Anda dapat membuat akun palsu di Steam dan menyuntikkan injected.dll ke dalam proses Dota 2. Saya akan segera berkata, saya tidak tahu bagaimana situasi saat ini dengan breakpoint "besi" - menggunakan Opcode Hook (cara kami melakukannya di saat ini form) Anda pasti akan mendapatkan larangan. Saya melakukan ini sekitar enam bulan lalu - tidak ada larangan untuk Hardware Breakpoint, saya tidak bisa mengatakan apa situasinya saat ini. Sebelum menyiapkan artikel, saya mengambil dua akun dan mencoba Opcode Hook dan HWBP pada mereka, yang pertama terbang ke larangan (sekitar 2 minggu berlalu), yang kedua tidak (3 minggu berlalu). Namun tetap tidak ada jaminan bahwa larangan tersebut tidak akan ada di masa depan. Maka jangan tersinggung jika Anda secara tidak sengaja membuat pengantar dari akun utama Anda atau lupa untuk masuk ke akun palsu - maka jaga diri Anda dan berhati-hatilah.

( )

22 —


23 —

Implementasi dalam mode 1x1. Gambar 24 - Injeksi ke dalam korek Juga perlu disebutkan bahwa ada cara lain untuk rendering - rendering permukaan dengan membuat jendela kedua dengan ukuran yang sesuai. Sayangnya, saya tidak dapat menyadari kemungkinan menggunakan pendekatan permukaan untuk kasus mode layar penuh, tetapi pendekatan yang dijelaskan dalam artikel ini memungkinkan Anda untuk menerapkan rendering dalam mode layar penuh dan jendela tanpa masalah. UI tertanam kami hanya berisi label teks dan tombol yang diterapkan pada DirectX 9 murni - hanya ini yang diperlukan untuk menyelesaikan tugas. Anda dapat mengimplementasikan tabel kompleks, menu dan diagram yang indah - secara umum, UI kompleksitas apa pun, baik pada API murni dan menggunakan pustaka siap pakai. Tentu saja, bukan hanya 2D.







10. Menggunakan fungsi mesin


Menerapkan fungsi yang sama untuk setiap API agak suram, pengembang membuat pembungkus yang nyaman dengan menyediakan fungsi untuk menggambar, UI, dan sebagainya, yang digunakan permainan secara langsung. Valve juga menyediakan Dota 2 API untuk Javascript dan Lua . Hal ini dilakukan untuk membuat hidup lebih mudah bagi moderator dan desainer game untuk siapa C ++ rumit (bahkan C ++ sendiri, tetapi penggunaan yang tepat dalam konteks mesin). Di sini ada fungsi untuk rendering, dan untuk logika permainan - Anda dapat menentukan perilaku unit, misalnya, memilih item, menggunakan keterampilan, dan banyak lagi. Sebenarnya, dengan bantuan ini, surat adat ditulis.

Kami akan tertarik dengan fungsi DoIncludeScript, yang memungkinkan Anda untuk menjalankan skrip Anda di Lua dan menggunakan Scripting API di sana. Saya tidak menggunakannya dalam proyek saya, karena saya tidak melihat nilai di dalamnya, menggunakan fungsi langsung dari C ++, saya melihat ide untuk menggunakannya dengan or_75 dan memutuskan untuk memasukkannya ke dalam artikel. Ini akan memperkenalkan Anda pada apa yang akan ada di bagian kedua dan menghemat ruang di dalamnya, Anda tidak perlu menjelaskan aspek-aspek tertentu dari debugger.

Mari kita mulai.Tugasnya adalah sebagai berikut: Anda perlu menemukan pointer ke fungsi DoIncludeScript, yang mengambil nama skrip dan pengendali, untuk mempelajarinya. Kami akan mencari fungsi menggunakan pemindai dari perpustakaan silk_way.lib kami. Fungsi, seperti yang telah kita ketahui, dikodekan dalam memori menggunakan tabel opcode - mari kita periksa fungsi ini dan mencoba mengidentifikasi pola penyimpanannya dalam memori. Sekarang pemindai tidak memiliki fungsionalitas yang diperlukan, kita perlu kemampuan untuk mencari templat di memori proses.

Untuk mempercepat pencarian, kami tidak akan mencari pola di seluruh memori proses, tetapi dalam modul tertentu (fungsi kami terletak di client.dll, ini akan terlihat di debugger dan akan dibahas di bawah). Kami akan mencari modul menggunakan tlHelp32 dengan nama dengan menyebutkan semua modul dari proses, yang mana kami akan membuat fungsi untuk menemukan modul dalam proses GetModuleInfo saat ini.

Kode Fungsi GetModuleInfo
 int IScanner::GetModuleInfo(const char* name, MODULEENTRY32* entry) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId()); if (snapshot == INVALID_HANDLE_VALUE) return 1; entry->dwSize = sizeof(MODULEENTRY32); if (!Module32First(snapshot, entry)) { CloseHandle(snapshot); return 1; } do { if (!_stricmp(entry->szModule, name)) break; } while (Module32Next(snapshot, entry)); CloseHandle(snapshot); return 0; } 


Polanya adalah string dengan nilai byte, melewatkan byte ditandai dengan simbol "??" - misalnya, “j9 ?? ?? ?? ??48 03 08 ?? f1 ff ”.

Mem-parsing string, untuk kenyamanan kami akan mentransfer pola dari representasi string ke daftar nilai char yang tidak ditandai, mengatur flag byte yang akan dilewati.

 unsigned char* IScanner::Parse(int& len, const char* strPattern, unsigned char* skipByteMask) { int strPatternLen = strlen(strPattern); unsigned char* pattern = new unsigned char[strPatternLen]; for (int i = 0; i < strPatternLen; i++) pattern[i] = 0; len = 0; for (int i = 0; i < strPatternLen; i += 2) { unsigned char code = 0; if (strPattern[i] == SKIP_SYMBOL) skipByteMask[len] = 1; else code = Parse(strPattern[i]) * 16 + Parse(strPattern[i + 1]); i++; pattern[len++] = code; } return pattern; } unsigned char IScanner::Parse(char byte) { // some magic values if (byte >= '0' && byte <= '9') return byte - '0'; else if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10; else if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10; return 0; } 

Inti pencarian diimplementasikan dalam fungsi FindPattern, di mana, berdasarkan pada informasi yang diterima tentang modul, alamat awal dan akhir pencarian diatur. Informasi tentang memori yang akan dicari diminta oleh fungsi VirtualQuery, ada sejumlah persyaratan untuk memori - itu harus sibuk (itu akan menjadi kesalahan untuk mencari di memori bebas), memori harus dapat dibaca, dieksekusi dan tidak mengandung bendera PageGuard:

 void* pStart = moduleEntry.modBaseAddr; void* pFinish = moduleEntry.modBaseAddr + moduleEntry.modBaseSize; unsigned char* current = (unsigned char*)pStart; for (; current < pFinish && j < patternLen; current++) { if (!VirtualQuery((LPCVOID)current, &info, sizeof(info))) continue; unsigned long long protectMask = PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE | PAGE_EXECUTE_READ; if (info.State == MEM_COMMIT && info.Protect & protectMask && !(info.Protect & PAGE_GUARD)) { unsigned long long finish = (unsigned long long)pFinish < (unsigned long long)info.BaseAddress + info.RegionSize ? (unsigned long long)pFinish : (unsigned long long) info.BaseAddress + info.RegionSize; current = (unsigned char*)info.BaseAddress; unsigned char* rip = 0; for (unsigned long long k = (unsigned long long)info.BaseAddress; k < finish && j < patternLen; k++, current++) { if (skipByteMask[j] || pattern[j] == *current) { if (j == 0) rip = current; j++; } else { j = 0; if (pattern[0] == *current) { rip = current; j = 1; } } } if (j == patternLen) { current = rip; break; } } else current += sysInfo.dwPageSize; } 

Sekarang kita dapat mencari template yang diinginkan dalam memori proses, tetapi belum tahu apa yang harus dicari. Jalankan Steam di bawah akun Palsu dan buka debugger favorit Anda (mari kita sepakat bahwa untuk saat membaca artikel x64dbg juga untuk Anda - Saya tidak memiliki lisensi berbayar untuk IDA Pro), jalankan dota2.exe di dalamnya dari ... \ Steam \ steamapps \ direktori umum \ dota 2 beta \ game \ bin \ win64. Pada prinsipnya, saya tidak memperhatikan bahwa VAC tidak acuh terhadap Cheat Engine dan x64dbg, saya tidak ingat bahwa ketika menggunakan alat-alat ini, akun itu diblokir. Omong-omong, debugger memiliki plugin ScyllaHide yang memotong fungsi sistem seperti NtCreateThreadEx, NtSetInformationThread, dll., Menyembunyikan fakta kerjanya, Anda dapat menginstal plugin ini.

Di setiap perhentian (akan ada 10-15), kami terus mengeksekusi menggunakan Run (F9). Ketika permainan dimulai, kita akan melihat menu dan dapat mulai meneliti. Setelah memulai gim, lakukan pencarian pada baris (Cari-> Semua Modul-> Referensi String), setel filter “DoIncludeScript”. Gambar 25 - Mencari garis-garis di memori proses permainan. Mari kita pergi ke disassembler (tab CPU) dengan mengklik dua kali pada hasil pertama. Ini akan menjadi alamat awal kami, karena berada di client.dll, sisanya hasilnya di server.dll dan animationsystem.dll. Kami membuat grafik panggilan dari alamat yang diterima. Gambar 26 - Grafik panggilan Setelah dekompilasi, kami menemukan titik masuk di mana DoIncludeScript digunakan - simpul keempat dari grafik. Sebenarnya fungsinya sendiri.














Gambar 27 - Fungsi

Grafik DoIncludeScript . Gambar 28 - Grafik panggilan dari DoIncludeScript Mengompilasi penggunaan fungsi menunjukkan kode berikut dan tempat panggilannya (dekompilasi dilakukan dari grafik, bukan dari disassembler). Gambar 29 - Mengompilasi panggilan ke fungsi DoIncludeScript Mari kita membuat templat dari instruksi pada Gambar 27 dari panggilan ke fungsi DoIncludeScript. Argumen dapat berubah, masing-masing, kami ingin melewatkan argumen dalam template saat mencari, kami menyatakannya dengan “??”. Saya mendapat yang berikut: 40 57 48 81 EC ??









?? ?? ?? 48 83 3D ?? ?? ?? ?? ??48 8B F9 0F 84. Untuk mengkompilasi template, kami menggunakan simpul pertama dari grafik dari Gambar 28, instruksi yang dapat ditemukan pada Gambar 27.

Buat skrip pada Lua silk_way.lua, masukkan ke dalam "... \ Steam \ steamapps \ common \ dota 2 beta \ game \ dota \ scripts \ vscripts ".

 print("SILK_WAY START") local first = Entities:First() while (first ~= nil) do local position = first:GetAbsOrigin() local strInfo = "[" .. "pos:" .. tostring(position.x) .. "," .. tostring(position.y) .. "," .. tostring(position.z) .. "]" DebugDrawText(position, strInfo, true, 300.0) first = Entities:Next(first) end print("SILK_WAY FINISH") --[[ListenToGameEvent("dota_roshan_kill",roshan_kill,nil)]] 

Script ini mem-bypass semua entitas dan menampilkan koordinat sesuai dengan posisinya.

Deklarasikan fungsi menggunakan dokumentasi di atas dan kode dekompilasi dari Gambar 29.

 typedef bool(*fDoIncludeScript)(const char*, unsigned long long); 


Panggilan fungsi.

 HRESULT WINAPI EndSceneHook(IDirect3DDevice9* pd3dDevice) { if (controller == nullptr) { controller = new GameController(); controller->SetDevice(pd3dDevice); fDoIncludeScript DoIncludeScript = (fDoIncludeScript) scanner->FindPattern("client.dll", "40 57 48 81 EC ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ?? 48 8B F9 0F 84"); DoIncludeScript("silk_way", 0); } //... } 

Setelah implementasi, kita akan melihat informasi tentang posisi entitas game. Gambar 30 - Hasil implementasi Sekarang kita dapat menjalankan skrip kita. Tetapi mereka dieksekusi di Lua, dan katakanlah peristiwa bahwa Roshan meninggal diperlukan untuk kita dalam kode C ++ (karena kita memiliki logika utama tertulis di atasnya), apa yang harus kita lakukan? Kita harus menemukan pointer ke fungsi yang diperlukan dengan cara yang sama (seperti yang kita lakukan untuk DoIncludeScript), fungsi mesin, dan fungsi lain yang menarik bagi kita menggunakan Source SDK dan Source2Gen. Tetapi lebih lanjut tentang itu di bagian selanjutnya, di mana kita akan menemukan pointer ke daftar entitas dan menulis logika lebih dekat dengan mekanisme permainan. Jika Anda ingin semuanya sekaligus, Anda dapat mencoba, saya lampirkan ini , ini , ini dan ini sebagai bantuan Anda




tautan.

11. Kesimpulan


Sebagai penutup, saya ingin mengucapkan terima kasih kepada semua orang yang berbagi praktik dan pengetahuan terbaik mereka di bidang kebalikan, berbagi pengalaman mereka dengan orang lain. Berbicara hanya tentang Dota 2 tanpa anjing peliharaan, saya akan menghabiskan banyak waktu untuk mendapatkan struktur data permainan menggunakan Cheat Engine, dan pencapaian yang dicapai bisa pecah dengan pembaruan Valve apa pun. Pembaruan memecah pointer statis yang ditemukan dan sesekali mengubah struktur entitas. Pada or75, saya melihat penggunaan fungsi DoIncludeScript dan dengan bantuannya saya menunjukkan contoh output teks menggunakan mesin game.

Dalam mengejar kesederhanaan presentasi, saya bisa kehilangan sesuatu, mengabaikan berbagai kasus yang saya anggap tidak layak diperhatikan, atau sebaliknya, mengembang penjelasan - jika pembaca yang penuh perhatian menemukan kesalahan seperti itu, saya akan senang untuk memperbaikinya dan mendengarkan komentar. Kode sumber dapat ditemukan di tautan .

Terima kasih kepada semua orang yang meluangkan waktu untuk membaca artikel.

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


All Articles