Visualisation du temps de renaissance de Roshan

Cet article traite de l'interception de fonctions API graphiques à l'aide de l'exemple de DirectX 9 pour x64 par rapport au jeu Dota 2 .

Il sera décrit en détail comment infiltrer le processus de jeu, comment changer le flux d'exécution, une brève description de la logique implémentée est donnée. Au final, nous parlerons des autres fonctionnalités de rendu fournies par le moteur.



Avertissement: L'auteur n'est pas responsable de votre utilisation des connaissances acquises dans cet article ou des dommages résultant de leur utilisation. Toutes les informations présentées ici sont uniquement à des fins éducatives. Surtout pour les entreprises développant MOBA pour les aider à faire face aux tricheurs. Et, bien sûr, l'auteur de l'article est un pilote de bot, un tricheur, et il l'a toujours été.
La dernière phrase mérite d'être expliquée - je suis pour une concurrence loyale. J'utilise les tricheurs uniquement par intérêt sportif, améliore les compétences inverses, étudie le travail des anti-tricheurs et uniquement en dehors des compétitions de classement.

1. Introduction


Cet article est prévu comme le premier d'une série et donne une idée de la façon dont vous pouvez utiliser l'API graphique à vos propres fins, décrit les fonctionnalités requises pour comprendre la partie suivante. Je prévois de consacrer le deuxième article à la recherche d'un pointeur sur la liste des entités dans Source 2 (en utilisant également Dota 2 comme exemple) et à l'utiliser en conjonction avec Source2Gen pour écrire une logique «supplémentaire» (quelque chose comme ça affichera très probablement «pirater la carte» (vérifier attention aux citations, ce qui est en jeu peut être vu dans la vidéo), ou automatisation du premier article). Le troisième article est prévu sous la forme d'écrire un pilote, de communiquer avec lui (IOCTL), de l'utiliser pour contourner la protection VAC (quelque chose de similaire à cela ).

2. Pourquoi en ai-je besoin


J'avais besoin de l'utilisation de l'API graphique pour déboguer visuellement mon bot, que j'ai écrit pour Dota 2 (les informations visualisées en temps réel sont très pratiques). Je suis un étudiant diplômé et je suis engagé dans la reconstruction de têtes 3D et le morphing à l'aide d'images et d'une caméra de profondeur - le sujet est assez intéressant, mais pas mon préféré. Depuis que je fais ça depuis la cinquième année (en commençant par le programme de maîtrise), j'ai compris une chose - oui, j'ai bien étudié ce domaine, j'étudie facilement des articles avec des méthodes et des approches, et je les mets en œuvre. Mais c’est tout, je ne peux moi-même qu’optimiser le prochain algorithme appris, le comparer avec ceux déjà étudiés et mis en œuvre et décider de l’utiliser dans une tâche particulière. C'est la fin de l'optimisation, il n'est pas possible de trouver quelque chose de nouveau moi-même, ce qui est très important pour les études supérieures (la nouveauté de l'étude). J'ai commencé à penser - alors qu'il reste du temps, vous pouvez trouver un nouveau sujet. Vous devez déjà bien comprendre le sujet (au niveau actuel) ou vous pouvez le retirer rapidement.

En même temps, j'ai travaillé dans le développement de jeux, et c'est probablement le plus intéressant de ce qu'un programmeur peut faire (opinion personnelle) et j'étais très intéressé par le sujet de l'IA, les bots. À cette époque, il y avait deux sujets que je comprenais très bien - puis je construisais un maillage de navigation dynamique (client-serveur) et j'étudiais la partie réseau d'un jeu de tir dynamique. Un sujet avec un navigateur dynamique ne cadrait pas tout de suite - je l'ai fait pendant les heures de travail, j'ai dû demander à la direction l'autorisation de l'utiliser dans mon diplôme, d'ailleurs, le sujet de nouveauté était ouvert - j'ai également étudié et mis en œuvre les approches existantes bien par article, mais ce n'était pas nouveau. Le sujet avec la partie réseau du jeu de tir dynamique (je prévoyais de l'utiliser pour l'interaction en réalité virtuelle) est à nouveau tombé en panne à la fois sur le fait que je le faisais pendant les heures de travail, et sur la nouveauté, vous pouvez lire une série d'articles de Pixonic, où l'auteur lui-même dit que le sujet c'est intéressant, seules les approches ont été inventées il y a 30 ans et n'ont pas beaucoup changé.

A cette époque, OpenAI a sorti son bot. Ce n'est certainement pas 5 par 5 , mais c'était génial! Je ne pouvais pas jeter des idées pour essayer de faire un bot et tout d'abord j'ai commencé à réfléchir à la façon de l'utiliser comme dissertation, à la nouveauté et à la présenter à un leader. Avec la nouveauté à cet égard, tout allait beaucoup mieux - c'était sûr qu'il était possible de trouver quelque chose pour les deux sujets précédents, mais apparemment, le bot m'a fait réfléchir, m'accrocher, développer et rechercher des idées beaucoup plus fortes. J'ai donc décidé de créer un bot 1 contre 1 (une bataille au milieu, comme OpenAI), de le présenter au leader, de dire à quel point c'est cool, combien d'approches différentes, les mathématiques et, surtout, la nouvelle.

La chose la plus nécessaire dont le bot a besoin à la première étape est la connaissance de l'environnement dans lequel il se trouve - j'avais l'intention de prendre l'état du monde de la mémoire du jeu et j'ai passé la première étape à chercher un pointeur vers la liste des entités et à l'intégrer avec l'idée originale de la prière Dog2 Source2Gen - cette chose génère la structure du moteur Source2, qu'elle prend des circuits. L'idée principale et la condition préalable à l'émergence de schémas est la réplication de l'état entre le client et le serveur, mais apparemment les développeurs ont vraiment aimé l'idée et ils l'ont distribuée beaucoup plus largement, je vous conseille de lire ici .

J'ai eu une expérience d'ingénierie inverse: j'ai fait des tricheurs pour Silent Storm, fait des générateurs de clés (le plus intéressant était pour Black & White) - ce qui est keygen peut être lu à partir de DrMefistO ici , l'exécution de combo dans Cabal Online (tout était compliqué par le fait que ce jeu était protégé par Game Guard , l'a gardé de ring0 (sous le pilote en mode noyau), cachant le processus (ce qui au moins ne facilite pas l'infiltration) - plus de détails peuvent être lus ici ).
En conséquence, j'ai eu des développements dans ce domaine, le bot a eu accès à l'environnement pour la durée prévue. Il est étonnant de voir combien d'informations le serveur de bunker réplique via le delta au client, par exemple, le client a des informations sur les téléporteurs, la santé et ses changements parmi les agents (sauf Roshan, il ne réplique pas) - tout cela est dans le brouillard de la guerre. Bien que j'aie rencontré quelques difficultés, c'est ce dont je vais parler dans le prochain article.
Si vous avez une question pourquoi je n'ai pas utilisé Dota Bot Scripting , je répondrai avec un extrait de la documentation:
L'API est restreinte de sorte que les scripts ne peuvent pas tricher - les unités dans FoW ne peuvent pas être interrogées, les commandes ne peuvent pas être émises vers les unités que le script ne contrôle pas, etc.
Cette série d'articles s'adresse aux débutants intéressés par le sujet de la rétro-ingénierie.

3. Pourquoi j'écris à ce sujet


En conséquence, j'ai rencontré beaucoup de problèmes dans la mise en œuvre du bot de ml, sur lequel j'ai passé suffisamment de temps pour comprendre que deux ans avant la fin de la formation, je ne pouvais pas dépasser mes connaissances et mon expérience dans le sujet actuel. Dans Dota 2, je ne joue pas depuis la sortie de la coutume Dota Auto Chess, je passe maintenant mon temps libre sur le diplôme et l'inverse d'Apex Legend (dont la structure est assez similaire à Dota 2, comme il me semble). En conséquence, le seul avantage du travail réalisé est la publication d'un article technique sur ce sujet.

4. Dota 2


Je prévois de montrer ces principes sur un vrai jeu - Dota 2. Le jeu utilise l' anti- triche Valve Anti Cheat . J'aime vraiment Valve en tant qu'entreprise: produits très cool, réalisateur, attitude envers les joueurs, Steam, Source Engine 2, ... VAC. VAC fonctionne en mode utilisateur (ring3), il ne scanne pas tout et est inoffensif par rapport aux autres anti-tricheurs (de ce que fait l'easea (en particulier leur anti-triche), tout désir d'utiliser cette plateforme disparaît). Je suis sûr que VAC fait son travail avec parcimonie - il ne surveille pas le mode noyau, il n'interdit pas le matériel (uniquement un compte), il n'insère pas de filigrane dans les captures d'écran - grâce à l'attitude de Valve envers les joueurs, ils n'installent pas pour vous un antivirus à part entière, comme ils le font Game Guard, BattlEye, Warden et autres, car tout est piraté et dépense des ressources processeur que le jeu pourrait prendre (même si cela est fait périodiquement), il y a des faux positifs (en particulier pour les joueurs sur les ordinateurs portables). N'y a-t-il pas un hack de mur, aimbot, speed hack, ESP dans PUBG, Apex, Fortnite?

En fait à propos de Dota 2. Le jeu fonctionne à une fréquence de 40 Hz (25 ms), le client interpole l'état du jeu, la prédiction d'entrée n'est pas utilisée - si vous avez un décalage, un jeu - il est important même pas un jeu, les unités contrôlées - sont complètement gelés. Le serveur de mécanique de jeu échange des messages cryptés avec le client via RUDP (UDP fiable), le client envoie essentiellement des entrées (si vous hébergez le lobby, des commandes peuvent être envoyées), le serveur envoie une réplique du monde du jeu et des équipes. La navigation s'effectue sur une grille 3D, chaque cellule a son propre type de perméabilité. Le mouvement s'effectue par la navigation et la physique (impossibilité de traverser la fissure d'un shaker, kogi clokverka, etc.).

L'état du monde avec toutes les entités est en mémoire dans sa forme la plus pure sans cryptage - vous pouvez étudier la mémoire du jeu en utilisant le Cheat Engine. L'obfuscation ne s'applique pas aux chaînes et au code.

DirectX9, DirectX11, Vulkan, OpenGL sont disponibles à partir de l'API graphique.


5. Énoncé du problème


Dans le jeu Dota 2, il y a un "ancien" neutre, dont le meurtre donne une bonne récompense: l'expérience, l'or, la possibilité de faire reculer les temps de recharge des compétences et des objets, Aegis (seconde vie), son nom est Roshan. Obtenir Aegis peut fondamentalement changer la donne ou donner un avantage encore plus grand à la partie la plus forte, respectivement, les joueurs essaient de se souvenir / d'enregistrer l'heure de sa mort afin de planifier quand se réunir et l'attaquer, ou être à proximité pour sa protection. Les dix joueurs sont informés de la mort de Roshan, qu'il soit ou non caché dans le brouillard de la guerre. Le temps de réapparition a huit minutes obligatoires, après quoi Roshan peut apparaître au hasard dans l'intervalle de trois minutes.

La tâche est la suivante : fournir au joueur des informations sur l'état actuel de Roshan (vivant-vivant, ressurect_base-revives temps de base, ressurect_extra-revives temps supplémentaire).


Figure 1 - Conditions de transition entre les états et actions pendant la transition

Pour les conditions dans lesquelles Roshan est mort, affichez l'heure de fin du séjour dans cet état. La transition de l'état vivant à ressurect_base doit être effectuée par le joueur en mode manuel par le bouton. En cas de détection / mort de Roshan dans l'état ressurect_extra (par exemple, une équipe ennemie s'est faufilée secrètement dans la tanière et l'a tué), la transition vers l'état vivant / ressurect_base est également effectuée manuellement à l'aide du bouton. Le statut de Roshan (et l'heure de fin d'être dans un état de réveil) doit être affiché sous forme de texte, l'entrée nécessaire (destruction et interruption de l'état de ressurect_extra) doit être fournie avec un bouton.


Figure 2 - Éléments d'interface - étiquette, bouton et canevas

C'est la seule tâche que je pouvais imaginer pour ne pas avoir à travailler avec la mémoire du jeu et il y avait au moins une certaine valeur pour le joueur - même pour dériver des caractéristiques élémentaires, telles que la santé, le mana et les positions des entités, vous devez soit les trouver à l'avance aider le Cheat Engine dans la mémoire du jeu, qui doit en outre être expliqué pendant assez longtemps, ou avec l'aide de Source2Gen, qui sera discuté dans le prochain article. L'énoncé du problème oblige le joueur à suivre Roshan, déplaçant beaucoup d'actions vers lui, ce qui est plutôt gênant - mais il y aura quelque chose sur lequel s'appuyer dans la deuxième partie.

Nous allons écrire notre injected.dll, qui contiendra la logique métier basée sur MVC et l'implémenter dans le processus Dota 2. La DLL utilisera notre bibliothèque silk_way.lib, qui contiendra la logique d'interruption pour modifier le flux d'exécution, l'enregistreur, le scanner de mémoire et les structures de données .

6. Injecteur


Créez un projet C ++ vide, appelez NativeInjector. Le code principal se trouve dans la fonction Inject.

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

La fonction obtient le chemin et le nom du processus, recherche son ID par le nom du processus à l'aide de GetProcessIdentificator.

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


En bref, GetProcessIdentificator parcourt tous les processus en cours d'exécution et recherche un processus avec le nom approprié.


Figure 3 - l'état initial du processus

Ensuite, l'implémentation directe de la bibliothèque en créant un flux distant.

Explication détaillée de la fonction d'injection
Sur la base de l'ID trouvé, le processus est ouvert à l'aide de la fonction OpenProcess avec les droits pour créer un thread, recevoir des informations sur le processus, écrire et lire des capacités. La fonction GetModuleHandle récupère le module de bibliothèque kernel32, cela est fait pour obtenir l'adresse de la fonction LoadLibraryA contenue dans celle-ci par la fonction GetProcAddress. Le but de LoadLibrary est de charger notre injected.dll dans le processus spécifié. Autrement dit, nous devons appeler LoadLibrary à partir du processus qui nous intéresse («Dota2.exe»), pour cela, nous créons à distance un nouveau thread à l'aide de CreateRemoteThread. En tant que pointeur sur la fonction à partir de laquelle le nouveau thread démarre, nous transmettons l'adresse de la fonction LoadLibraryA. Si vous regardez la signature de la fonction LoadLibraryA, elle requiert le chemin d'accès à la bibliothèque chargée comme argument - HMODULE LoadLibraryA (LPCSTR lpLibFileName). Nous livrons cet argument comme suit: CreateRemoteThread dans les paramètres après que l'adresse de la fonction de démarrage a pris un pointeur sur ses paramètres, nous formons un pointeur sur lpLibFileName en écrivant la valeur dans la mémoire de processus à l'aide de la fonction WriteProcessMemory (après avoir alloué de la mémoire à l'aide de VirtualAllocEx).


Figure 4 - Création d'un flux distant

Assurez-vous de fermer le gestionnaire de processus à la fin avec la fonction CloseHandle, vous pouvez également libérer la mémoire allouée. Notre injecteur est prêt et attend que nous écrivions la logique métier dans injected.dll avec la bibliothèque silk_way.lib.


Figure 5 - Fin de l'implémentation de la bibliothèque

Pour une meilleure compréhension du principe, vous pouvez regarder la vidéo . En conclusion, je dirai que c'est une approche plus sûre avec l'implémentation directe de code dans le thread principal du processus.

7. Silk Way


Commençons à implémenter silk_way.lib, une bibliothèque statique qui contient des structures de données, un enregistreur, un scanner de mémoire et des interruptions. En fait, j'ai pris une petite partie de mon travail, quelque chose qui s'explique le plus simplement, qui n'est pas trop lié au reste, mais qui résout en même temps le problème.

7.1. Structures de données.


En bref sur les structures de données: vecteur - liste classique, temps d'insertion et de suppression O (N), recherche O (N), mémoire O (N); File d'attente - une file d'attente circulaire, le temps d'insertion et de suppression de O (1), pas de recherche, la mémoire O (N); RBTree - arbre rouge-noir, temps d'insertion et de suppression O (logN), recherche O (logN), mémoire O (N). Je préfère le hachage utilisé pour implémenter les dictionnaires en C # et Python, les arbres rouge-noir que la bibliothèque C ++ standard utilise. La raison en est qu'un hachage est plus difficile à mettre en œuvre correctement par rapport à un arbre (je trouve et essaie des variétés de hachage environ tous les six mois), et généralement un hachage prend plus de mémoire (bien qu'il fonctionne plus rapidement). Ces structures sont utilisées pour créer des collections à la fois dans la logique métier et dans les traps.

J'essaie de ne pas utiliser les structures de la bibliothèque standard et de les implémenter moi-même, en particulier, cela n'a pas d'importance dans notre cas, mais il est important si votre DLL est déboguée ou que l'assembly est en clair (c'est plus probable pour les tricheurs commerciaux, que nous condamnons ) Je vous conseille d'écrire vous-même toutes les structures, cela vous donne plus d'opportunités.
Par exemple, si vous créez un jeu et que vous ne voulez pas que les «écoliers» le scannent à l'aide du Cheat Engine, vous pouvez créer des wrappers pour les types primitifs et stocker la valeur chiffrée en mémoire. En fait, ce n'est pas un salut, mais cela peut éliminer certains de ceux qui essaient de lire et de changer la mémoire du jeu.

7.2. Enregistreur


Sortie implémentée sur la console et écriture dans un fichier. Interface:

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

Implémentation pour sortie dans un fichier:

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

L'implémentation pour la sortie vers la console est la même. Si nous voulons utiliser la journalisation, il est nécessaire de définir l'interface ILogger *, de déclarer le logger nécessaire, d'appeler la fonction Log au format requis, par exemple:

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

7.3. Scanner


Le scanner est engagé dans le fait qu'il affiche la valeur de mémoire pointée par le pointeur transféré et la compare avec l'échantillon en mémoire. La comparaison fonctionnelle avec le modèle sera examinée plus loin.

Interface:

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

Implémentation du fichier d'en-tête:

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

Implémentation du fichier source:

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

Pour l'utiliser, vous devez définir l'interface IScanner *, déclarer le scanner souhaité et appeler la fonction PrintMemory, où vous pouvez définir le titre, le pointeur et la longueur, par exemple:

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

7.4. Pièges


La partie la plus intéressante de la bibliothèque silk_way.lib. Les crochets sont utilisés pour modifier le flux d'exécution du programme. Créez un projet exécutable appelé Sandbox.

La classe Device sera notre mannequin pour enquêter sur le fonctionnement des pièges.
 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; }; 


La classe Device est héritée de l'interface IUnknown, notre tâche est d'intercepter l'appel des fonctions Present et EndScene de toute instance de Device, et d'appeler les fonctions d'origine dans le récepteur. Nous ne savons pas où dans le code où et pourquoi ces fonctions sont appelées, dans quel thread.

En regardant les fonctions Present et EndScene, nous voyons qu'elles sont virtuelles. Des fonctions virtuelles sont nécessaires pour remplacer le comportement de la classe parente. Les fonctions virtuelles, ainsi que les fonctions non virtuelles, sont un pointeur vers une mémoire dans laquelle les opcodes et les valeurs des arguments sont écrits. Étant donné que les fonctions virtuelles diffèrent entre les héritiers et les parents, elles ont des pointeurs différents (ce sont des fonctions complètement différentes) et sont stockées dans la table de méthode virtuelle (VMT). Cette table est stockée en mémoire et est un pointeur vers un pointeur de classe, nous le trouvons pour Device:

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

VMT stocke des pointeurs vers des fonctions virtuelles, si nous voulons hériter de Device, l'héritier contiendra son VMT. VMT stocke les pointeurs de fonction séquentiellement avec un pas égal à la taille du pointeur (pour x86, il est de 4 octets, pour x64, il est de 8), correspondant à l'ordre dans lequel la fonction est définie dans la classe. Trouvez les pointeurs vers les fonctions Present et EndScene, qui se trouvent aux troisième et quatrième endroits:

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

Il est également important que le pointeur vers la méthode de classe contienne le premier argument comme référence à l'instance de classe. En C ++, C #, cela nous est caché, et le compilateur le sait - en Python, self est explicitement indiqué par le premier paramètre de la méthode de classe. Plus d'informations sur la convention d'appel ici , vous devez rechercher cet appel.

Considérez l'instruction e9 ff 3a fd ff - ici e9 est un opcode (avec des mnémoniques JMP) qui indique au processeur de changer le pointeur sur l'instruction (EIP pour x86, RIP pour x64), passez de l'adresse actuelle à FFFD3AFF (4294785791). Il convient également de noter que les numéros de mémoire sont stockés «vice versa». Les fonctions ont un prologue et un épilogue et sont stockées dans la section .code. Voyons ce qui est stocké avec le pointeur sur la fonction Present à l'aide du scanner:

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

Dans la console, nous voyons:

 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 

Pour comprendre l'ensemble de ces codes, vous pouvez consulter le tableau ou utiliser les démonteurs disponibles. Nous prendrons un démonteur prêt à l'emploi - hde (hacker disassembler engine). Vous pouvez également regarder distorm et capstone pour comparaison. Passez un pointeur sur une fonction à n'importe quel désassembleur et il dira quels opcodes il utilise, les valeurs des arguments, etc.

7.4.1 Crochet Opcode


Maintenant, nous sommes prêts à aller directement aux pièges. Nous allons examiner le crochet Opcode et le point d'arrêt matériel. Les pièges les plus courants que je recommande de mettre en œuvre et d'explorer.

Le piège le plus utilisé et le plus simple est probablement le crochet Opcode (dans l'article répertoriant les pièges, il est appelé correction d'octets) - notez qu'il est facilement reconnu par l'anti-triche lorsqu'il est mal utilisé (sans comprendre comment fonctionne l'anti-triche, sans savoir dans quelle zone et section de mémoire il scanne le moment actuel et d'autres choses que l'interdiction ne ralentira pas pour attendre). Lorsqu'il est utilisé avec talent, c'est un grand piège, rapide et facile à comprendre.
Si, pendant la lecture d'un article, vous lisez simultanément du code et êtes en mode débogage, passez en version - c'est important.

Donc, permettez-moi de vous rappeler que nous devons intercepter l'exécution des fonctions Present et EndScene.
Nous implémentons des intercepteurs - des fonctions où nous voulons transférer le contrôle:

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

Réfléchissons aux abstractions dont nous avons besoin. Nous avons besoin d'une interface qui nous permettra de définir un piège, de le supprimer et de fournir des informations à ce sujet. Les informations sur le piège doivent contenir un pointeur sur la fonction interceptée, le récepteur et les fonctions de tremplin (le fait que nous ayons intercepté la fonction ne signifie pas qu'elle n'est plus nécessaire, nous voulons également pouvoir l'utiliser - le tremplin aidera à appeler la fonction interceptée d'origine).

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

L'interface IHook nous offre de telles capacités. Nous voulons que lorsqu'une instance de la classe Device appelle les fonctions Present et EndScene (c'est-à-dire que le pointeur RIP va vers ces adresses), nos fonctions PresentHook et EndSceneHook sont exécutées en conséquence.

Imaginez visuellement comment la fonction interceptée, le récepteur et le tremplin sont situés dans la mémoire (section .code) au moment où la commande entre dans la fonction interceptée:


Figure 6 - L'état initial de la mémoire, l'exécution passe dans la fonction interceptée

Maintenant, nous voulons que le RIP (flèche rouge) passe de la source au début de la destination. Comment faire Comme déjà indiqué ci-dessus, la mémoire source contient un opcode que le processeur exécutera lorsque l'exécution atteindra la source. Essentiellement, nous devons passer d'une partie à l'autre, rediriger le pointeur RIP. Comme vous l'avez peut-être deviné, il existe un opcode qui vous permet de transférer le contrôle de l'adresse actuelle à l'adresse souhaitée, cette mnémonique JMP est appelée.

Vous pouvez sauter directement à l'adresse souhaitée, ou par rapport à l'adresse actuelle, ces sauts peuvent être trouvés dans la plaque - ff et e9, respectivement. Créez des structures pour ces instructions:

 #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) 

L'instruction de saut relatif est plus courte, mais il y a une limitation - un entier non signé indique que vous pouvez sauter dans 4 294 967 295, ce qui n'est pas suffisant pour x64.
En conséquence, l'adresse de la fonction de destination du récepteur de destination peut facilement dépasser cette valeur et être en dehors de l'int entier non signé, ce qui est tout à fait possible pour le processus x64 (pour x86, tout est beaucoup plus simple et vous pouvez vous limiter à ce saut très relatif pour la mise en œuvre du crochet Opcode). Un saut direct prend 14 octets, à titre de comparaison, un saut relatif n'est que de 5 (nous avons compressé les structures, faites attention au pack #pragma (push, 1)).

Nous devons réécrire la valeur à la source dans l'une de ces instructions de saut.
Avant d'attraper une fonction, vous devez l'étudier - la façon la plus simple de le faire est avec un débogueur (je vais vous montrer comment le faire avec x64dbg plus tard), ou avec un désassembleur. Pour Present, nous avons déjà sorti 30 octets depuis son début, l'instruction 48 89 4c 24 8 occupe 5 octets.
Implémentons un saut relatif. J'aime plus cette option en raison de la longueur de l'instruction. L'idée est la suivante: nous remplaçons les 5 premiers octets de la fonction d'origine, en préservant les octets modifiés, les remplaçons par un saut relatif à l'adresse d'instruction, qui se trouve dans l'int entier non signé.


Figure 7 - Les 5 octets source de la fonction source sont remplacés par un saut relatif

Qu'est-ce qui nous fait sauter dans la mémoire allouée (région violette), comment nous sommes-nous rapprochés du transfert du contrôle vers la destination avec cette action? Dans la mémoire allouée par nous, il y a un saut direct, qui déplacera RIP vers la destination.


Figure 8 - Passer du RIP à la fonction de récepteur

Il reste à comprendre comment appeler la fonction interceptée. Nous devons exécuter les instructions bloquées et commencer l'exécution à partir de la partie intacte de la source. Nous procédons comme suit - enregistrez les instructions endommagées au début du trampoline, rappelez-vous combien d'octets ont été endommagés et passez directement à source + corruptLen, aux instructions «saines».

Exécution des instructions enregistrées effacées par un saut relatif:


Figure 9 - Utilisation d'un tremplin pour appeler une fonction interceptée

Poursuite de l'exécution des instructions qui n'ont pas affecté le brassage:


Figure 10 - Poursuite de l'exécution des instructions de la fonction interceptée

Code mettant en œuvre l'idée décrite ci-dessus
 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; } 


Explication de la fonction SetHook
Un enregistrement est créé qui stocke des informations sur l'interruption, après quoi l'enregistrement est ajouté à la collection. Les instructions sont analysées depuis le début de l'adresse source jusqu'à ce que l'instruction de saut relative puisse être complètement entrée (5 octets), les instructions bloquées sont copiées dans la réservation, leur longueur est mémorisée.
Un point très important est que nous devons allouer de la mémoire pour le tremplin et le relais, dans lesquels nous stockons des instructions pour rediriger le flux de la source vers la destination et l'adresse de cette mémoire doit être dans les limites qu'un saut relatif peut se permettre de sauter vers (non signé int).

Cette fonctionnalité implémente la fonction 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; } 

L'idée est simple - nous irons de la mémoire, en partant d'une certaine adresse (dans notre cas, un pointeur vers la source) de haut en bas jusqu'à ce que nous trouvions une pièce de taille libre appropriée.

Retour à la fonction SetHook. Copiez les octets usés de la source dans la mémoire allouée et insérez immédiatement un saut direct vers la source + corrompu pour continuer l'exécution avec des instructions intactes.

Vient ensuite l'installation du pointeur de relais, qui est responsable de la redirection du thread d'exécution vers la destination en sautant directement à l'adresse du récepteur. À la fin, nous changeons la source - nous définissons des autorisations d'écriture à l'endroit de la mémoire où se trouve la fonction et remplaçons les 5 premiers octets par un saut relatif menant à l'adresse du relais.

Nous avons installé un piège, mais il doit également pouvoir être nettoyé. Briser - pas construire, l'idée est simple - nous retournerons les octets minables de la source, supprimerons l'enregistrement de l'interruption de la collection et libérerons la mémoire allouée:

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

Test du travail. Changez immédiatement nos récepteurs afin qu'ils puissent appeler les fonctions interceptées à l'aide du tremplin:

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

Nous testons si nous avons tout fait correctement, si la mémoire coule, si tout est correctement exécuté.
 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; } } 


Ça marche.Vous pouvez également archiver x64dgb.

Rappelez-vous, au début, je vous ai demandé de travailler dans la version Release? Allez maintenant dans Debug et exécutez le programme. Le programme se bloque ... Le piège est déclenché, mais une tentative d'appeler le tremplin lève une exception, qui dit que l'adresse à laquelle nous appelons le tremplin n'est pas du tout pour l'exécution. Qu'avons-nous manqué? Quel est le problème de la construction du débogage? Nous commençons et regardons l'opcode de la fonction 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 

Lors de l'exécution dans x64dbg, vous pouvez voir ce qui suit. Figure 11 - Instructions d'assemblage de débogage Dans Debug, l'opcode a changé, maintenant le compilateur ajoute le saut relatif e9 f4 36 0. Toutes les fonctions sont encapsulées dans le saut, y compris principal et le point d'entrée à mainCRTStartup. Un autre opcode, eh bien, ok, il a dû être copié sur le tremplin, lorsque le tremplin a été appelé, ce saut relatif devrait être appelé, puis un saut direct sur la partie intacte de la source. Ici, il devient clair que tout est fait comme nous l'avons mis en œuvre, seul le saut relatif à cela et le relatif, que son exécution à partir d'adresses différentes, de la source et du trampoline, expose RIP à des valeurs complètement différentes.








Dans mon humble expérience, la mise en œuvre de l'étui de saut relatif couvre 99% de l'utilisation. Il existe plusieurs autres opcodes qui doivent être traités séparément. N'oubliez pas qu'avant de poser un piège sur une fonction, vous ne devriez pas être trop paresseux et l'étudier. Je ne vais pas vous déranger et ajouter des fonctionnalités à la version 100% (encore une fois, dans mon humble expérience), si vous en avez besoin ou êtes intéressé, vous pouvez voir comment ces bibliothèques sont organisées et quels autres cas elles vérifient spécifiquement - ce sera facile à faire si vous avez compris de quoi il s'agit.

Un saut relatif est en effet assez courant, je propose donc de le mettre en œuvre. Un saut relatif se compose de l'opcode e9 et de la valeur à laquelle vous devez passer par rapport à l'adresse actuelle. En conséquence, vous pouvez simplement savoir où sauter et sauter directement du tremplin avec un saut direct. Même si nous y rencontrons un nouveau saut relatif, ce sera déjà à partir de la bonne adresse.

Mise en place de l'installation du piège en tenant compte du saut 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; } 


Si le désassembleur renvoie des informations indiquant que l'opcode de cette commande est e9, nous calculons l'adresse vers laquelle sauter (ULONG_PTR ripPtr = (ULONG_PTR) pSource + context.len + (INT32) context.imm.imm32), et écrivons l'adresse dans le tremplin comme valeur de l'argument de saut direct.

Je note également que dans un environnement multi-thread, une situation peut se produire lorsque, au moment de l'installation / du retrait d'un crochet, l'un des threads peut commencer à exécuter la fonction que nous interceptons - en conséquence, le processus tombera. Une partie de la façon de gérer cela sera décrite dans Hardware Breakpoint.

Si vous avez besoin d'un outil éprouvé, vous voulez être sûr que votre piège fonctionnera, vous n'avez pas vos propres idées et vous ne voulez pas étudier la fonction prologue - utilisez des solutions prêtes à l'emploi, par exemple, Microsoft propose sa propre bibliothèque Detour. Je n'utilise pas de telles bibliothèques et utilise une solution propriétaire pour un certain nombre de raisons, donc je ne peux pas conseiller quelque chose, je ne peux nommer que les bibliothèques que j'ai étudiées afin de découvrir quelque chose de nouveau et de l'utiliser: PolyHook , MinHook , EasyHook (en particulier si vous avez besoin de crochets en C #).

7.4.2. Point d'arrêt matériel


Opcode Hook est un piège simple et rapide, mais pas le plus efficace. Un anti-triche peut facilement suivre un changement dans un morceau de mémoire, mais le crochet Opcode peut être utilisé par rapport à l'anti-triche lui-même ou pour intercepter les appels système (par exemple, NtSetInformationThread) qu'il utilise. Le point d'arrêt matériel est un piège qui ne modifie pas la mémoire du processus. J'ai vu des discussions sur des forums demandant si ACC suivait ce piège - les réponses sont généralement mitigées. Personnellement, ACC ne m'a pas interdit de les utiliser et n'a pas réinitialisé les registres (c'était il y a un peu moins de six mois, peut-être que quelque chose a changé).
, , VAC DR /, - , . HWBP , - , , , DR0-DR7 .
HWBP utilise des registres de processeur spéciaux pour interrompre l'exécution du thread. Si le contexte de flux contient les registres DR0-DR7 définis d'une certaine manière et que RIP va à l'une des quatre adresses stockées dans DR0-DR3, une exception est levée qui peut être interceptée, par le type d'exception et l'état du contexte, déterminez à quelle adresse le contrôle a levé l'exception et concluez - un piège ou pas. Une limitation importante de cette approche est que vous ne pouvez utiliser que quatre fonctions à la fois et les définir séparément pour chaque thread, ce qui entraîne des inconvénients si l'interruption est définie et qu'une nouvelle est créée / l'ancien thread est recréé, ce qui provoque l'interruption. Ce n'est pas un obstacle particulier et est régi par l'interception de la fonction BaseThreadInitThunk; la restriction sur l'utilisation de 4 pièges ne m'a pas vraiment dérangé personnellement.Si le nombre de crochets est critique pour vous, regardez l'approche PageGuard.

Donc, la tâche est la même - nous sommes dans le sandbox (projet Sandbox), il est nécessaire d'intercepter les méthodes de la classe Device Present et EndScene dans lesquelles appeler les méthodes originales. Nous avons déjà une interface prête à l'emploi pour les pièges - IHook, traitons le travail des points d'arrêt "de fer".

Le principe est le suivant: il y a quatre registres DR0-DR3 «fonctionnels» dans lesquels l'adresse peut être écrite, selon le réglage du registre de contrôle DR7 lors de la tentative d'écriture, de lecture ou d'exécution à l'adresse spécifiée, une exception avec le type EXCEPTION_SINGLE_STEP se produira, qui doit être traitée dans un gestionnaire précédemment enregistré . Vous pouvez utiliser à la fois le gestionnaire SEH et VEH - nous utiliserons ce dernier, car il a une priorité plus élevée.

Nous réalisons cette idée:

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

Que se passe-t-il dans le code
, , DR7. .

Plus en détail sur ce que sont DR6 et DR7, ainsi que sur l'approche PageGuard, je peux conseiller Gray Hat Python: Programmation Python pour les pirates et les ingénieurs inversés. En bref, DR7 active / désactive l'utilisation d'un registre «de travail» - même si l'un des registres DR0-DR3 contient une adresse, mais dans DR7 le drapeau du registre correspondant est désactivé, le point d'arrêt ne fonctionnera pas. DR7 définit également le type de travail avec l'adresse à laquelle il est nécessaire de lever une exception - si l'adresse a été lue, si l'enregistrement a été effectué ou si l'adresse est utilisée pour exécuter l'instruction (nous sommes intéressés par la dernière option).

La suppression d'un piège est également assez simple et se fait via le registre de contrôle 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; } 

Il reste à gérer les threads - le piège doit être défini pour les threads qui appellent la fonction interceptée. Nous ne nous en préoccuperons pas.

Nous avons mis un piège pour tous les threads du processus.
 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; } 


Le code ci-dessus contourne tous les processus visibles et recherche le processus en cours. Dans le processus trouvé pour le thread suivant, nous obtenons le gestionnaire de flux, trouvons l'un des quatre registres libres et définissons une interruption. Il convient de prêter attention aux fonctions Freeze et Unfreeze - c'est ce que Opcode Hook a parlé de multithreading - elles arrêtent complètement l'exécution des threads de ce processus (sauf le processus actuel) afin qu'il n'y ait pas de situation lorsqu'un des threads entre dans la fonction interceptée.

Protection des threads contre l'appel d'une fonction hook
 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; } 


Des besoins similaires doivent être mis en œuvre dans la fonction de suppression du piège.

Il reste à ajouter un gestionnaire d'exceptions VEH. L'ajout et la suppression sont effectués par les fonctions AddVectoredExceptionHandler et RemoveVectoredExceptionHandler de n'importe quel flux.

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

Le gestionnaire doit vérifier le type d'exception (EXCEPTION_SINGLE_STEP est nécessaire), vérifier la correspondance de l'adresse à laquelle l'exception s'est produite avec ce qui se trouve dans les registres et, si une telle adresse est trouvée, réorganise le pointeur RIP sur l'adresse du destinataire. L'état de la pile est conservé, de sorte que lors de l'exécution ultérieure du récepteur, tous les paramètres de la pile seront intacts.

Nous implémentons le gestionnaire décrit dans le bac à sable:

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

En théorie, tout est prêt, nous exécutons le programme, en attendant exactement le même travail que OpcodeHook.
Cela ne se produit pas, notre programme se bloque - plus précisément, il va constamment dans PresentHook et au moment où le tremplin doit être appelé, la fonction est à nouveau appelée. Le fait est que le point d'arrêt «fer» n'a pas disparu, car lorsque vous appelez le tremplin (qui, dans le cas des points d'arrêt «fer», indique la fonction d'origine), nous alarmons à nouveau la même adresse et levons une exception. La solution est la suivante: nous supprimerons le point d'arrêt lorsqu'il sera trouvé dans le gestionnaire pour un thread spécifique, et au bon moment nous le redéfinirons. Le lieu de mise à jour choisira le moment où la fonction du récepteur se terminera.

Ceci est implémenté comme suit - dans le gestionnaire, en plus de supprimer le point d'arrêt, une commande en attente est ajoutée, ce qui signifie mettre à jour le point d'arrêt dans le flux spécifié. La commande s'exécute à la fin de la fonction de réception.

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

Mise en œuvre de la commande en attente
 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() {} }; } 


Imaginez visuellement le travail des points d'arrêt "de fer".


Figure 12 - État initial

Nous définissons un piège, ajoutons un gestionnaire VEH, attendons que le contrôle atteigne la fonction source:


Figure 13 - Étape de préparation de l'interception

Une exception est levée, un gestionnaire est appelé qui redirige le RIP vers le récepteur et réinitialise le point d'arrêt:


Figure 14 - Redirige le thread d'exécution sur le récepteur de fonction

Sur ce sujet, les interruptions peuvent être terminées, la bibliothèque statique silk_way.lib est prête. D'après ma propre expérience, je peux dire que j'utilise souvent OpcodeHook, VMT Hook, Forced Exception Hook (probablement le piège le plus «hémorroïdal»), HardwareBreakpoint et PageGuard (lorsque le temps d'exécution n'est pas critique, interceptions ponctuelles).

8. Architecture de la logique


La base de la logique est présentée sous forme de MVC (model-view-controller). Toutes les entités principales héritent de l'interface ISilkObject.

8.1. Modèle


Lors du développement d'un bot dans la bibliothèque, j'ai d'abord implémenté ECS (vous pouvez en savoir plus sur cette approche ici et ici ). Quand j'ai réalisé que lancer un bot avec de vrais joueurs était une tâche assez longue, j'ai écrit une simulation où nous avons testé des bibliothèques ml (avec une grille tridimensionnelle pour la navigation (Dota 2 utilise simplement une grille 3D pour la navigation) et une physique 2D simplifiée pour un bloc de corps). Lorsque le besoin de simulation a disparu et j'ai compris comment et quoi enregistrer, quelles informations collecter pendant la bataille, ECS n'était plus nécessaire et les modèles ont simplement commencé à contenir un dictionnaire de composants (pour représenter quelque chose comme les gars de SkyForge, section «Avatars et foules), qui contenait, en fait, des wrappers sur des structures de Source2Gen. Pour cet article, je n'ai pas transféré cette implémentation afin de simplifier le matériel. Le modèle contient un schéma, dans lequel sa description est stockée (ce point est simplifié et dans cette implémentation le modèle n'est pas créé selon le schéma, le schéma le décrit uniquement (stocke les valeurs prédéfinies qui peuvent être codées en dur) - cela peut être comparé au stockage du contenu du jeu dans xml / json )

Schématiquement, le modèle d'appareil peut être représenté comme suit: Figure 15 - Représentation schématique de l' implémentation du modèle dans le code:






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

Le schéma comprend une description d'un modèle spécifique et contient le contexte que le modèle peut utiliser.

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

Collection de modèles et collection de schémas
 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; } }; 


Ainsi, par exemple, l'interface et l'implémentation d'un modèle qui stocke le statut de Roshan ressemble à
 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. Afficher, afficher l'état et le contrôleur


Il n'y a rien à dire sur la présentation, l'état de la présentation et le contrôleur, l'implémentation est similaire aux modèles. Ils se composent également de schéma et de contexte. Pour résoudre le problème pour la vue, le canevas, la ViewCollection, l'étiquette et le bouton sont implémentés, pour les deux derniers, des états correspondant aux états dans lesquels Roshan est situé sont également implémentés.

Vue schématique

16 —

Représentation schématique de l'état d'affichage

17 —

8.3. L'usine


Les objets sont créés à l'aide de l'usine. Les usines utilisent un type d'interface comme clé, le traduisant en une chaîne en utilisant typeid (T) .raw_name (). En général, cela est mauvais, pourquoi et comment lire correctement chez Andrei Alexandrescu, Design C ++ moderne: programmation générique. Mise en œuvre en usine:

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

Avant d'utiliser l'usine pour construire des objets, vous devez vous inscrire.
Exemple d'enregistrement de modèle
 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); } 


Le schéma peut être rempli de n'importe quelle manière - vous pouvez utiliser json, vous pouvez directement dans le code.
Option pour remplir le schéma des modèles dans 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": {} } } } 


Option pour remplir un schéma pour la soumission de code
 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. Les événements


La vue apprend les modifications apportées au modèle par le biais d'événements. Vous pouvez obtenir des commentaires sur les méthodes de classe et les fonctions ordinaires.

 #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() {} }; 

Si un objet souhaite signaler des événements se produisant à l'intérieur, vous devez ajouter IEvent * pour chaque événement. Un autre objet qui s'intéresse aux événements se produisant à l'intérieur de cet objet doit créer ICallback * et le passer dans IEvent * (s'abonner à l'événement).
Exemples d'abonnements survenant dans le contrôleur
 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); } 


Un exemple de déclaration d'un événement à l'intérieur d'une classe - à chaque coup de l'horloge (appel de la méthode Tick), un événement StruckEvent est déclenché
 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; }; 


Les types primitifs de base (SILK_INT, SILT_FLOAT, SILK_STRING, ...) sont implémentés dans Core.h.

9. DirectX 9


DirectX 9 est l'une des API graphiques prises en charge par Dota 2. Device est une classe héritée d'IUnknown et contient des fonctions virtuelles. Par conséquent, après avoir reçu un pointeur vers une table de méthode virtuelle, nous pouvons obtenir des pointeurs vers les fonctions dont nous avons besoin. Les fonctions de classe non virtuelles ne sont pas incluses dans la table et se trouvent dans le segment .code, car ce sont les seules qui ne peuvent pas être remplacées. Soit dit en passant, dans OpenGL et Vulkan, l'interception des fonctions de périphérique est beaucoup plus facile, car elles ne sont pas virtuelles et vous pouvez obtenir un pointeur à l'aide de GetProcAddress (). L'architecture de DirectX 11 est plus complexe que 9, mais pas beaucoup.

Pour intercepter la méthode de classe virtuelle (ainsi que la méthode non virtuelle), nous avons besoin d'une instance de cette classe, n'importe quelle instance. En utilisant l'instance, nous obtenons la table des méthodes virtuelles et obtenons les pointeurs nécessaires vers les fonctions. La façon la plus simple de trouver une instance d'une classe est de la créer vous-même.

Pour ce faire, nous devons créer un objet avec l'interface IDirect3D9 à l'aide de la fonction Direct3DCreate9, et nous créerons le périphérique à l'aide de cet objet en appelant la méthode CreateDevice. Nous pouvons appeler ces fonctions directement à partir de la bibliothèque DirectX, mais afin de consolider le matériel, nous les appellerons via des pointeurs. Comme on peut le voir dans d3d9.h, Direct3DCreate9 est une fonction régulière et un pointeur vers celle-ci peut être obtenu via GetProcAddress (tout comme nous l'avons fait dans NativeInjector pour obtenir un pointeur vers LoadLibrary).


Figure 18 - Description de CreateDevice dans d3d9.h

Créez une instance d'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); 

En utilisant IDirect3D9, nous pouvons créer un périphérique en appelant pD3D-> CreateDevice (...). Pour obtenir un pointeur sur les fonctions nécessaires de VMT, nous devons découvrir la procédure pour déterminer ces méthodes. Figure 19 - Recherche d'index pour la méthode CreateDevice de l'interface IDirect3D9 Obtenez le 16e index. En plus de CreateDevice, nous avons également besoin des méthodes Release et GetAdapterDisplayMode.






Nous implémentons la création de l'appareil en code
 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; } 


Eh bien, nous avons créé le périphérique DirectX 9, maintenant nous devons comprendre quelles fonctions sont utilisées pour rendre la scène, ce que nous devons intercepter. Nous devons répondre à la question: "Comment DirectX 9 nous montre-t-il la scène?" La fonction Présent est utilisée pour afficher la scène . Il vaut également la peine d'introduire des concepts tels que le tampon avant (un tampon qui stocke ce qui est affiché (action à long terme) à l'écran), le tampon arrière - contient ce qui est prêt à être affiché et se prépare à devenir un tampon avant, une chaîne d'échange - en fait un ensemble de tampons qui basculement d'avant en arrière (DirectX 9 n'a qu'une seule chaîne de permutation). Avant d'appeler Present, deux fonctions BeginScene et EndScene sont appelées , où vous pouvez modifier le tampon arrière.

Interceptons deux fonctions (en fait, pour exécuter la logique métier, une nous suffit): EndScene et Present. Pour ce faire, examinez l'emplacement de ces fonctions dans la classe IDirect3DDevice9 Figure 20 - Déclaration de l'interface IDirect3DDevice9 Déclarez des pointeurs avec les signatures de fonction suivantes:






 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; 

Nous déclarerons immédiatement un piège avec un gestionnaire d'erreurs, car HardwareBreakpoint est en fait notre seule option d'interception sécurisée implémentée qui ne suit pas VAC (vous pouvez également tester avec Opcode Hook, mais votre compte s'envolera très probablement dans une interdiction):

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

Sonnez les fonctions désignées de l'un de nos deux pièges:

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

Récepteurs de fonction:

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

Dans Present, chaque appel prend une capture d'écran du tampon de la carte vidéo (pour vérification) à l'aide de la fonction 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 crée un contrôleur de logique métier. Après la création, la mise à jour du contrôleur est appelée, où toute la logique est mise à jour.

Je note que nous avons maintenant implémenté le travail avec DirectX 9. Si nous voulons créer une sorte de mod, de triche, etc., les quatre API doivent être prises en charge. Cela est justifié si l'arsenal a déjà vos bibliothèques préférées, des blancs pour l'interface utilisateur, sinon vous pouvez utiliser une autre façon - la fonctionnalité qui utilise le moteur pour rendre le jeu.

Il est également utile de dire que l'appel aux mises à jour logiques à partir d'EndScene () n'est pas la meilleure option - vous pouvez trouver des appels périodiques aux fonctions du moteur ou la logique d'appel dans votre flux. Si, toutefois, vous êtes satisfait de l'appel de EndScene, il est préférable de le faire avec lockstep.

Maintenant, nous avons mis en œuvre tout ce que nous avions prévu.

Recommandations de test
DirectX SDK , , DirectX 9 DirectX 11. DirectX 11, - SDK, ( , ) , , DXUT, , — , FPS .


21 — DirectX SDK StateManager.exe

Vous pouvez maintenant créer un faux compte dans Steam et injecter injected.dll dans le processus Dota 2. Je dirai tout de suite, je ne sais pas comment est la situation actuelle avec les points d'arrêt «de fer» - en utilisant le crochet Opcode (la façon dont nous le faisons dans le courant forme) vous obtiendrez certainement une interdiction. Je l'ai fait il y a environ six mois - il n'y avait pas d'interdiction pour le Hardware Breakpoint, je ne peux pas dire quelle est la situation en ce moment. Avant de préparer l'article, j'ai pris deux comptes et j'ai essayé Opcode Hook et HWBP, le premier s'est envolé pour l'interdiction (environ 2 semaines se sont écoulées), le second non (3 semaines se sont écoulées). Mais il n'y a toujours aucune garantie que l'interdiction ne sera pas à l'avenir. Alors ne soyez pas offensé si vous faites accidentellement une introduction à partir de votre compte principal ou oubliez de vous connecter au faux compte - alors prenez déjà soin de vous et faites attention.

( )

22 —


23 —

Implémentation en mode 1x1. Figure 24 - Injection dans une correspondance Il convient également de mentionner qu'il existe un autre moyen de rendu - le rendu de surface en créant une deuxième fenêtre avec la taille appropriée. Malheureusement, je ne pouvais pas réaliser la possibilité d'utiliser une approche de surface pour le cas du mode plein écran, mais l'approche décrite dans l'article vous permet d'implémenter le rendu en mode plein écran et en mode fenêtre sans aucun problème. Notre interface utilisateur intégrée ne contient qu'une étiquette de texte et un bouton implémenté sur DirectX 9 pur - c'est tout ce qui est nécessaire pour résoudre la tâche. Vous pouvez implémenter des tableaux complexes, de beaux menus et des diagrammes - en général, une interface utilisateur de toute complexité, à la fois sur une API pure et à l'aide de bibliothèques prêtes à l'emploi. Bien sûr, pas seulement en 2D.







10. Utilisation des fonctions du moteur


L'implémentation de la même fonctionnalité pour chaque API est plutôt morne; les développeurs créent des wrappers pratiques en fournissant des fonctions de dessin, d'interface utilisateur, etc., que le jeu utilise directement. Valve fournit également des API Dota 2 pour Javascript et Lua . Ceci est fait afin de faciliter la vie des modérateurs et des concepteurs de jeux pour qui le C ++ est compliqué (pas même le C ++ lui-même, mais une utilisation appropriée dans le contexte du moteur). Ici, il y a des fonctions pour le rendu et pour la logique du jeu - vous pouvez prescrire le comportement de l'unité, par exemple, sélectionner des éléments, utiliser des compétences, etc. En fait, avec l'aide de cela, des lettres personnalisées sont écrites.

Nous serons intéressés par la fonction DoIncludeScript, qui vous permet d'exécuter vos scripts sur Lua et d'y utiliser l'API de script. Je ne l'ai pas utilisé dans mon projet, car je n'y voyais pas de valeur, en utilisant des fonctions directement depuis C ++, j'ai vu l'idée de l'utiliser avec or_75 et j'ai décidé de l'inclure dans l'article. Cela vous présentera ce qui se trouvera dans la deuxième partie et économisera de l’espace; vous n’avez pas à expliquer certains aspects du débogueur.

Commençons.La tâche est la suivante: vous devez rechercher un pointeur vers la fonction DoIncludeScript, qui prend le nom du script et du gestionnaire, pour l'étudier. Nous rechercherons la fonction à l'aide du scanner de notre bibliothèque silk_way.lib. Les fonctions, comme nous l'avons déjà découvert, sont encodées en mémoire à l'aide de la table d'opcode - examinons cette fonction et essayons d'identifier son modèle de stockage en mémoire. Maintenant, le scanner n'a pas les fonctionnalités nécessaires, nous devons pouvoir rechercher un modèle dans la mémoire de processus.

Pour accélérer la recherche, nous ne rechercherons pas un modèle dans la mémoire du processus, mais dans un module spécifique (notre fonction réside dans client.dll, cela sera vu dans le débogueur et sera discuté ci-dessous). Nous rechercherons le module en utilisant tlHelp32 par nom en énumérant tous les modules du processus, pour lesquels nous créerons une fonction pour trouver le module dans le processus GetModuleInfo en cours.

Code de fonction 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; } 


Le modèle est une chaîne avec une valeur d'octets, sauter un octet est indiqué par le symbole "??" - par exemple, "j9 ?? ?? ?? ??48 03 08 ?? f1 ff ”.

En analysant la chaîne, pour plus de commodité, nous transférerons le modèle de la représentation de la chaîne vers la liste des valeurs de caractères non signées, définissons les indicateurs d'octets à ignorer.

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

Le noyau de recherche est implémenté dans la fonction FindPattern, où, en fonction des informations reçues sur le module, les adresses de début et de fin de la recherche sont définies. Les informations sur la mémoire qui sera recherchée sont demandées par la fonction VirtualQuery, il y a un certain nombre d'exigences pour la mémoire - elle doit être occupée (ce sera une erreur de rechercher dans la mémoire libre), la mémoire doit être lisible, exécutable et ne pas contenir l'indicateur 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; } 

Maintenant, nous pouvons rechercher le modèle souhaité dans la mémoire de processus, mais nous ne savons pas encore quoi rechercher. Exécutez Steam sous le faux compte et ouvrez votre débogueur préféré (convenons que pour le moment de lire l'article x64dbg est aussi pour vous - je n'ai pas de licence payante pour IDA Pro), exécutez dota2.exe dedans depuis le répertoire ... \ Steam \ steamapps \ common \ dota 2 beta \ game \ bin \ win64. En principe, je n'ai pas remarqué que VAC n'était pas indifférent à Cheat Engine et x64dbg, je ne me souviens pas qu'en utilisant ces outils, le compte était interdit. Par ailleurs, le débogueur possède un plugin ScyllaHide qui intercepte les fonctions système comme NtCreateThreadEx, NtSetInformationThread, etc., cachant le fait de son travail, vous pouvez installer ce plugin.

À chaque arrêt (il y aura 10-15), nous continuons à exécuter en utilisant Run (F9). Lorsque le jeu commencera, nous verrons le menu et nous pourrons commencer la recherche. Après avoir démarré le jeu, effectuez une recherche sur les lignes (Rechercher-> Tous les modules-> Références de chaînes), définissez le filtre «DoIncludeScript». Figure 25 - Recherche des lignes dans la mémoire du processus de jeu Passons au désassembleur (onglet CPU) en double-cliquant sur le premier résultat. Ce sera notre adresse de départ, car il s'agit de client.dll, le reste des résultats se trouve dans server.dll et animationsystem.dll. Nous construisons un graphe d'appels à partir de l'adresse reçue. Figure 26 - Graphique d'appel Après la décompilation, nous trouvons le point d'entrée où DoIncludeScript est utilisé - le quatrième nœud du graphique. En fait, la fonction elle-même.














Figure 27 - La fonction

graphique DoIncludeScript . Figure 28 - Le graphe d'appel de DoIncludeScript Décompiler l'utilisation de la fonction montre le code suivant et le lieu de son appel (la décompilation se fait depuis le graphe, pas depuis le désassembleur). Figure 29 - Décompilation d'un appel à la fonction DoIncludeScript Composons un modèle à partir des instructions de la figure 27 de l'appel à la fonction DoIncludeScript. Les arguments peuvent changer, respectivement, nous voulons ignorer les arguments dans le modèle lors de la recherche, nous les désignons par «??». J'ai obtenu ce qui suit: 40 57 48 81 EC ??









?? ?? ?? 48 83 3D ?? ?? ?? ?? ??48 8B F9 0F 84. Pour compiler le modèle, nous avons utilisé le premier nœud du graphique de la figure 28, dont les instructions se trouvent dans la figure 27.

Créez un script sur Lua silk_way.lua, mettez-le dans "... \ 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)]] 

Ce script contourne toutes les entités et affiche les coordonnées en fonction de sa position.

Déclarez la fonction à l'aide de la documentation ci-dessus et du code décompilé de la figure 29.

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


Appel de fonction.

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

Après la mise en œuvre, nous verrons des informations sur la position des entités du jeu. Figure 30 - Résultat de l'implémentation Nous pouvons maintenant exécuter nos scripts. Mais ils sont exécutés dans Lua, et disons que l'événement où Roshan est mort est nécessaire pour nous dans le code C ++ (puisque nous avons la logique principale écrite dessus), que devons-nous faire? Nous devrons trouver des pointeurs vers les fonctions nécessaires de la même manière (comme nous l'avons fait pour DoIncludeScript), les fonctions du moteur et d'autres fonctionnalités qui nous intéressent en utilisant le SDK Source et Source2Gen. Mais plus à ce sujet dans la partie suivante, où nous trouverons un pointeur vers une liste d'entités et rédigerons une logique plus proche de la mécanique du jeu. Si vous voulez tout à la fois, vous pouvez essayer, je joins ceci , ceci , ceci et cela comme votre aide




liens.

11. Conclusion


En conclusion, je voudrais remercier tous ceux qui partagent leurs meilleures pratiques et connaissances dans le domaine du reverse, partageant leur expérience avec les autres. En ne parlant que de Dota 2 sans chien de prière, j'aurais tué beaucoup de temps pour obtenir la structure de données du jeu en utilisant le Cheat Engine, et les réalisations réalisées pourraient rompre avec toute mise à jour de Valve. Les mises à jour cassent les deux pointeurs statiques trouvés et changent parfois la structure des entités. À or75, j'ai vu l'utilisation de la fonction DoIncludeScript et avec son aide, j'ai montré un exemple de sortie de texte à l'aide du moteur de jeu.

Dans un souci de simplicité de présentation, je pourrais manquer quelque chose, omettre divers cas que je jugeais indignes d'attention, ou vice versa, gonfler l'explication - si un lecteur attentif trouve de telles erreurs, je serai heureux de les corriger et d'écouter les commentaires. Le code source se trouve sur le lien .

Merci à tous ceux qui ont pris le temps de lire l'article.

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


All Articles