Este artículo analiza la intercepción de funciones gráficas de API utilizando el ejemplo de DirectX 9 para x64 en relación con el juego
Dota 2 .
Se describirá en detalle cómo infiltrarse en el proceso del juego, cómo cambiar el flujo de ejecución, se proporciona una breve descripción de la lógica implementada. Al final, hablaremos sobre otras características de representación que proporciona el motor.

Descargo de responsabilidad: el autor no es responsable del uso que usted haga del conocimiento adquirido en este artículo o de los daños resultantes de su uso. Toda la información presentada aquí es solo para fines educativos. Especialmente para las empresas que desarrollan MOBA para ayudarles a lidiar con los tramposos. Y, por supuesto, el autor del artículo es un conmutador de bots, un tramposo, y siempre lo ha sido.
Vale la pena explicar la última oración: estoy a favor de la competencia leal. Utilizo trucos solo como un interés deportivo, mejoro las habilidades inversas, estudio el trabajo de los anti-trampas y solo fuera de las competencias de clasificación.
1. Introducción
Este artículo está planeado como el primero de una serie y da una idea de cómo puede usar la API gráfica para sus propios fines, describe la funcionalidad requerida para comprender la siguiente parte. Planeo dedicar el segundo artículo a buscar un puntero a la lista de entidades en Source 2 (también usando Dota 2 como ejemplo) y usarlo junto con
Source2Gen para escribir lógica "adicional" (algo como
esto probablemente muestre "hack de mapa" (verifique atención a las citas, lo que está en juego se puede ver en el video) o la automatización del primer artículo). El tercer artículo está planeado en forma de escribir un controlador, comunicarse con él (IOCTL), usarlo para evitar la protección VAC (algo similar a
esto ).
2. ¿Por qué lo necesitaba?
Necesitaba el uso de la API gráfica para depurar visualmente mi bot, que escribí para Dota 2 (la información visualizada en tiempo real es muy conveniente). Soy un estudiante graduado y me dedico a la reconstrucción de cabezas 3D y a la transformación con imágenes y una cámara de profundidad; el tema es bastante interesante, pero no es mi favorito. Como he estado haciendo esto por quinto año (comenzando con el programa de maestría), entendí una cosa: sí, he estudiado esta área bastante bien, estudio fácilmente artículos con métodos y enfoques, y los implemento. Pero eso es todo, yo solo puedo optimizar el siguiente algoritmo aprendido, compararlo con los ya estudiados e implementados y decidir si usarlo en una tarea en particular. Es el final de la optimización, no es posible llegar a algo nuevo, lo cual es muy importante para la escuela de posgrado (la novedad del estudio). Empecé a pensar: si bien hay tiempo, puedes encontrar un nuevo tema. Ya necesita comprender bien el tema (en el nivel actual) o puede abordarlo rápidamente.
Al mismo tiempo, trabajé en desarrollo de juegos, y esto es probablemente lo más interesante de lo que puede hacer un programador (opinión personal) y estaba muy interesado en el tema de la IA, los bots. En ese momento, había dos temas que entendía bastante bien: luego estaba construyendo una malla de navegación dinámica (cliente-servidor) y estudiando la parte de red de un tirador dinámico. Un tema con un navegador dinámico no encajaba de inmediato: lo hice durante las horas de trabajo, tuve que pedir permiso para su uso en el diploma de la gerencia, además, el tema de la novedad estaba abierto: también estudié e implementé bien los enfoques por artículo, pero Esto no era nuevo. El tema con la parte de la red del tirador dinámico (planeé usarlo para la interacción en la realidad virtual) nuevamente se rompió tanto por el hecho de que lo estaba haciendo durante las horas de trabajo como por la novedad, puede leer una
serie de artículos de Pixonic, donde el autor mismo dice que el tema Esto es interesante, solo los enfoques se inventaron hace 30 años y no han cambiado mucho.
Alrededor de este tiempo, OpenAI lanzó su bot. Esto ciertamente no es
5 por 5 , ¡pero fue increíble! No podía arrojar pensamientos para tratar de hacer un bot y, en primer lugar, comencé a pensar en cómo usarlo como disertación, en novedad y en cómo presentarlo a un líder. Con la novedad en este sentido, todo fue mucho mejor: seguro que fue posible encontrar algo para los dos temas anteriores, pero aparentemente el bot me hizo pensar, aferrarme, desarrollarme y buscar ideas mucho más fuertes. Entonces, decidí hacer un bot 1-a-1 (una batalla en el medio, como OpenAI), presentarlo al líder, decir cuán genial es, cuántos enfoques diferentes, matemáticas y, lo más importante, el nuevo.
Lo más necesario que necesita el bot en la primera etapa es el conocimiento del entorno en el que se encuentra: tenía la intención de sacar el estado del mundo de la memoria del juego y pasé la primera etapa buscando un puntero a la Lista de entidades e integración con la creación de la oración Dog2 Source2Gen. Esto genera la estructura del motor Source2, que toma de los circuitos. La idea principal y el requisito previo para la aparición de esquemas es la replicación de estado entre el cliente y el servidor, pero aparentemente a los desarrolladores realmente les gustó la idea y la distribuyeron mucho más ampliamente, le aconsejo que lea
aquí .
Tenía experiencia en ingeniería inversa: hice trucos para Silent Storm, hice generadores clave (el más interesante fue para Black & White): lo que es keygen se puede leer desde
DrMefistO aquí , ejecución
combinada en Cabal Online (todo fue complicado por el hecho de que este juego estaba protegido por Game Guard , lo guardó del ring0 (debajo del controlador en modo kernel), ocultando el proceso (lo que al menos no facilita la infiltración), puede leer más detalles
aquí ).
En consecuencia, tuve desarrollos en esta área, el bot tuvo acceso al medio ambiente durante el tiempo planificado. Es sorprendente la cantidad de información que el servidor bunker replica a través del delta para el cliente, por ejemplo, el cliente tiene información sobre los teletransportadores, la salud y sus cambios entre los agentes (excepto Roshan, no se replica): todo esto está en la niebla de la guerra. Aunque encontré algunas dificultades, esto es de lo que voy a hablar en el próximo artículo.
Si tiene alguna pregunta sobre por qué no
utilicé Dota Bot Scripting , responderé con un extracto de la documentación:
La API está restringida de modo que los scripts no pueden hacer trampa: no se pueden consultar las unidades en FoW, no se pueden emitir comandos a las unidades que el script no controla, etc.
Esta serie de artículos está dirigida a principiantes interesados en el tema de la ingeniería inversa.
3. ¿Por qué escribo sobre esto?
Como resultado, me encontré con muchos problemas en la implementación del bot de ml, en el que dediqué suficiente tiempo para darme cuenta de que dos años antes del final de la capacitación no podía superar mi conocimiento y experiencia en el tema actual. En Dota 2, no juego desde el lanzamiento de la costumbre Dota Auto Chess, ahora paso mi tiempo libre en el diploma y en el reverso de Apex Legend (la estructura de la cual es bastante similar a Dota 2, según me parece). En consecuencia, el único beneficio del trabajo realizado es la publicación de un artículo técnico sobre este tema.
4. Dota 2
Tengo la intención de mostrar estos principios en un juego real: Dota 2. El juego utiliza el
anti- trampa
Valve Anti Cheat . Realmente me gusta Valve como empresa: productos geniales, director, actitud hacia los jugadores, Steam, Source Engine 2, ... VAC. VAC funciona desde el modo de usuario (ring3), no escanea todo y es inofensivo en comparación con otros anti-trampas (todo lo que hace esea (específicamente su anti-trampas) hace que todo el deseo de usar esta plataforma desaparezca). Estoy seguro de que VAC hace su trabajo de una manera tan moderada: no supervisa desde el modo kernel, no prohíbe el hardware (solo una cuenta), no inserta marcas de agua en las capturas de pantalla; gracias a la actitud de Valve hacia los jugadores, no instalan un antivirus completo para usted, como lo hacen Game Guard, BattlEye, Warden y otros, porque todo está pirateado y gasta los recursos del procesador que el juego podría tomar (incluso si esto se hace periódicamente), hay falsos positivos (especialmente para jugadores en computadoras portátiles). ¿No hay un hack de pared, aimbot, speed hack, ESP en PUBG, Apex, Fortnite?
En realidad, sobre Dota 2. El juego se ejecuta a una frecuencia de
40Hz (25 ms), el cliente interpola el estado del juego, no se usa la predicción de entrada; si tiene un retraso, un juego, es importante ni siquiera un juego, las unidades controladas están completamente congeladas. El servidor de mecánica del juego intercambia mensajes cifrados con el cliente a través de RUDP (UDP confiable), el cliente básicamente envía entradas (si aloja el lobby, se pueden enviar comandos), el servidor envía una réplica del mundo del juego y los equipos. La navegación se realiza en una cuadrícula 3D, cada celda tiene su propio tipo de permeabilidad. El movimiento se lleva a cabo utilizando la navegación y la física (la imposibilidad de pasar a través de la fisura de un agitador, kogi clokverka, etc.).
El estado del mundo con todas las entidades está en la memoria en su forma más pura sin encriptación: puedes estudiar la memoria del juego usando Cheat Engine. La ofuscación no se aplica a cadenas y códigos.
DirectX9, DirectX11, Vulkan, OpenGL están disponibles en la API gráfica. 5. Declaración del problema.
En el juego Dota 2 hay un "antiguo" neutral, cuyo asesinato da una buena recompensa: experiencia, oro, la capacidad de revertir los tiempos de reutilización de habilidades y objetos, Aegis (segunda vida), su nombre es Roshan. Obtener Aegis puede fundamentalmente cambiar el juego o dar una ventaja aún mayor al lado más fuerte, respectivamente, los jugadores intentan recordar / registrar el momento de su muerte para planificar cuándo reunirse y atacarlo, o estar cerca para su protección. Los diez jugadores son notificados de la muerte de Roshan, independientemente de si está oculto en la niebla de la guerra. El tiempo de reaparición tiene ocho minutos obligatorios, después de lo cual Roshan puede aparecer aleatoriamente en el intervalo de tres minutos.
La tarea es la siguiente : proporcionar al jugador información sobre el estado actual de Roshan (vivo-vivo, tiempo base ressurect_base-revives, tiempo extra ressurect_extra-revives).
Figura 1 - Condiciones para las transiciones entre estados y acciones durante la transiciónPara las condiciones en las que Roshan está muerto, muestre la hora de finalización de la estadía en este estado. La transición del estado vivo a ressurect_base debe ser realizada por el jugador en modo manual con el botón. En caso de detección / muerte de Roshan en el estado ressurect_extra (por ejemplo, un equipo enemigo se escondió secretamente en el estudio y lo mató), la transición al estado vivo / ressurect_base también se lleva a cabo manualmente usando el botón. El estado de Roshan (y el tiempo de finalización de un estado de reactivación) debe mostrarse en forma de texto, la entrada necesaria (eliminación y finalización del estado ressurect_extra) debe proporcionarse con un botón.
Figura 2 - Elementos de la interfaz: etiqueta, botón y lienzoEsta es la única tarea que se me ocurrió, de modo que no necesitaba trabajar con la memoria del juego y había al menos algún valor para el jugador, incluso para derivar características elementales, como la salud, el maná y las posiciones de las entidades, debes encontrarlas de antemano ayuda al Cheat Engine en la memoria del juego, que debe
explicarse adicionalmente
durante un tiempo bastante largo, o con la ayuda de Source2Gen, que será el próximo artículo. La declaración del problema obliga al jugador a seguir a Roshan, cambiando muchas acciones hacia él, lo cual es bastante inconveniente, pero habrá algo en lo que confiar en la segunda parte.
Escribiremos nuestro injected.dll, que contendrá la lógica empresarial basada en MVC y lo implementaremos en el proceso Dota 2. Dll utilizará nuestra biblioteca silk_way.lib, que contendrá la lógica de trampa para cambiar el flujo de ejecución, el registrador, el escáner de memoria y las estructuras de datos .
6. inyector
Cree un proyecto C ++ vacío, llame a NativeInjector. El código principal está en la función 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 función obtiene la ruta y el nombre del proceso, busca su Id por el nombre del proceso usando GetProcessIdentificator.
función 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 resumen, GetProcessIdentificator ejecuta todos los procesos en ejecución y busca un proceso con el nombre apropiado.
Figura 3 - el estado inicial del procesoA continuación, la implementación directa de la biblioteca mediante la creación de una secuencia remota.
Explicación detallada de la función InjectSegún el Id. Encontrado, el proceso se abre utilizando la función OpenProcess con los derechos para crear un hilo, recibir información del proceso, escribir y leer capacidades. La función GetModuleHandle recupera el módulo de biblioteca kernel32, esto se hace para obtener la dirección de la función LoadLibraryA que contiene la función GetProcAddress. El propósito de LoadLibrary es cargar nuestro injected.dll en el proceso especificado. Es decir, necesitamos llamar a LoadLibrary desde el proceso que nos interesa ("Dota2.exe"), para esto creamos remotamente un nuevo hilo usando CreateRemoteThread. Como puntero a la función desde la cual se inicia el nuevo hilo, pasamos la dirección de la función LoadLibraryA. Si observa la firma de la función LoadLibraryA, entonces requiere la ruta a la biblioteca cargada como argumento: HMODULE LoadLibraryA (LPCSTR lpLibFileName). Entregamos este argumento de la siguiente manera: CreateRemoteThread en los parámetros después de que la dirección de la función de inicio toma un puntero a sus parámetros, formamos un puntero a lpLibFileName escribiendo el valor en la memoria de proceso usando la función WriteProcessMemory (después de asignar memoria usando VirtualAllocEx).
Figura 4: creación de una transmisión remotaAsegúrese de cerrar el controlador de procesos al final con la función CloseHandle, también puede liberar la memoria asignada. Nuestro inyector está listo y esperando que escribamos lógica de negocios en injected.dll con la biblioteca silk_way.lib.
Figura 5 - Completando la implementación de la bibliotecaPara una mejor comprensión del principio, puede ver el
video . En conclusión, diré que es un enfoque más seguro con la implementación directa de código en el
hilo principal del proceso.
7. Camino de seda
Comencemos a implementar silk_way.lib, una biblioteca estática que contiene estructuras de datos, un registrador, un escáner de memoria y trampas. De hecho, tomé una pequeña parte de mi trabajo, algo que puede explicarse de manera más simple, que no está demasiado ligado al resto, pero al mismo tiempo resuelve el problema.
7.1. Estructuras de datos.
Brevemente sobre las estructuras de datos: Vector: lista clásica, tiempo de inserción y eliminación O (N), búsqueda O (N), memoria O (N); Cola: una cola circular, el tiempo de inserción y eliminación de O (1), sin búsqueda, memoria O (N); RBTree - árbol rojo-negro, tiempo de inserción y eliminación O (logN), búsqueda O (logN), memoria O (N). Prefiero el hash que se usa para implementar diccionarios en C # y Python, los árboles rojo-negros que usa la biblioteca estándar de C ++. La razón es que un hash es más difícil de implementar más correctamente que un árbol (aproximadamente cada medio año encuentro y pruebo variedades de hash), y generalmente un hash ocupa más memoria (aunque funciona más rápido). Estas estructuras se utilizan para crear colecciones tanto en lógica empresarial como en trampas.
Intento no usar estructuras de la biblioteca estándar y las implemento yo mismo, no importa específicamente en nuestro caso, pero es importante si tu dll se depura o el ensamblaje está en claro (esto es más probable para trampas comerciales, lo que condenamos ) Te aconsejo que escribas todas las estructuras tú mismo, esto te da más oportunidades.
Como ejemplo, si creas un juego y no quieres que los "escolares" lo escaneen con Cheat Engine, puedes hacer envoltorios para tipos primitivos y almacenar el valor
cifrado en la memoria. De hecho, esto no es una salvación, pero puede eliminar a algunos de los que están tratando de leer y cambiar la memoria del juego.
7.2. Registrador
Salida implementada a la consola y escritura en un archivo. Interfaz:
class ILogger { protected: ILogger(const char * _path) { path = path; } public: virtual ~ILogger() {} virtual void Log(const char * format, ...) = 0; protected: const char * path; };
Implementación para salida a un archivo:
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; };
La implementación para la salida a la consola es la misma. Si queremos usar el registro, es necesario definir la interfaz ILogger *, declarar el registrador necesario, llamar a la función Log con el formato requerido, por ejemplo:
ILogger* logger = new MemoryLogger(filename); logger->Log("(%llu)%s: %d\n", GetCurrentThreadId(), "EnumerateThread result", result);
7.3. Escáner
El escáner se dedica al hecho de que muestra el valor de memoria señalado por el puntero transferido y lo compara con la muestra en la memoria. La comparación funcional con el patrón se considerará más adelante.
Interfaz:
class IScanner { protected: IScanner() {} public: virtual ~IScanner() {} virtual void PrintMemory(const char * title, unsigned char * memPointer, int size) = 0; };
Implementación del archivo de encabezado:
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; };
Implementación del archivo fuente:
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); }
Para usarlo, debe definir la interfaz IScanner *, declarar el escáner deseado y llamar a la función PrintMemory, donde puede establecer el título, el puntero y la longitud, por ejemplo:
IScanner* scan = new ConsoleScanner(); scan->PrintMemory("source orig", (unsigned char*)source, 30);
7.4. Trampas
La parte más interesante de la biblioteca silk_way.lib. Los ganchos se usan para cambiar el flujo de ejecución del programa. Cree un proyecto ejecutable llamado Sandbox.
La clase Dispositivo será nuestro maniquí para investigar el funcionamiento de las trampas. 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 clase Device se hereda de la interfaz IUnknown, nuestra tarea es interceptar la llamada de las funciones Present y EndScene de cualquier instancia de Device, y llamar a las funciones originales en el receptor. No sabemos el lugar en el código donde y por qué se llaman estas funciones, en qué hilo.
Al observar las funciones Presente y EndScene, vemos que son virtuales. Se necesitan funciones virtuales para anular el comportamiento de la clase principal. Las funciones virtuales, así como las no virtuales, son un puntero a una memoria en la que se escriben códigos de operación y valores de argumentos. Dado que las funciones virtuales difieren entre herederos y padres, tienen punteros diferentes (estas son funciones completamente diferentes) y se almacenan en la Tabla de métodos virtuales (VMT). Esta tabla se almacena en la memoria y es un puntero a un puntero de clase, lo encontramos para Dispositivo:
Device* device = new Device(); unsigned long long vmt = **(unsigned long long**)&device;
VMT almacena punteros a funciones virtuales, si queremos heredar del dispositivo, el heredero contendrá su VMT. VMT almacena punteros de función secuencialmente con un paso igual al tamaño del puntero (para x86 es 4 bytes, para x64 es 8), correspondiente al orden en que se define la función en la clase. Encuentre los punteros a las funciones Present y EndScene, que se encuentran en el tercer y cuarto lugar:
typedef int (*pPresent)(Device*); typedef void (*pEndScene)(Device*, int j); pPresent ptrPresent = nullptr; pEndScene ptrEndScene = nullptr; int main() {
También es importante que el puntero al método de clase contenga el primer argumento como referencia a la instancia de clase. En C ++, C #, esto está oculto para nosotros, y el compilador lo sabe: en Python self se indica explícitamente en el primer parámetro del método de clase. Más sobre la convención de llamadas
aquí , debe buscar esta llamada.
Considere la instrucción e9 ff 3a fd ff: aquí e9 es un código de operación (con mnemónicos JMP) que le dice al procesador que cambie el puntero a la instrucción (EIP para x86, RIP para x64), salte de la dirección actual a FFFD3AFF (4294785791). También vale la pena señalar que en la memoria los números se almacenan "viceversa". Las funciones tienen un prólogo y un epílogo y se almacenan en la sección .code. Veamos qué se almacena con el puntero a la función Presente usando el escáner:
IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30);
En la consola vemos:
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
Para comprender el conjunto de estos códigos, puede mirar la
tabla o utilizar los desensambladores disponibles. Tomaremos un desensamblador listo para
usar -
hde (motor de desensamblador de hackers). También puede ver
distorm y
capstone para comparar. Pase un puntero a una función a cualquier desensamblador y dirá qué códigos de operación utiliza, los valores de los argumentos, etc.
7.4.1 Gancho de código de operación
Ahora estamos listos para ir directamente a las trampas. Examinaremos el gancho de Opcode y el punto de interrupción de hardware. Las
trampas más
comunes que recomiendo implementar y explorar.
Probablemente la trampa más utilizada y simple es el gancho Opcode (en el artículo que enumera las trampas, se llama parcheado de bytes); tenga en cuenta que es fácil de reconocer por anti-trampa cuando se usa incorrectamente (sin entender cómo funciona la anti-trampa, sin saber qué área y sección de memoria escanea en el momento actual y otras cosas, la prohibición no se ralentizará para esperar). Cuando se usa con habilidad, esta es una gran trampa, rápida y fácil de entender.
Si mientras lee un artículo está jugando código simultáneamente y está en modo de depuración, cambie a Liberar, esto es importante.
Entonces, permítanme recordarles que necesitamos interceptar la ejecución de las funciones Present y EndScene.
Implementamos interceptores, funciones donde queremos transferir el control:
int PresentHook(Device* device) { cout << "PresentHook" << endl; return 1; } void EndSceneHook(Device* device, int j) { cout << "EndSceneHook" << " " << j << endl; }
Pensemos en las abstracciones que necesitamos. Necesitamos una interfaz que nos permita establecer una trampa, eliminarla y proporcionar información al respecto. La información sobre la trampa debe contener un puntero a la función interceptada, el receptor y las funciones de trampolín (el hecho de que interceptemos la función no significa que ya no sea necesaria, también queremos poder usarla; el trampolín ayudará a llamar a la función interceptada original).
#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; };
La interfaz IHook nos brinda tales capacidades. Queremos que cuando cualquier instancia de la clase Device llame a las funciones Present y EndScene (es decir, el puntero RIP vaya a estas direcciones), nuestras funciones PresentHook y EndSceneHook se ejecuten en consecuencia.
Imagine visualmente cómo la función interceptada, el receptor y el trampolín se encuentran en la memoria (sección .code) en el momento en que el control ingresa a la función interceptada:
Figura 6 - El estado inicial de la memoria, la ejecución entra en la función interceptadaAhora queremos que el RIP (flecha roja) vaya desde el origen hasta el comienzo del destino. Como hacerlo Como ya se indicó anteriormente, la memoria fuente contiene un código de operación que el procesador ejecutará cuando la ejecución llegue a la fuente. En esencia, necesitamos saltar de una parte a otra, redirigir el puntero RIP. Como habrás adivinado, hay un código de operación que te permite transferir el control desde la dirección actual a la deseada, se llama a esta mnemotecnia JMP.
Puede saltar directamente a la dirección deseada, o en relación con la dirección actual, estos saltos se pueden encontrar en la placa: ff y e9, respectivamente. Cree estructuras para estas instrucciones:
#pragma pack(push, 1)
La instrucción de salto relativa es más corta, pero hay una limitación: unsigned int dice que puedes saltar dentro de 4,294,967,295, lo que no es suficiente para x64.
En consecuencia, la dirección de la función de destino del receptor de destino puede superar fácilmente este valor y estar fuera del int sin signo, lo cual es bastante posible para el proceso x64 (para x86 todo es mucho más simple y puede limitarse a este salto muy relativo para implementar el gancho Opcode Hook). Un salto directo toma 14 bytes, para comparación, un salto relativo es solo 5 (empaquetamos las estructuras, presta atención al paquete #pragma (push, 1)).
Necesitamos reescribir el valor en origen a una de estas instrucciones de salto.
Antes de capturar una función, debe estudiarla: la forma más fácil de hacerlo es con un depurador (más adelante le mostraré cómo hacerlo con x64dbg) o con un desensamblador. Para el presente, ya enviamos 30 bytes desde su comienzo, la instrucción 48 89 4c 24 8 ocupa 5 bytes.
Implementemos un salto relativo. Me gusta más esta opción debido a la duración de la instrucción. La idea es esta: reemplazamos los primeros 5 bytes de la función original, preservando los bytes modificados, los reemplazamos con un salto relativo a la dirección de instrucción, que se encuentra dentro del int sin signo.
Figura 7: los 5 bytes de origen de la función de origen se reemplazan por un salto relativo¿Qué nos da un salto a la memoria asignada (región púrpura), cómo nos acercamos a transferir el control al destino con esta acción? En la memoria asignada por nosotros, hay un salto directo, que moverá RIP al destino.
Figura 8 - Cambio del RIP a la función del receptorQueda por descubrir cómo llamar a la función atrapada. Necesitamos ejecutar las instrucciones atascadas e iniciar la ejecución desde la parte intacta de la fuente. Procedemos de la siguiente manera: guarde las instrucciones dañadas al comienzo del trampolín, recuerde cuántos bytes se dañaron y salte directamente a source + corruptenLen, a las instrucciones "saludables".
Ejecución de instrucciones guardadas borradas por un salto relativo:
Figura 9: uso de un trampolín para llamar a una función interceptadaEjecución adicional de instrucciones que no afectaron el macerado:
Figura 10 - Continuación de la ejecución de instrucciones de la función interceptadaCódigo que implementa la idea descrita anteriormente 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,
Explicación de la función SetHookSe crea un registro que almacena información sobre la trampa, luego de lo cual el registro se agrega a la colección. Las instrucciones se rastrean desde el comienzo de la dirección de origen hasta que la instrucción de salto relativa se puede ingresar por completo (5 bytes), las instrucciones atascadas se copian en la reserva y se recuerda su longitud.
Un punto muy importante es que necesitamos asignar memoria para el trampolín y el relé, en el que almacenaremos instrucciones para redirigir la transmisión desde el origen al destino y la dirección de esta memoria debe estar dentro de los límites a los que un salto relativo puede permitirse saltar (sin firmar) int).
Esta funcionalidad implementa la función 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; }
La idea es simple: iremos desde la memoria, comenzando desde una determinada dirección (en nuestro caso, un puntero a la fuente) hacia arriba y hacia abajo hasta encontrar una pieza adecuada de tamaño libre.
Volver a la función SetHook. Copie los bytes desgastados de la fuente en la memoria asignada e inserte inmediatamente un salto directo a la fuente + corrupto para continuar la ejecución con instrucciones no dañadas.
Luego está la instalación del puntero de retransmisión, que es responsable de redirigir el hilo de ejecución al destino saltando directamente a la dirección del receptor. Al final, cambiamos la fuente: establecemos permisos de escritura en el lugar de la memoria donde se encuentra la función y reemplazamos los primeros 5 bytes con un salto relativo que conduce a la dirección de retransmisión.
Ponemos una trampa, pero también necesita poder limpiar. Romper: no construir, la idea es simple: devolveremos los bytes en mal estado de la fuente, eliminaremos el registro de la trampa de la colección y liberaremos la memoria asignada:
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; }
Probar el trabajo. Cambie inmediatamente nuestros receptores para que puedan llamar a las funciones interceptadas utilizando el trampolín:
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; }
Probamos si hicimos todo correctamente, si la memoria fluye, si todo se ejecuta correctamente. 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; } }
Funciona
También puede registrarse en x64dgb.¿Recuerdas que al principio te pedí que trabajaras en la versión Release? Ahora ve a Debug y ejecuta el programa. El programa se bloquea ... La trampa se dispara, pero un intento de llamar al trampolín genera una excepción, que dice que la dirección en la que llamamos el trampolín no se ejecuta en absoluto. ¿Qué extrañamos? ¿Cuál es el problema de la compilación de depuración? Comenzamos y miramos el código de operación de la función Presente: 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
Cuando se ejecuta en x64dbg, puede ver lo siguiente. Figura 11 - Instrucciones de compilación de depuración En Debug, el código de operación ha cambiado, ahora el compilador agrega el salto relativo e9 f4 36 0. Todas las funciones se envuelven en el salto, incluidos main y el punto de entrada a mainCRTStartup. Otro código de operación, bueno, bueno, tenía que copiarse en el trampolín, cuando se llamó al trampolín, se debería llamar a este salto relativo, luego un salto directo a la parte no dañada de la fuente. Aquí queda claro que todo se hace como lo hemos implementado, solo el salto relativo a eso y el relativo, que su ejecución desde diferentes direcciones, fuente y trampolín, expone el RIP a valores completamente diferentes.
En mi humilde experiencia, la implementación del caso de salto relativo cubre el 99% del uso. Hay varios códigos de operación más que deben manejarse por separado. Recuerde que antes de colocar una trampa en una función, no debe ser demasiado vago y estudiarla. No lo molestaré y agregaré funcionalidad a la versión 100 por ciento (una vez más, en mi humilde experiencia), si lo necesita o le interesa, puede ver cómo están organizadas esas bibliotecas y específicamente qué otros casos verifican; será fácil hacerlo si descubriste de qué se trata.Un salto relativo es bastante común, por lo que propongo implementarlo. Un salto relativo consiste en el código de operación e9 y el valor al que debe saltar en relación con la dirección actual. En consecuencia, puede averiguar dónde saltar y saltar directamente desde el trampolín con un salto directo. Incluso si nos encontramos con un nuevo salto relativo allí, ya será desde la dirección correcta.Implementación de la instalación de la trampa teniendo en cuenta el salto relativo 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,
Si el desensamblador devuelve información de que el código de operación de este comando es e9, calculamos la dirección para saltar a (ULONG_PTR ripPtr = (ULONG_PTR) pSource + context.len + (INT32) context.imm.imm32), y escribimos la dirección en el trampolín como el valor del argumento de salto directo.También noto que en un entorno de subprocesos múltiples, puede surgir una situación en la que, al momento de instalar / quitar un gancho, uno de los subprocesos puede comenzar a realizar la función que atrapamos, como resultado, el proceso se caerá. Parte de cómo lidiar con esto se describirá en Hardware Breakpoint.Si necesita una herramienta probada, desea asegurarse de que su trampa funcionará, no tiene sus propias ideas y no desea estudiar el prólogo de la función: utilice soluciones ya preparadas, por ejemplo, Microsoft ofrece su propia biblioteca Detour. No uso dichas bibliotecas y uso una solución patentada por varias razones, por lo tanto, no puedo aconsejar algo, solo puedo nombrar las bibliotecas que estudié para descubrir algo nuevo y usarlo: PolyHook , MinHook , EasyHook (especialmente si necesita ganchos en C #).7.4.2. Punto de interrupción de hardware
Opcode Hook es una trampa simple y rápida, pero no la más eficiente. Un anti-trampa puede rastrear fácilmente un cambio en una pieza de memoria, pero el gancho de Opcode se puede usar con respecto al anti-trampa en sí o para interceptar llamadas del sistema (por ejemplo, NtSetInformationThread) que usa. Hardware Breakpoint es una trampa que no altera la memoria del proceso. Vi hilos en foros preguntando si VAC estaba siguiendo esta trampa; las respuestas generalmente son mixtas. Personalmente, VAC no me prohibió usarlos y no restableció los registros (fue hace poco menos de seis meses, tal vez algo ha cambiado)., , VAC DR /, - , . HWBP , - , , , DR0-DR7 .
HWBP utiliza registros de procesador especiales para interrumpir la ejecución de subprocesos. Si el contexto de flujo contiene los registros DR0-DR7 establecidos de una manera determinada y RIP va a una de las cuatro direcciones almacenadas en DR0-DR3, se genera una excepción que puede capturarse, según el tipo de excepción y el estado del contexto, determine en qué dirección el control lanzó la excepción y concluya - Una trampa o no. Una limitación importante de este enfoque es que puede usar solo cuatro funciones a la vez y establecerlas por separado para cada subproceso, lo que genera inconvenientes si se establece la trampa y se crea una nueva / se recrea el subproceso anterior, lo que causa la trampa. Este no es un obstáculo especial y se rige por la intercepción de la función BaseThreadInitThunk; la restricción en el uso de 4 trampas realmente no me molestó personalmente.Si el número de ganchos es crítico para usted, mire el enfoque de PageGuard.Entonces, la tarea es la misma: estamos en el sandbox (proyecto Sandbox), es necesario interceptar los métodos de la clase Device Present y EndScene para llamar a los métodos originales. Ya tenemos una interfaz preparada para trampas: IHook, tratemos con el trabajo de los puntos de interrupción "de hierro".El principio es este: hay cuatro registros DR0-DR3 "en funcionamiento" en los que se puede escribir la dirección, dependiendo de la configuración del registro de control DR7 al intentar escribir, leer o ejecutar en la dirección especificada, se producirá una excepción con el tipo EXCEPTION_SINGLE_STEP, que debe procesarse en un controlador registrado previamente . Puede usar tanto el controlador SEH como VEH; usaremos este último, ya que tiene una prioridad más alta.Nos damos cuenta de esta idea: 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; }
¿Qué pasa en el código?Este código recibe el contexto de un subproceso específico, cuando se recibe con éxito, inserta la dirección de la función que se intercepta en un registro libre, establece el registro de control DR7. Finalmente, se establece el contexto modificado.
Con más detalle acerca de lo que son DR6 y DR7, así como el enfoque de PageGuard, puedo aconsejar a Gray Hat Python: Python Programming for Hackers and Reverse Engineers. En resumen, DR7 habilita / deshabilita el uso de un registro "en funcionamiento", incluso si alguno de los registros DR0-DR3 contiene una dirección, pero en DR7 el indicador del registro correspondiente está desactivado, el punto de interrupción no funcionará. DR7 también establece el tipo de trabajo con la dirección en la que es necesario lanzar una excepción: si se leyó la dirección, si se hizo el registro o si la dirección se utiliza para ejecutar la instrucción (estamos interesados en la última opción).Eliminar una trampa también es bastante simple y se realiza a través del registro de control 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; }
Queda por tratar con los subprocesos: la trampa debe configurarse para los subprocesos que llaman a la función interceptada. No nos preocuparemos por esto.Establecemos una trampa para todos los hilos del proceso. 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; }
El código anterior omite todos los procesos visibles y busca el proceso actual. En el proceso que se encuentra para el siguiente subproceso, obtenemos el controlador de flujo, encontramos uno de los cuatro registros libres y establecemos una trampa. Vale la pena prestar atención a las funciones Congelar y Descongelar: esto es lo que Opcode Hook habló sobre el subprocesamiento múltiple: detienen por completo la ejecución de subprocesos de este proceso (excepto el actual) para que no haya una situación en la que uno de los subprocesos ingrese a la función interceptada.Proteger hilos de llamar a una función de enlace 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() {
Necesidades similares deben implementarse en la función de eliminar la trampa.Queda por agregar un controlador de excepciones VEH. La adición y eliminación se realiza mediante las funciones AddVectoredExceptionHandler y RemoveVectoredExceptionHandler de cualquier flujo. void HardwareBPHook::SetExceptionHandler(PVECTORED_EXCEPTION_HANDLER pVecExcHandler) { pException = AddVectoredExceptionHandler(1, pVecExcHandler); } ~HardwareBPHook() { info->Clear(); delete info; RemoveVectoredExceptionHandler(pException); }
El controlador debe verificar el tipo de excepción (se necesita EXCEPTION_SINGLE_STEP), verificar la correspondencia de la dirección en la que ocurrió la excepción con lo que está en los registros y, si se encuentra dicha dirección, reorganiza el puntero RIP a la dirección del receptor. Se conserva el estado de la pila, de modo que, tras una ejecución posterior del receptor, todos los parámetros de la pila estarán intactos.Implementamos el controlador descrito en el sandbox: 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 teoría, todo está listo, ejecutamos el programa, esperando exactamente el mismo trabajo que OpcodeHook.Esto no sucede, nuestro programa se congela; más precisamente, entra constantemente en PresentHook y en el momento en que se debe llamar al trampolín, se vuelve a llamar a la función. El hecho es que el punto de interrupción "hierro" no ha desaparecido, porque cuando se llama al trampolín (que, en el caso de puntos de interrupción "hierro", indica la función original), de nuevo alarmamos la misma dirección y hacemos una excepción. La solución es la siguiente: eliminaremos el punto de interrupción cuando se encuentre en el controlador para un subproceso específico, y en el momento adecuado lo configuraremos nuevamente. El lugar de actualización elegirá el momento en que finaliza la función del receptor.Esto se implementa de la siguiente manera: en el controlador, junto con la eliminación del punto de interrupción, se agrega un comando pendiente, cuyo significado es actualizar el punto de interrupción en la secuencia especificada. El comando se ejecuta al final de la función del receptor. 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; }
Implementación de comando pendiente 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() {} }; }
Imagine visualmente el trabajo de los puntos de corte "de hierro".
Figura 12 - Estado inicialConfiguramos una trampa, agregamos un controlador VEH, esperamos que el control alcance la función fuente:
Figura 13 - Etapa de preparación para la intercepciónSe lanza una excepción, se llama a un controlador que redirige el RIP al receptor y restablece el punto de interrupción:
Figura 14 - Redirige el hilo de ejecución en el receptor de funciones.Sobre este tema se pueden terminar las trampas, la biblioteca estática silk_way.lib está lista. Desde mi propia experiencia, puedo decir que a menudo uso OpcodeHook, VMT Hook, Forced Exception Hook (probablemente la trampa más "hemorroidal"), HardwareBreakpoint y PageGuard (cuando el tiempo de ejecución no es crítico, intercepciones de una sola vez).8. Arquitectura de la lógica.
La base de la lógica se presenta en forma de MVC (model-view-controller). Todas las entidades centrales heredan de la interfaz ISilkObject.8.1. Modelo
Al desarrollar un bot en la biblioteca, primero implementé ECS (puedes leer sobre este enfoque aquí y aquí ). Cuando me di cuenta de que lanzar un bot con jugadores reales era una tarea bastante larga, escribí una simulación en la que probamos bibliotecas ml (con una cuadrícula tridimensional para la navegación (Dota 2 solo usa una cuadrícula 3D para la navegación) y física 2D simplificada para un bloque corporal). Cuando desapareció la necesidad de simulación y descubrí cómo y qué registrar, qué información recopilar durante la batalla, ECS ya no necesitaba y los modelos simplemente comenzaron a contener un diccionario de componentes (para representar algo como los chicos de SkyForge, sección "Avatares y turbas), que contenía, de hecho, envoltorios sobre estructuras de Source2Gen. Para este artículo, no transferí esta implementación para simplificar el material. El modelo contiene un esquema, en el que se almacena su descripción (este punto se simplifica y en esta implementación el modelo no se crea de acuerdo con el esquema, el esquema solo lo describe (almacena los valores predefinidos que pueden codificarse) - esto se puede comparar con el almacenamiento del contenido del juego en xml / json )Esquemáticamente, el dispositivo modelo se puede representar de la siguiente manera: Figura 15 - Representación esquemática de la implementación del modelo en código:
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; };
El esquema incluye una descripción de un modelo específico y contiene el contexto que el modelo puede usar. 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; } };
Colección de modelos y colección de esquemas. 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; } };
Así, por ejemplo, la interfaz y la implementación de un modelo que almacena el estado de Roshan parece 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. Ver, ver estado y controlador
No hay mucho que decir sobre Presentación, Estado de presentación y Controlador, la implementación es similar a la de Modelos. También consisten en esquema y contexto. Para resolver el problema de la vista, se implementan Canvas, ViewCollection, Label y Button, para los dos últimos, también se implementan los estados correspondientes a los estados en los que se encuentra Roshan.Vista esquemática
Figura 16 - Representación esquemática de la vista Representación esquemática del estado de vista
Figura 17 - Representación esquemática del estado de vista 8.3. La fabrica
Los objetos se crean utilizando la fábrica. Las fábricas usan un tipo de interfaz como clave, traduciéndolo en una cadena usando typeid (T) .raw_name (). En general, hacerlo es malo, por qué y cómo leer correctamente en Andrei Alexandrescu, Modern C ++ Design: Generic Programming. Implementación de fábrica: 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; };
Antes de usar la fábrica para construir objetos, debe registrarse.Ejemplo de registro de modelo 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); }
El esquema se puede completar de cualquier manera: puede usar json, directamente en el código.Opción para llenar el esquema para Modelos en 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": {} } } }
Opción para completar un esquema para el envío de código 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. Eventos
La vista aprende sobre el cambio en el Modelo a través de eventos. Puede obtener comentarios en métodos de clase y funciones ordinarias. #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 objeto desea informar eventos que ocurren dentro de él, debe agregar IEvent * para cada evento. Otro objeto que esté interesado en los eventos que ocurren dentro de este objeto debe crear ICallback * y pasarlo dentro de IEvent * (suscribirse al evento).Ejemplo de suscripciones que ocurren en el controlador 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 ejemplo de declaración de un evento dentro de una clase: con cada golpe del reloj (llamando al método Tick), se genera un evento StruckEvent 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; };
Los tipos primitivos básicos (SILK_INT, SILT_FLOAT, SILK_STRING, ...) se implementan en Core.h.9. DirectX 9
DirectX 9 es una de las API gráficas compatibles con Dota 2. El dispositivo es una clase heredada de IUnknown y contiene funciones virtuales. En consecuencia, después de haber recibido un puntero a una tabla de método virtual, podemos obtener punteros a las funciones que necesitamos. Las funciones de clase no virtuales no están incluidas en la tabla y están en el segmento .code, ya que son las únicas que no pueden ser anuladas. Por cierto, en OpenGL y Vulkan, interceptar las funciones del dispositivo es mucho más fácil, ya que no son virtuales y puedes obtener un puntero usando GetProcAddress (). La arquitectura DirectX 11 es más compleja que la 9, pero no mucho.Para interceptar el método de clase virtual (así como el no virtual) necesitamos una instancia de esta clase, cualquier instancia. Usando la instancia, obtenemos la tabla de métodos virtuales y obtenemos los punteros necesarios para las funciones. La forma más fácil de encontrar una instancia de una clase es crearla usted mismo.Para hacer esto, necesitamos crear un objeto con la interfaz IDirect3D9 usando la función Direct3DCreate9, y crearemos el dispositivo usando este objeto llamando al método CreateDevice. Podemos llamar a estas funciones directamente desde la biblioteca DirectX, pero para consolidar el material, las llamaremos a través de punteros. Como se puede ver en d3d9.h, Direct3DCreate9 es una función regular y se puede obtener un puntero a través de GetProcAddress (tal como lo hicimos en NativeInjector para obtener un puntero a LoadLibrary).
Figura 18 - Descripción de CreateDevice en d3d9.hCree una instancia de IDirect3D9: typedef IDirect3D9* (WINAPI *SILK_Direct3DCreate9) (UINT SDKVersion);
Usando IDirect3D9, podemos crear un dispositivo llamando a pD3D-> CreateDevice (...). Para obtener un puntero a las funciones necesarias de VMT, necesitamos encontrar el procedimiento para determinar estos métodos. Figura 19 - Búsqueda de índice para el método CreateDevice de la interfaz IDirect3D9 Obtenga el 16º índice. Además de CreateDevice, también necesitamos los métodos Release y GetAdapterDisplayMode.
Implementamos la creación del dispositivo en código 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;
Bueno, creamos el dispositivo DirectX 9, ahora necesitamos entender qué funciones se usan para renderizar la escena, qué necesitamos interceptar. Necesitamos responder a la pregunta: "¿Cómo nos muestra DirectX 9 la escena?" La función Presente se usa para mostrar la escena . También vale la pena introducir conceptos tales como el búfer frontal (un búfer que almacena lo que se muestra (acción a largo plazo) en la pantalla), el búfer trasero - contiene lo que está listo para mostrar y se está preparando para convertirse en el búfer frontal, la cadena de intercambio - en realidad un conjunto de búferes que voltear de adelante hacia atrás (DirectX 9 tiene solo 1 cadena de intercambio). Antes de llamar a Present, se llama a un par de funciones BeginScene y EndScene , donde puede modificar el búfer de respaldo.Interceptemos dos funciones (de hecho, para ejecutar la lógica de negocios, una es suficiente para nosotros): EndScene y Present. Para hacer esto, observe la ubicación de estas funciones en la clase IDirect3DDevice9 Figura 20 - Declaración de la interfaz IDirect3DDevice9 Declare punteros con las siguientes firmas de funciones:
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;
Declararemos una trampa inmediatamente con un controlador de errores, ya que HardwareBreakpoint es en realidad nuestra única opción de intercepción segura implementada que no rastrea VAC (también puede probar con Opcode Hook, pero es muy probable que su cuenta salga volando en una prohibición): 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; }
Sonar las funciones designadas de cualquiera de nuestras dos trampas: 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();
Receptores de funciones: 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; }
En Presente, cada llamada toma una captura de pantalla del búfer de la tarjeta de video (para verificación) usando la función Captura 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, 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 crea un controlador de lógica de negocios. Después de la creación, se llama a la actualización del controlador, donde se actualiza toda la lógica.Observo que ahora hemos implementado el trabajo con DirectX 9. Si queremos hacer algún tipo de mod, truco, etc., las cuatro API deben ser compatibles. Esto se justifica si el arsenal ya tiene sus bibliotecas favoritas, espacios en blanco para la interfaz de usuario, de lo contrario, puede usar otra forma: la funcionalidad que usa el motor para representar el juego.También vale la pena decir que llamar a las actualizaciones lógicas de EndScene () no es la mejor opción: puede encontrar llamadas periódicas a las funciones del motor o lógica de llamadas en su transmisión. Sin embargo, si está satisfecho con la llamada de EndScene, es mejor hacerlo con bloqueo.Ahora hemos implementado todo lo que planeamos.Recomendaciones de pruebaDirectX SDK , , DirectX 9 DirectX 11. DirectX 11, - SDK, ( , ) , , DXUT, , — , FPS .
21 — DirectX SDK StateManager.exe Ahora puede crear una cuenta falsa en Steam e inyectar injected.dll en el proceso de Dota 2. Diré de inmediato, no sé cómo es la situación actual con los puntos de interrupción "de hierro", utilizando el gancho de Opcode (la forma en que lo hacemos en el actual forma) definitivamente obtendrá una prohibición. Lo hice hace unos seis meses: no había prohibición para el punto de interrupción de hardware, no puedo decir cuál es la situación en este momento. Antes de preparar el artículo, tomé dos cuentas y probé Opcode Hook y HWBP en ellas, la primera voló a la prohibición (pasaron aproximadamente 2 semanas), la segunda no (pasaron 3 semanas). Pero aún no hay garantías de que la prohibición no será en el futuro. Entonces, no se ofenda si accidentalmente realiza una introducción desde su cuenta principal u olvida iniciar sesión en la cuenta falsa; luego, cuídese y tenga cuidado.( )
22 —
23 — Implementación en modo 1x1. Figura 24 - Inyección en una coincidencia También vale la pena mencionar que hay otra forma de renderizado: el renderizado de superficie creando una segunda ventana con el tamaño apropiado. Desafortunadamente, no pude darme cuenta de la posibilidad de usar un enfoque de superficie para el caso del modo de pantalla completa, pero el enfoque descrito en el artículo le permite implementar el renderizado en modos de pantalla completa y ventana sin ningún problema. Nuestra interfaz de usuario integrada contiene solo una etiqueta de texto y un botón implementado en DirectX 9 puro; esto es todo lo que se requiere para resolver la tarea. Puede implementar tablas complejas, menús y diagramas hermosos, en general, una interfaz de usuario de cualquier complejidad, tanto en una API pura como utilizando bibliotecas listas para usar. Por supuesto, no solo 2D.
10. Uso de las funciones del motor.
Implementar la misma funcionalidad para cada API es bastante aburrido; los desarrolladores crean envoltorios convenientes al proporcionar funciones de dibujo, interfaz de usuario, etc., que el juego usa directamente. Valve también proporciona API de Dota 2 para Javascript y Lua . Esto se hace para facilitar la vida de los moderadores y diseñadores de juegos para quienes C ++ es complicado (ni siquiera C ++ en sí, sino un uso adecuado en el contexto del motor). Aquí hay funciones para renderizar y para la lógica del juego: puede prescribir el comportamiento de la unidad, por ejemplo, seleccionar elementos, usar habilidades y más. En realidad, con la ayuda de esto, se escriben cartas personalizadas.Estaremos interesados en la función DoIncludeScript, que le permite ejecutar sus scripts en Lua y usar la API de Scripting allí. No lo usé en mi proyecto, porque no vi el valor en él, usando funciones directamente de C ++, vi la idea de usarlo con or_75 y decidí incluirlo en el artículo. Esto le presentará lo que habrá en la segunda parte y le ahorrará espacio; no tiene que explicar ciertos aspectos del depurador.Empecemos
La tarea es la siguiente: necesita encontrar un puntero a la función DoIncludeScript, que toma el nombre del script y el controlador, para estudiarlo. Buscaremos la función usando el escáner de nuestra biblioteca silk_way.lib. Las funciones, como ya hemos descubierto, están codificadas en la memoria utilizando la tabla de código de operación; examinemos esta función e intentemos identificar su patrón de almacenamiento en la memoria. Ahora el escáner no tiene la funcionalidad necesaria, necesitamos la capacidad de buscar una plantilla en la memoria del proceso.Para acelerar la búsqueda, no buscaremos un patrón en toda la memoria del proceso, sino en un módulo específico (nuestra función se encuentra en client.dll, esto se verá en el depurador y se analizará a continuación). Buscaremos el módulo usando tlHelp32 por nombre enumerando todos los módulos del proceso, para lo cual crearemos una función para encontrar el módulo en el proceso GetModuleInfo actual.Código de función 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; }
El patrón es una cadena con un valor de bytes, la omisión de un byte se indica con el símbolo "??" - por ejemplo, "j9 ?? ?? ?? ??
48 03 08 ?? f1 ff ".Analizando la cadena, por conveniencia transferiremos el patrón de la representación de la cadena a la lista de valores de caracteres sin signo, estableceremos los indicadores de bytes que se omitirán. 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) {
El núcleo de búsqueda se implementa en la función FindPattern, donde, en función de la información recibida sobre el módulo, se establecen las direcciones de inicio y finalización de la búsqueda. La función VirtualQuery solicita información sobre la memoria que se buscará, hay varios requisitos para la memoria: debe estar ocupada (será un error buscar en la memoria libre), la memoria debe ser legible, ejecutable y no contener el indicador 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; }
Ahora podemos buscar la plantilla deseada en la memoria del proceso, pero aún no sabemos qué buscar. Ejecute Steam con la cuenta falsa y abra su depurador favorito (aceptemos que para el momento de leer el artículo x64dbg es lo mismo para usted, no tengo una licencia IDA Pro paga), ejecute dota2.exe desde el directorio ... \ Steam \ steamapps \ common \ dota 2 beta \ game \ bin \ win64. En principio, no me di cuenta de que VAC no era indiferente al Cheat Engine y x64dbg, no recuerdo que al usar estas herramientas se bloqueó la cuenta. Por cierto, el depurador tiene un complemento ScyllaHide que intercepta funciones del sistema como NtCreateThreadEx, NtSetInformationThread, etc., ocultando el hecho de su trabajo, puede instalar este complemento.En cada parada (habrá 10-15), continuamos ejecutando usando Ejecutar (F9). Cuando comience el juego, veremos el menú y podremos comenzar a investigar. Después de comenzar el juego, realice una búsqueda en las líneas (Buscar-> Todos los módulos-> Referencias de cadena), configure el filtro "DoIncludeScript". Figura 25: Búsqueda de líneas en la memoria del proceso del juego Vayamos al desensamblador (pestaña CPU) haciendo doble clic en el primer resultado. Esta será nuestra dirección de inicio, ya que está en client.dll, el resto de los resultados están en server.dll y animationsystem.dll. Construimos un gráfico de llamadas desde la dirección recibida. Figura 26: Gráfico de llamadas Después de la descompilación, encontramos el punto de entrada donde se usa DoIncludeScript, el cuarto nodo del gráfico. En realidad, la función en sí.

Figura 27 - La función DoIncludeScriptGraph. Figura 28: Gráfico de llamadas de DoIncludeScript Al descompilar el uso de la función, se muestra el siguiente código y el lugar de su llamada (la descompilación se realiza desde el gráfico, no desde el desensamblador). Figura 29: Descompilación de una llamada a la función DoIncludeScript. Compongamos una plantilla a partir de las instrucciones de la Figura 27 de la llamada a la función DoIncludeScript. Los argumentos pueden cambiar, respectivamente, queremos omitir los argumentos en la plantilla al buscar, los denotamos con “??”. Tengo lo siguiente: 40 57 48 81 EC ??

?? ?? ??
48 83 3D ?? ?? ?? ?? ??
48 8B F9 0F 84. Para compilar la plantilla, utilizamos el primer nodo del gráfico de la Figura 28, cuyas instrucciones se pueden encontrar en la Figura 27.Cree un script en Lua silk_way.lua, póngalo en "... \ 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")
Este script omite todas las entidades y muestra las coordenadas de acuerdo con su posición.Declare la función utilizando la documentación anterior y el código descompilado de la Figura 29. typedef bool(*fDoIncludeScript)(const char*, unsigned long long);
Llamada de función. 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); }
Después de la implementación, veremos información sobre la posición de las entidades del juego. Figura 30: resultado de la implementación Ahora podemos ejecutar nuestros scripts. Pero se ejecutan en Lua, y digamos que el evento de que Roshan murió es necesario para nosotros en el código C ++ (ya que tenemos la lógica principal escrita en él), ¿qué debemos hacer? Tendremos que encontrar punteros a las funciones necesarias de la misma manera (como lo hicimos para DoIncludeScript), funciones del motor y otras funcionalidades de interés para nosotros usando Source SDK y Source2Gen. Pero más sobre eso en la siguiente parte, donde encontraremos un puntero a una lista de entidades y escribiremos una lógica más cercana a la mecánica del juego. Si quieres todo a la vez, puedes intentarlo. Adjunto esto , esto , esto y esto como tu ayuda.
enlaces.11. Conclusión
En conclusión, me gustaría agradecer a todos los que comparten sus mejores prácticas y conocimientos en el campo de la inversión, compartiendo su experiencia con los demás. Hablando solo de Dota 2 sin un perro de oración, habría matado mucho tiempo para obtener la estructura de datos del juego usando el Cheat Engine, y los logros logrados podrían romperse con cualquier actualización de Valve. Las actualizaciones rompen los punteros estáticos encontrados y ocasionalmente cambian la estructura de las entidades. En or75, vi el uso de la función DoIncludeScript y con su ayuda mostré un ejemplo de salida de texto usando el motor del juego.En aras de la simplicidad de la presentación, podría pasar por alto algo, omitir varios casos que considero indignos de atención, o viceversa, inflar la explicación: si un lector atento encuentra tales errores, me complacerá corregirlos y escuchar los comentarios. El código fuente se puede encontrar en el enlace .Gracias a todos los que se tomaron el tiempo de leer el artículo.