本文使用与
Dota 2游戏相关的DirectX 9 for x64示例讨论图形API函数的拦截。
将详细描述如何渗透游戏过程,如何改变执行流程,并给出对所实现逻辑的简要描述。 最后,我们将讨论引擎提供的其他渲染功能。

免责声明:作者对您对本文中获得的知识的使用或使用它们所造成的损害不承担任何责任。 此处提供的所有信息仅用于教育目的。 特别是对于开发MOBA来帮助他们应对作弊者的公司。 而且,当然,本文的作者是一个机器人切换器,一个骗子,他一直都是。
最后一句话值得解释-我是为了公平竞争。 我仅将作弊用作体育运动的兴趣,提高反向技巧,研究反作弊的工作,并且仅在评分比赛中使用。
1.简介
本文计划作为系列文章的第一篇,并给出了如何将图形API用于自己的目的的想法,并介绍了理解下一部分所需的功能。 我计划将第二篇文章专门用于搜索指向Source 2中实体列表的指针(也以Dota 2为例),并将其与
Source2Gen结合使用以编写“其他”逻辑(类似
这样的东西很可能会显示“ map hack”(检查)注意引号,可以在视频中看到有争议的东西,或者第一篇文章的自动化)。 第三篇文章的计划形式是编写驱动程序,与之通信(IOCTL),使用它绕过VAC保护(与
此类似)。
2.为什么我需要它
我需要使用图形API直观地调试我的机器人,这是我为Dota 2编写的(实时可视化信息非常方便)。 我是一名研究生,并且从事3D头的重建以及使用图像和深度相机进行变形的工作-这个主题很有趣,但不是我最喜欢的一个。 自从第五年(从硕士课程开始)以来,我已经了解了一件事-是的,我已经很好地研究了这一领域,我很容易用方法和途径来研究文章并加以实施。 但这就是全部,我本人只能优化下一个学习到的算法,将其与已经研究和实施的算法进行比较,并决定是否在特定任务中使用它。 优化到此结束,不可能提出新的东西,这对研究生院非常重要(这项研究的新颖性)。 我开始思考-虽然有时间,但是您可以找到一个新的话题。 您已经需要很好地理解该主题(在当前级别上),或者可以快速将其拉起。
同时,我从事游戏开发人员的工作,这可能是程序员可以做的最有趣的事情(个人观点),并且对AI机器人这一主题非常感兴趣。 那时,我很了解两个主题-然后我建立了一个动态导航网格(客户端-服务器)并研究了动态射击者的网络部分。 带有动态导航器的主题无法立即解决-我是在工作时间做到这一点的,我必须征得管理层的许可才能在文凭课程中使用,此外,新颖性主题是开放的-我也按条款对方法进行了很好的研究和实施,但是这不是什么新鲜事。 动态射击游戏的网络部分的主题(我计划将其用于虚拟现实中的交互)再次分解了我在工作时间所做的事实以及新颖性,您可以阅读Pixonic的
一系列文章 ,作者本人说,该主题有趣的是,只有30年前发明的方法并没有太大变化。
大约在这个时候,OpenAI发布了他们的机器人。 当然不是
5乘5了 ,但是太棒了! 我无法想出一个机器人的想法,首先我开始思考如何将其用作论文,新颖性以及如何将其呈现给领导者。 有了这方面的新颖性,一切都变得更好了-当然有可能针对前两个主题提出一些建议,但是很明显,该机器人使我思考,坚持,发展和寻找更强大的想法。 因此,我决定制作一个1对1机器人(像OpenAI一样在中间进行一场战斗),将其呈现给领导者,告诉它有多酷,有多少种不同的方法,数学方法,最重要的是,新方法。
该机器人在第一阶段需要的最必要的事情是了解其所处的环境-我打算从游戏的记忆中了解世界的状态,并在第一阶段中寻找指向实体列表的指针,并与Dog2 Source2Gen的创造力进行整合-这个东西产生了Source2引擎的结构,它从电路中获取。 出现方案的主要思想和前提条件是客户端和服务器之间的状态复制,但是显然,开发人员真的很喜欢这个思想,他们将其广泛分发,我建议您在
这里阅读。
我有过反向工程经验:我为《寂静风暴》作弊,制作了密钥生成器(最有趣的是《黑与白》)-可以从
DrMefistO 那里读到什么密钥生成,在Cabal Online中进行
连击 (由于该游戏受到Game Guard的保护,所以一切都变得复杂,使其免受ring0(在内核模式下处于驱动程序下)的保护,隐藏了进程(这至少使其难以渗透)-可以在
此处阅读更多详细信息。
因此,我在这方面进行了开发,该机器人在计划的时间内可以访问环境。 令人惊奇的是,replicate堡服务器通过增量复制了多少信息给客户端,例如,客户端具有有关代理之间的任何传送者,健康状况及其变化的信息(除了Roshan,他没有复制)-所有这些都在战争迷雾中。 尽管遇到了一些困难,但这是我将在下一篇文章中讨论的内容。
如果您有一个疑问,为什么我不使用
Dota Bot Scripting ,我将以文档摘录回答:
该API受到限制,以使脚本无法作弊-无法查询FoW中的单元,无法将命令发布给脚本无法控制的单元,等等。
本系列文章针对的是对逆向工程主题感兴趣的初学者。
3.我为什么要写这个
结果,我在ml的bot的实现中遇到了许多问题,我花了足够的时间意识到在培训结束前的两年,我无法超越当前主题的知识和经验。 在Dota 2中,我从Dota Auto Chess风俗发布后就不再玩游戏了,我现在将空闲时间花在Apex Legend的文凭和反向课程上(在我看来,其结构与Dota 2非常相似)。 因此,所做工作的唯一好处是发表了有关该主题的技术文章。
4. Dota 2
我打算在一个真实的游戏Dota 2中展示这些原理。该游戏使用了
防作弊的
Valve Anti Cheat 。 我真的很喜欢Valve公司:很棒的产品,导演,对球员的态度,Steam,Source Engine 2,... VAC。 VAC在用户模式(ring3)下工作,它不会扫描所有内容,并且与其他反作弊方法相比是无害的(esea所做的一切(特别是其反作弊行为都使使用此平台的所有愿望消失了))。 我确信VAC的工作方式非常谨慎-它不会从内核模式进行监视,不会禁止硬件(仅是一个帐户),不会在屏幕截图中插入水印-由于Valve对播放器的态度,它们不会为您安装完整的防病毒软件Game Guard,BattlEye,Warden和其他人,因为这一切都被黑了,而且还花费了游戏可能占用的处理器资源(即使这是定期进行的),因此存在误报(尤其是笔记本电脑上的玩家)。 在PUBG,Apex,Fortnite中是否不存在墙壁黑客,瞄准机器人,速度黑客,ESP?
实际上是关于Dota 2的游戏。游戏以
40Hz (25毫秒)的频率运行,客户端会插值游戏状态,不使用输入预测-如果您有滞后,则游戏-重要的是,甚至游戏,受控单位也不会完全冻结。 游戏机制服务器通过RUDP(可靠的UDP)与客户端交换加密的消息,客户端基本上发送输入(如果您托管大厅,则可以发送命令),服务器发送游戏世界和团队的副本。 导航在3D网格上进行,每个单元格都有其自己的通畅类型。 运动是通过导航和物理方法进行的(不可能通过振动筛,kogi clokverka等的裂缝)。
世界上所有实体的状态都以最原始的形式存在于内存中,没有加密-您可以使用作弊引擎来研究游戏的内存。 混淆不适用于字符串和代码。
可从图形API获得DirectX9,DirectX11,Vulkan,OpenGL。 5.问题陈述
在Dota 2游戏中,有一个中立的“远古时代”,杀死它可以得到很好的回报:经验,金钱,击退技能和物品冷却时间的能力,宙斯盾(第二生命),他的名字叫Roshan。 获得《宙斯盾》可以从根本上扭转游戏局面,或者让更强大的一方获得更大的优势,玩家试图记住/记录他的死亡时间,以便计划何时聚在一起攻击他,或者为了保护他而在附近。 通知Roshan死亡的所有十个玩家,无论Roshan是否被隐藏在战争迷雾中。 重生时间为强制性的八分钟,此后Roshan可能会在三分钟的间隔内随机出现。
任务如下 :向玩家提供Roshan当前状态的信息(“存活”,“ ressurect_base-复活基准时间”,“ ressurect_extra-复活额外时间”)。
图1-过渡期间状态与动作之间的过渡条件对于Roshan死亡的情况,请显示在此状态下停留的结束时间。 从活动状态到ressurect_base的转换必须由播放器在手动模式下通过按钮完成。 如果在ressurect_extra状态下检测到Roshan /死亡(例如,一个敌方团队偷偷潜入窝中并杀死了他),也可以使用按钮手动进行到live / ressurect_base状态的转换。 Roshan的状态(以及处于恢复状态的结束时间)应以文本形式显示,必要的输入(杀死和中断ressurect_extra状态)应带有一个按钮。
图2-界面元素-标签,按钮和画布这是我能完成的唯一任务,因此我不需要处理游戏的记忆,并且至少对玩家有一定的价值-即使要得出任何基本特征,例如健康,法力和实体位置,您也需要事先找到它们帮助游戏中的作弊引擎,需要在相当长的时间内进行额外
说明 ,或者在Source2Gen的帮助下进行解释,这将在下一篇文章中进行介绍。 问题的陈述迫使玩家跟随Roshan,将很多动作转移给他,这很不方便-但在第二部分中将需要依靠。
我们将编写我们的Injection.dll,它将包含基于MVC的业务逻辑并在Dota 2流程中实现它。Dll将使用我们的Silk_way.lib库,该库将包含陷阱逻辑以更改执行流,记录器,内存扫描器和数据结构。
6.喷油器
创建一个空的C ++项目,调用NativeInjector。 主要代码在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); }
该函数获取进程的路径和名称,并使用GetProcessIdentificator通过进程的名称搜索其ID。
函数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; }
简而言之,GetProcessIdentificator运行所有正在运行的进程,并查找具有适当名称的进程。
图3-流程的初始状态接下来,通过创建远程流直接实现该库。
进样功能的详细说明根据找到的ID,使用具有创建线程,接收进程信息,写入和读取功能的权限的OpenProcess函数打开该进程。 GetModuleHandle函数检索kernel32库模块,这样做是为了获取GetProcAddress函数包含在其中的LoadLibraryA函数的地址。 LoadLibrary的目的是将我们的injection.dll加载到指定的进程中。 也就是说,我们需要从我们感兴趣的进程(“ Dota2.exe”)中调用LoadLibrary,为此,我们使用CreateRemoteThread远程创建一个新线程。 作为指向新线程启动的函数的指针,我们传递了LoadLibraryA函数的地址。 如果查看LoadLibraryA函数的签名,则它需要将已加载库的路径作为参数-HMODULE LoadLibraryA(LPCSTR lpLibFileName)。 我们按以下方式传递此参数:在start函数的地址获取指向其参数的指针之后,在参数中创建CreateRemoteThread,通过使用WriteProcessMemory函数将值写入进程内存(使用VirtualAllocEx分配内存之后),我们形成指向lpLibFileName的指针。
图4-创建远程流确保最后使用CloseHandle函数关闭进程处理程序,也可以释放分配的内存。 我们的注射器已经准备就绪,正在等待我们使用silk_way.lib库在Injection.dll中编写业务逻辑。
图5-完成库的实现为了更好地理解该原理,您可以观看
视频 。 最后,我要说的是
,在过程的
主线程中直接执行代码是一种更安全的方法。
7.丝绸之路
让我们开始实现Silk_way.lib,这是一个静态库,其中包含数据结构,记录器,内存扫描器和陷阱。 实际上,我只花了我一小部分的工作,这是最容易解释的事情,虽然与其他部分并不太紧密,但同时可以解决问题。
7.1。 数据结构。
简要介绍一下数据结构:向量-经典列表,插入和删除时间O(N),搜索O(N),内存O(N); 队列-循环队列,插入和删除的时间为O(1),无搜索,内存为O(N); RBTree-红黑树,插入和删除时间O(logN),搜索O(logN),内存O(N)。 我更喜欢用于在C#和Python(标准C ++库使用的红黑树)中实现字典的哈希。 原因是散列比树更难以正确实现(大约每半年我会发现并尝试各种散列),并且散列通常占用更多内存(尽管它工作得更快)。 这些结构用于在业务逻辑和陷阱中创建集合。
我尝试不使用标准库中的结构,而是由我自己实现,在我们的情况下,这并不重要,但是重要的是,如果您的dll被调试或组装清晰(这很可能是出于商业目的,我们对此表示谴责) ) 我建议您自己编写所有结构,这会给您带来更多机会。
例如,如果您制作游戏并且不希望“学童”使用作弊引擎对其进行扫描,则可以为原始类型制作包装并将
加密后的值存储在内存中。 实际上,这不是救赎,但它可以淘汰一些尝试阅读和更改游戏记忆的人。
7.2。 记录仪
实现输出到控制台并写入文件。 介面
class ILogger { protected: ILogger(const char * _path) { path = path; } public: virtual ~ILogger() {} virtual void Log(const char * format, ...) = 0; protected: const char * path; };
输出到文件的实现:
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; };
输出到控制台的实现是相同的。 如果要使用日志记录,则必须定义ILogger *接口,声明必要的记录器,以所需的格式调用Log函数,例如:
ILogger* logger = new MemoryLogger(filename); logger->Log("(%llu)%s: %d\n", GetCurrentThreadId(), "EnumerateThread result", result);
7.3。 扫描仪
扫描仪会进行以下操作:显示由传输的指针指向的内存值,并将其与内存中的样本进行比较。 与模式的功能比较将在以后考虑。
介面
class IScanner { protected: IScanner() {} public: virtual ~IScanner() {} virtual void PrintMemory(const char * title, unsigned char * memPointer, int size) = 0; };
头文件的实现:
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; };
源文件实现:
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); }
要使用它,您需要定义IScanner *接口,声明所需的扫描仪并调用PrintMemory函数,您可以在其中设置标题,指针和长度,例如:
IScanner* scan = new ConsoleScanner(); scan->PrintMemory("source orig", (unsigned char*)source, 30);
7.4。 陷阱
silk_way.lib库中最有趣的部分。 挂钩用于更改程序执行的流程。 创建一个名为Sandbox的可执行项目。
Device类将是我们用来调查陷阱操作的虚拟对象。 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; };
Device类是从IUnknown接口继承的,我们的任务是拦截Device的任何实例的Present和EndScene函数的调用,并在接收器中调用原始函数。 我们不知道在代码中在哪个线程中何处以及为何调用这些函数。
查看Present和EndScene函数,我们看到它们是虚拟的。 需要虚拟函数来覆盖父类的行为。 虚拟函数和非虚拟函数都是指向其中写入操作码和参数值的内存的指针。 由于继承人和父母之间的虚拟功能不同,因此它们具有不同的指针(它们是完全不同的功能),并存储在虚拟方法表(VMT)中。 该表存储在内存中,并且是指向类指针的指针,我们在Device中找到它:
Device* device = new Device(); unsigned long long vmt = **(unsigned long long**)&device;
VMT存储指向虚拟函数的指针,如果我们要从Device继承,则继承人将包含其VMT。 VMT按等于指针大小的步长顺序存储函数指针(对于x86,它是4个字节,对于x64,它是8个字节),对应于在类中定义函数的顺序。 找到位于第三和第四位的Present和EndScene函数的指针:
typedef int (*pPresent)(Device*); typedef void (*pEndScene)(Device*, int j); pPresent ptrPresent = nullptr; pEndScene ptrEndScene = nullptr; int main() {
同样重要的是,指向类方法的指针必须包含第一个参数,作为对类实例的引用。 在C ++,C#中,这对我们是隐藏的,编译器知道这一点-在Python中,self是由class方法中的第一个参数明确表示的。 有关
此处的调用约定的更多信息,您需要查找此调用。
考虑指令e9 ff 3a fd ff-这里的e9是一个操作码(带有JMP助记符),它告诉处理器将指针更改为指令(对于x86为EIP,对于x64为RIP),从当前地址跳转到FFFD3AFF(4294785791)。 还值得注意的是,在内存中存储的数字“反之亦然”。 函数有一个序言和一个结尾,并存储在.code节中。 让我们看看使用扫描器使用指向Present函数的指针存储的内容:
IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30);
在控制台中,我们看到:
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
要了解这些代码的集合,可以查看
表格 ,或使用可用的反汇编程序。 我们将使用现成的反汇编程序
-hde (黑客反汇编程序引擎)。 您也可以查看
distorm和
顶点以进行比较。 将指向函数的指针传递给任何反汇编程序,它将说明其使用的操作码,参数的值,等等。
7.4.1操作码挂钩
现在我们准备直接进入陷阱。 我们将研究操作码挂钩和硬件断点。 我建议实施和探索的最
常见陷阱 。
可能最常用和最简单的陷阱是Opcode Hook(在列出陷阱的文章中,它称为字节修补)-请注意,如果误用它,它很容易被反作弊识别(无需了解反作弊的工作原理,而无需知道扫描的内存区域和部分。当前时刻和其他禁令将不会放慢脚步等待)。 如果熟练使用,这是一个很好的陷阱,快速且易于理解。
如果在阅读文章时您正在同时播放代码并处于Debug模式,请切换到Release-这很重要。
因此,让我提醒您,我们需要拦截Present和EndScene函数的执行。
我们实现拦截器-我们要转移控制的函数:
int PresentHook(Device* device) { cout << "PresentHook" << endl; return 1; } void EndSceneHook(Device* device, int j) { cout << "EndSceneHook" << " " << j << endl; }
让我们考虑一下我们需要的抽象。 我们需要一个接口,使我们能够设置陷阱,删除陷阱并提供有关它的信息。 有关陷阱的信息应包含指向被拦截函数,接收器函数和跳板的指针(事实上,我们拦截该函数并不意味着不再需要它,我们还希望能够使用它-跳板将有助于调用原始的被拦截函数)。
#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; };
IHook界面为我们提供了此类功能。 我们希望当Device类的任何实例调用Present和EndScene函数时(即RIP指针都指向这些地址),我们的PresentHook和EndSceneHook函数将相应执行。
直观地想象一下,当控件进入拦截函数时,拦截函数,接收器和跳板如何位于内存(.code节)中:
图6-内存的初始状态,执行进入被拦截的函数现在我们希望RIP(红色箭头)从源到目标的起点。 怎么做? 如上所述,源存储器包含操作码,当执行到达源代码时,处理器将执行该操作码。 本质上,我们需要从一个部分跳到另一部分,重定向RIP指针。 您可能已经猜到了,有一个操作码可让您将控制权从当前地址转移到所需地址,此JMP助记符称为。
您可以直接跳转到所需的地址,也可以相对于当前地址跳转,分别在板ff和e9中可以找到这些跳转。 为这些说明创建结构:
#pragma pack(push, 1)
相对跳转指令较短,但是有一个限制-unsigned int表示您可以在4,294,967,295之内跳转,这对于x64而言还不够。
因此,目标接收者的目标函数地址很容易超过该值并且在unsigned int之外,这对于x64进程来说是很有可能的(对于x86,一切都简单得多,您可以将自己限制在实现操作码挂钩的相对相对位置上)。 直接跳转需要14个字节,为了进行比较,相对跳转只有5个字节(我们打包了结构,请注意#pragma pack(push,1))。
我们需要将源处的值重写为这些跳指令之一。
在捕获函数之前,应先进行研究-最简单的方法是使用调试器(稍后将向您展示如何使用x64dbg进行调试)或使用反汇编器。 对于Present,我们已经从其开始输出了30个字节,指令48 89 4c 24 8占用5个字节。
让我们实现一个相对跳转。 由于指令的长度,我更喜欢此选项。 这个想法是这样的:我们替换原始函数的前5个字节,保留更改后的字节,并用相对跳转到指令地址的相对替换来替换,该地址位于unsigned int内。
图7-源函数的源5个字节被相对跳转替换是什么让我们跳到了分配的内存(紫色区域),我们如何通过这种动作使自己更接近将控制权转移到目的地? 在我们分配的内存中,有一个直接跳转,它将把RIP移到目的地。
图8-将RIP切换到接收器功能还需要弄清楚如何调用捕获的函数。 我们需要执行阻塞的指令,并从源代码的原始部分开始执行。 我们进行如下操作-将损坏的指令保存到蹦床的开头,记住有多少字节被损坏,然后直接跳转到source + destroyLen,转到“健康”指令。
执行相对跳转删除的已保存指令:
图9-使用跳板调用拦截的函数进一步执行不影响混搭的指令:
图10-继续执行所拦截函数的指令实现上述想法的代码 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,
功能说明SetHook将创建一条记录,该记录存储有关陷阱的信息,然后将记录添加到集合中。 从源地址的开头开始对指令进行爬网,直到可以完全输入相对的跳转指令(5个字节),然后将阻塞的指令复制到保留区中,并记住它们的长度。
非常重要的一点是,我们需要为跳板和中继分配内存,在其中我们将存储用于将流从源重定向到目标的指令,并且该内存的地址应在相对跳转可以跳转到的范围内(无符号) int)。
此功能实现了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; }
这个想法很简单-我们将从内存开始,从某个地址(在本例中为指向源的指针)开始向上和向下移动,直到找到合适的自由大小。
返回SetHook函数。 将已磨损的字节从源复制到分配的内存中,然后立即直接插入到源+损坏的跳转中,以继续执行未损坏的指令。
接下来是中继指针的安装,它负责通过直接跳转到接收器地址来将执行线程重定向到目标。 最后,我们更改了源-我们将写权限设置为函数所在的内存位置,并将前5个字节替换为导致中继地址的相对跳转。
我们设置了一个陷阱,但它也需要能够清洁。 中断-不是构建,想法很简单-我们将返回源的破旧字节,从集合中删除有关陷阱的记录,并释放分配的内存:
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; }
测试工作。 立即更改我们的接收器,以便它们可以使用跳板调用拦截的函数:
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; }
我们测试是否正确执行了所有操作,内存是否在流动,是否正确执行了所有操作。 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; } }
可以用
您还可以检入x64dgb。还记得吗,起初我要求您从事发布版本?现在去调试并运行程序。程序崩溃...陷阱被触发,但是尝试调用跳板会引发异常,该异常表示我们调用跳板的地址根本无法执行。我们错过了什么?调试版本有什么问题?我们开始看一下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
在x64dbg中运行时,可以看到以下内容。图11-调试构建指令在Debug中,操作码已更改,现在编译器添加了相对跳转e9 f4 360。所有函数都包装在跳转中,包括main和mainCRTStartup的入口点。好吧,好吧,另一个操作码,必须将其复制到跳板上,当调用跳板时,应调用此相对跳转,然后直接跳转到源的未损坏部分。在这里可以清楚地看到,一切都按照我们已经实现的完成,只有相对跳转和相对跳转,从不同地址,源和蹦床的执行使RIP暴露于完全不同的值。
以我的拙劣经验,相对跳转案例的实现涵盖了99%的使用率。还有更多的操作码应单独处理。请记住,在对函数设置陷阱之前,您不应该太懒惰并研究它。我不会打扰您,也不会在100%版本中添加功能(再次,以我的拙劣经验),如果您需要它或感兴趣的话,您可以看到此类库的排列方式以及它们专门检查的其他情况-这样做很容易如果您知道这是什么。相对跳转确实很常见,因此我建议实现它。相对跳转由e9操作码和您需要跳转到相对于当前地址的值组成。因此,您只需找出要跳到的位置,然后直接从跳板跳到那里即可。即使我们在那里遇到了新的相对跳转,也已经来自正确的地址。考虑到相对跳转的陷阱安装的实现 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,
如果反汇编程序返回该命令的操作码为e9的信息,我们将计算要跳转到的地址(ULONG_PTR ripPtr =(ULONG_PTR)pSource + context.len +(INT32)context.imm.imm32),并将该地址写入跳板作为直接跳转参数的值。我还注意到,在多线程环境中,当安装/卸下钩子时,其中一个线程可以开始执行我们捕获的功能时,可能会出现这种情况-结果,该进程将崩溃。硬件断点中将介绍部分处理方法。如果您需要经过验证的工具,则希望确保陷阱能够正常工作,没有自己的想法,也不想研究功能序言-使用现成的解决方案,例如,Microsoft提供了自己的Detour库。由于多种原因,我不使用此类库并使用自制的解决方案,因此我无法提供任何建议,我只能命名研究的库以发现新内容并使用它:PolyHook,MinHook,EasyHook(尤其是如果您需要在C#中使用钩子)。7.4.2。硬件断点
Opcode Hook是一个简单快速的陷阱,但不是最有效的。反作弊可以很容易地跟踪内存中的变化,但是可以将操作码挂钩用于反作弊本身或拦截它所使用的系统调用(例如NtSetInformationThread)。硬件断点是一个不会更改进程内存的陷阱。我在论坛上看到线程询问VAC是否遵循此陷阱-答案通常是混杂的。就个人而言,VAC并没有禁止我使用它们,也没有重置寄存器(不到六个月前,也许有所改变)。, , VAC DR /, - , . HWBP , - , , , DR0-DR7 .
HWBP使用特殊的处理器寄存器来中断线程执行。如果流上下文包含以某种方式设置的DR0-DR7寄存器,并且RIP转到DR0-DR3中存储的四个地址之一,则将引发异常,可以通过异常的类型和上下文的状态来捕获该异常,确定控制将异常抛出到哪个地址并得出结论-是否有陷阱。这种方法的一个重大局限性是您一次只能使用四个函数,并分别为每个线程设置它们,如果设置了陷阱并创建了一个新函数/重新创建了旧线程,这会导致陷阱,这将带来不便。这不是一个特殊的障碍,并且由对BaseThreadInitThunk函数的拦截来控制;对使用4个陷阱的限制并没有真正使我个人感到困扰。如果挂钩数量对您来说很关键,请查看PageGuard方法。因此,任务是相同的-我们在沙盒(Sandbox项目)中,有必要拦截Device Present和EndScene类的方法,以在其中调用原始方法。我们已经有现成的陷阱接口-IHook,让我们处理“铁”断点的工作。原理是这样的:有四个可以写入地址的“工作中” DR0-DR3寄存器,当尝试在指定地址进行写入,读取或执行时,取决于DR7控制寄存器的设置,将发生EXCEPTION_SINGLE_STEP类型的异常,必须在先前注册的处理程序中进行处理。 。您可以同时使用SEH处理程序和VEH-我们将使用后者,因为它具有更高的优先级。我们意识到这个想法: 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; }
关于DR6和DR7是什么以及PageGuard方法的更多详细信息,我可以建议Gray Hat Python:面向黑客和逆向工程师的Python编程。简而言之,DR7启用/禁用“工作”寄存器的使用-即使任何DR0-DR3寄存器包含一个地址,但在DR7中,相应寄存器的标志被禁用,断点也将不起作用。 DR7还使用需要引发异常的地址来设置工作类型-是否读取了地址,是否进行了记录还是使用了地址来执行指令(我们对最后一个选项感兴趣)。清除陷阱也非常简单,可以通过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; }
它仍然需要处理线程-应该为那些调用拦截函数的线程设置陷阱。我们不会为此而烦恼。我们为进程的所有线程设置了陷阱。 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; }
上面的代码绕过所有可见的进程并搜索当前进程。在为下一个线程找到的过程中,我们获取流处理程序,找到四个空闲寄存器之一并设置一个陷阱。值得关注的是Freeze和Unfreeze函数-这就是Opcode 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() {
在清除陷阱的功能中需要实现类似的功能。仍然需要添加VEH异常处理程序。添加和删除是通过任意流的AddVectoredExceptionHandler和RemoveVectoredExceptionHandler函数完成的。 void HardwareBPHook::SetExceptionHandler(PVECTORED_EXCEPTION_HANDLER pVecExcHandler) { pException = AddVectoredExceptionHandler(1, pVecExcHandler); } ~HardwareBPHook() { info->Clear(); delete info; RemoveVectoredExceptionHandler(pException); }
处理程序必须检查异常的类型(需要EXCEPTION_SINGLE_STEP),检查发生异常的地址与寄存器中的内容的对应关系,如果找到了这样的地址,则将RIP指针重新排列为接收者的地址。堆栈的状态得以保留,以便在进一步执行接收器时,堆栈上的所有参数将保持不变。我们在沙箱中实现了所描述的处理程序: 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; }
理论上,一切就绪,我们运行程序,等待与OpcodeHook完全相同的工作。不会发生这种情况,我们的程序会冻结-更确切地说,它会不断进入PresentHook,并在应调用跳板的那一刻再次调用该函数。事实是,“铁”断点并没有消失,因为当您调用跳板(在“铁”断点的情况下,它指示原始功能)时,我们再次报警相同的地址并引发异常。解决方案如下:在特定线程的处理程序中找到断点后,我们将其删除,并在适当的时候再次设置该断点。更新的位置将选择接收器功能结束的时间。这是按以下方式实现的-在处理程序中,除了删除断点外,还添加了一个待处理命令,其含义是更新指定流中的断点。该命令在接收器功能的末尾运行。 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; }
待执行命令 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() {} }; }
形象地想象“铁”断点的工作。
图12-初始状态我们设置一个陷阱,添加一个VEH处理程序,等待控件到达源函数:
图13-准备拦截的阶段引发异常,调用处理程序将RIP重定向到接收方并重置断点:
图14-重定向执行线程在函数接收器上关于此主题,可以完成陷阱,静态库Silk_way.lib已准备就绪。根据我自己的经验,我可以说我经常使用OpcodeHook,VMT Hook,强制异常Hook(可能是最“痔疮”陷阱),HardwareBreakpoint和PageGuard(在执行时间不是很关键的时候,一次拦截)。8.逻辑架构
逻辑的基础以MVC(模型视图控制器)的形式表示。所有核心实体都从ISilkObject接口继承。8.1。 型号
在库中开发机器人时,我首先实现了ECS(您可以在此处和此处了解有关此方法的信息)。当我意识到用真正的玩家启动机器人是一项艰巨的任务时,我编写了一个模拟,其中测试了ml库(使用三维网格进行导航(Dota 2仅使用3D网格进行导航),并对主体块使用简化的2D物理)。当对模拟的需求消失了,我弄清楚了如何记录日志以及在战斗中收集什么信息后,不再需要ECS,模型开始包含一个组件字典(代表诸如SkyForge的家伙,“头像和暴民),实际上包含来自Source2Gen的结构的包装。对于本文,我没有为了简化材料而转移此实现。该模型包含Schema,在其中存储其描述(此点已简化,并且在本实现中不是根据方案创建模型,方案仅描述了它(存储可以硬编码的预定义值)-可以与将游戏内容存储在xml / json中进行比较)示意图设备可以用以下方式表示:图15-代码中的模型实现的示意图:
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; };
该方案包括对特定模型的描述,并包含该模型可以使用的上下文。 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; } };
模型的收集和计划的收集 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; } };
因此,例如,存储Roshan状态的模型的接口和实现看起来像 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。 查看,查看状态和控制器
Presentation,Presentation State和Controller没什么好说的,其实现与Models类似。它们还包括架构和上下文。为了解决View的问题,实现了Canvas,ViewCollection,Label和Button,对于后两个,还实现了与Roshan所处的状态相对应的状态。示意图
16 — 视图状态的示意图
17 — 8.3。 工厂
使用工厂创建对象。工厂将接口类型用作键,并使用typeid(T).raw_name()将其转换为字符串。通常,这样做不好,为什么以及如何以及如何在Andrei Alexandrescu的《现代C ++设计:通用编程》中正确阅读。工厂实施: 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; };
在使用工厂构建对象之前,需要注册。型号注册示例 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); }
该方案可以以任何方式填充-您可以使用json,也可以直接在代码中。用于在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": {} } } }
用于填充代码提交方案的选项 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。 大事记
该视图通过事件了解模型中的更改。您可以获得有关类方法和普通函数的反馈。 #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() {} };
如果对象要报告其中发生的事件,则需要为每个事件添加IEvent *。对在此对象内发生的事件感兴趣的另一个对象应该创建ICallback *并将其传递给IEvent *(订阅该事件)。发生在控制器中的示例订阅 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); }
在类中声明事件的示例-每次敲钟(调用Tick方法),都会引发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; };
基本基本类型(SILK_INT,SILT_FLOAT,SILK_STRING等)在Core.h中实现。9. DirectX 9
DirectX 9是Dota 2支持的图形API之一。设备是从IUnknown继承的类,并且包含虚函数。因此,在收到指向虚拟方法表的指针后,我们可以获取指向所需功能的指针。非虚拟类函数不包含在表中,而是位于.code段中,因为它们是唯一不能覆盖的函数。顺便说一下,在OpenGL和Vulkan中,拦截设备函数要容易得多,因为它们不是虚拟的,您可以使用GetProcAddress()获取指针。 DirectX 11架构比9更复杂,但不多。要拦截虚拟类方法(以及非虚拟类方法),我们需要此类的一个实例,任何实例。使用实例,我们获得虚拟方法的表并获得指向函数的必要指针。查找类实例的最简单方法是自己创建它。为此,我们需要使用Direct3DCreate9函数通过IDirect3D9接口创建一个对象,然后通过调用CreateDevice方法使用该对象创建设备。我们可以直接从DirectX库调用这些函数,但是为了合并材料,我们将通过指针对其进行调用。从d3d9.h中可以看出,Direct3DCreate9是一个常规函数,可以通过GetProcAddress获得指向它的指针(就像我们在NativeInjector中获得指向LoadLibrary的指针一样)。
图18-d3d9.h中CreateDevice的描述创建IDirect3D9的实例: typedef IDirect3D9* (WINAPI *SILK_Direct3DCreate9) (UINT SDKVersion);
使用IDirect3D9,我们可以通过调用pD3D-> CreateDevice(...)创建设备。为了从VMT获得指向必要功能的指针,我们需要找出确定这些方法的过程。图19-索引搜索IDirect3D9接口的CreateDevice方法获取第 16个索引。除了CreateDevice,我们还需要Release和GetAdapterDisplayMode方法。
我们用代码实现设备的创建 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;
好了,我们创建了DirectX 9设备,现在我们需要了解用于渲染场景的功能以及需要拦截的功能。我们需要回答以下问题:“ DirectX 9如何向我们展示场景?” Present功能用于显示场景。还值得介绍这样的概念,例如前缓冲区(用于存储屏幕上显示的内容(长期动作)的缓冲区),后缓冲区-包含准备显示并准备成为前缓冲区的内容,交换链-实际上是一组缓冲区从前向后翻转(DirectX 9只有1个交换链)。在调用Present之前,先调用了几个BeginScene和EndScene函数,您可以在其中修改后台缓冲区。让我们截取两个函数(实际上,执行业务逻辑,对于我们来说一个就足够了):EndScene和Present。为此,请在IDirect3DDevice9类中查看这些函数的位置。图20-声明IDirect3DDevice9接口声明具有以下函数签名的指针:
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;
我们将立即使用错误处理程序声明一个陷阱,因为HardwareBreakpoint实际上是我们唯一实施的不跟踪VAC的安全拦截选项(您也可以使用Opcode Hook进行测试,但您的帐户很可能会被禁飞): 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; }
发出我们两个陷阱中任何一个的指定功能: 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();
功能接收者: 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; }
在“当前”中,每个呼叫都使用“捕获”功能从视频卡缓冲区中截取屏幕截图(用于验证) 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创建一个业务逻辑控制器。创建后,将调用控制器更新,其中所有逻辑都将更新。我注意到,现在我们已经实现了DirectX 9的工作。如果我们想进行某种mod,作弊等操作,则必须支持所有四个API。如果阿森纳已经拥有您喜欢的库,而UI则为空白,那么这是有道理的,否则您可以使用另一种方式-使用引擎渲染游戏的功能。还值得一提的是,从EndScene()调用逻辑更新不是最佳选择-您可以在流中找到对引擎函数或调用逻辑的定期调用。但是,如果您对EndScene的调用感到满意,则最好使用锁步操作。现在,我们已经实现了我们计划的一切。测试建议DirectX SDK , , DirectX 9 DirectX 11. DirectX 11, - SDK, ( , ) , , DXUT, , — , FPS .
21 — DirectX SDK StateManager.exe 现在您可以在Steam中创建一个虚假帐户,并将注入的dll注入Dota 2进程中。我马上要说,我不知道带有“铁”断点的当前情况是如何-使用Opcode Hook(我们目前使用的方式表格),您肯定会被禁止。我大约六个月前就这样做了-没有硬件断点的禁令,我不能说目前的情况。在准备本文之前,我考虑了两个问题,并对它们进行了Opcode Hook和HWBP的尝试,第一个进入禁令(大约2个星期过去了),第二个否决了(3个星期过去了)。但是仍然不能保证禁令不会在将来发生。如果您不小心从主帐户进行了介绍,或者忘记登录假帐户,则不要生气-然后请多保重并小心。( )
22 —
23 — 以1x1模式实施。图24-向火柴中注入还值得一提的是,还有另一种渲染方法-通过创建具有适当大小的第二个窗口进行表面渲染。不幸的是,我无法意识到在全屏模式下使用表面处理的可能性,但是本文中描述的方法允许您在全屏和窗口模式下实现渲染而没有任何问题。我们的嵌入式UI仅包含文本标签和在纯DirectX 9上实现的按钮-这是解决任务所需的全部。您可以在纯API上或使用现成的库来实现复杂的表,精美的菜单和图表-通常,可以实现任何复杂的UI。当然,不仅是2D。
10.使用引擎功能
为每个API实现相同的功能相当乏味;开发人员通过提供游戏直接使用的绘图,UI等功能,可以方便地进行包装。Valve还提供了Java和Lua的 Dota 2 API 。这样做是为了使C ++复杂的主持人和游戏设计师(甚至不是C ++本身,而是在引擎上下文中正确使用)变得更轻松。这里有渲染功能和游戏逻辑-您可以规定单位的行为,例如,选择物品,使用技能等。实际上,借助于此,可以编写自定义字母。我们将对DoIncludeScript函数感兴趣,该函数允许您在Lua上运行脚本并在其中使用脚本API。我没有在项目中使用它,因为我没有看到它的值,而是直接使用C ++中的函数,因此看到了将其与or_75一起使用的想法,并决定将其包含在本文中。这将向您介绍第二部分的内容并节省其中的空间;您无需解释调试器的某些方面。让我们开始吧。
任务如下:您需要找到一个指向DoIncludeScript函数的指针,该函数需要脚本和处理程序的名称来进行研究。我们将使用Silk_way.lib库中的扫描器搜索功能。我们已经发现,函数是使用操作码表在内存中编码的-让我们检查一下此函数,并尝试确定其在内存中的存储模式。现在,扫描仪没有必要的功能,我们需要能够在过程存储器中搜索模板。为了加快搜索速度,我们不会在整个过程内存中搜索模式,而是在特定模块中搜索(我们的功能位于client.dll中,这将在调试器中看到,并将在下面进行讨论)。我们将通过枚举进程的所有模块来使用tlHelp32来搜索模块,为此,我们将为其创建一个函数以在当前GetModuleInfo进程中找到该模块。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; }
该模式是一个具有字节值的字符串,跳过一个字节用符号“ ??”表示 -例如,“ j9??? ?? ?? 48 03 08 ?? f1 ff”。解析字符串,为方便起见,我们将模式从字符串表示形式转移到无符号char值列表,设置要跳过的字节标志。 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) {
搜索核心是在FindPattern函数中实现的,其中基于接收到的有关模块的信息,设置搜索的开始和结束地址。VirtualQuery函数会请求有关要搜索的内存的信息,内存有很多要求-它必须很忙(在空闲内存中搜索会出错),该内存必须可读,可执行且不包含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; }
现在,我们可以在过程存储器中搜索所需的模板,但是还不知道要查找什么。在Fake帐户下运行Steam并打开您喜欢的调试器(让我们同意,在阅读x64dbg这篇文章时也很适合您-我没有IDA Pro的付费许可证),从... \ Steam \ steamapps \目录中运行dota2.exe常见的\ dota 2 beta \游戏\ bin \ win64。原则上,我没有注意到VAC对Cheat Engine和x64dbg并不冷漠,我不记得使用这些工具时帐户被禁止了。顺便说一下,调试器有一个ScyllaHide插件,可以拦截NtCreateThreadEx,NtSetInformationThread等系统功能,隐藏其工作原理,您可以安装此插件。在每个停靠点(会有10-15点),我们继续使用运行(F9)执行。游戏开始时,我们将看到菜单并可以开始研究。开始游戏后,在各行上进行搜索(搜索->所有模块->字符串引用),设置“ DoIncludeScript”过滤器。图25-搜索游戏进程内存中的行让我们双击第一个结果进入反汇编程序(“ CPU”选项卡)。这将是我们的起始地址,因为它位于client.dll中,其余结果位于server.dll和animationsystem.dll中。我们从接收到的地址构造一个呼叫图。图26-调用图反编译后,我们找到使用DoIncludeScript的入口点-图的第四个节点。实际上,功能本身。

图27-DoIncludeScriptGraph 函数。图28-来自DoIncludeScript的调用图对该函数的反编译显示了以下代码及其调用位置(反编译是从该图完成的,而不是从反汇编程序完成的)。图29-反编译对DoIncludeScript函数的调用让我们根据对DoIncludeScript函数的调用的图27中的指令来组成一个模板。参数可以分别更改,我们要在搜索时跳过模板中的参数,用“ ??”表示。我得到以下信息:40 57 48 81 EC ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ?? 48 8B F9 0F 84.为了编译模板,我们使用了图28中图形的第一个节点,其指令可以在图27中找到。
在Lua silk_way.lua上创建一个脚本,将其放在“ ... \ 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")
该脚本绕过所有实体,并根据其位置显示坐标。使用上面的文档和来自图29的反编译代码声明功能。 typedef bool(*fDoIncludeScript)(const char*, unsigned long long);
函数调用。 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); }
实施后,我们将看到有关游戏实体位置的信息。图30-实现结果现在我们可以运行脚本了。但是它们是在Lua中执行的,并且说Roshan死的事件对我们来说是C ++代码所必需的(因为我们已经写了主要逻辑),我们该怎么办?我们将必须使用Source SDK和Source2Gen以相同的方式(就像我们对DoIncludeScript所做的那样),必要的引擎功能和其他功能找到指向必要功能的指针。但是在下一部分中,我们会找到更多关于实体列表的指针,并在逻辑上更接近于游戏机制,以提供更多信息。如果您一次想要所有东西,可以尝试,我附上了这个,这个,这个和这个作为您的帮助
链接。11.结论
最后,我要感谢在反向领域分享最佳实践和知识并与他人分享经验的每个人。只讲没有祈祷者的《 Dota 2》,我会花很多时间才能使用作弊引擎获得游戏的数据结构,而所做的成就可能会随着Valve的更新而中断。更新会破坏找到的静态指针,并偶尔更改实体的结构。在or75,我看到了DoIncludeScript函数的用法,并在它的帮助下,展示了一个使用游戏引擎输出文本的示例。为了简化演示文稿,我可能会漏掉一些东西,忽略我认为不值得关注的各种情况,反之亦然,然后增加解释的范围-如果细心的读者发现此类错误,我将很乐意纠正它们并听取评论。可以在链接中找到源代码。感谢所有花时间阅读本文的人。