关于源码
所有的源码已经提交到了我的github仓库中。需要的可以自行取用。本文中只截取重点部分的代码。
本文中涉及到的内存部分的代码并没有进行封装。而github中的代码内容是用面向对象的方式重写的(虽然感觉不封装起来会更好一些)。编写过程使用的是多字节字符集,如果需要使用源码注意修改字符集。同时由于窗口绘制使用了dx,需要下载dx的库并在vs中设置好其路径。
链接:ShiJiJS/CSGOCheatBase: 基于C++实现的简单的CSGO方框透视 (github.com)
梳理一下我们要做什么吧
在内存的部分,我们需要完成的事情主要有以下几个部分
- 读取自己的坐标和视角矩阵
- 循环读取每一个实体的坐标并筛选出人
- 用筛选出的对象的坐标和自己的坐标,视角矩阵,算出对象在屏幕中应该显示在哪个位置
之后再交给绘图的部分完成方框的绘制就好。
如何读取到我们想要的数据
在上一篇中,我们已经得到了类似与client.dll + 偏移值的这种模块 + 偏移量的地址了。所以我们只需要写一段代码,让程序每次执行时都寻找一下client.dll的地址,再加上特定的偏移量,就可以读取到我们想要的数据了。
先让我们的程序找到csgo
每个程序都有一个特定的pid作为标识,虽然在任务管理器的详细信息里可以看到,但每次开启手动输入未免也太过麻烦。下面这个函数可以用于获取程序的pid。我们只需要传入一个程序的名字(“csgo.exe”)和你要用于存储pid的变量。就可以得到程序的pid了。具体细节就不过多的赘述了。大致就是遍历整个列表,找出与所给名字相同的程序,记录下其pid。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| BOOL GetPIDByName(LPCSTR ProcessName, DWORD& dwPid) { HANDLE hProcessSnap; PROCESSENTRY32 pe32; hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) { return(FALSE); } pe32.dwSize = sizeof(PROCESSENTRY32); if (!Process32First(hProcessSnap, &pe32)) { CloseHandle(hProcessSnap); return(FALSE); } BOOL bRet = FALSE; do { if (!strcmp(ProcessName, pe32.szExeFile)) { dwPid = pe32.th32ProcessID; bRet = TRUE; break; }
} while (Process32Next(hProcessSnap, &pe32)); CloseHandle(hProcessSnap); return bRet; }
|
###再找到模块的基址
在找到程序的pid之后,我们就可以去找client.dll的基址了。用下面的方式即可找到。基址均以DWORD的格式保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| DWORD GetProcessModuleBase(DWORD dwPID, const char* moduleName) { HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID); if (hModuleSnap == INVALID_HANDLE_VALUE) { cout << "[ERROR] Failed to CreateToolhelp32Snapshot\n"; return 0; } me32.dwSize = sizeof(MODULEENTRY32); if (!Module32First(hModuleSnap, &me32)) { cout << "[ERROR] Failed to Module32First\n"; return 0; } do { if (!strcmp(me32.szModule, moduleName)) { CloseHandle(hModuleSnap); return (DWORD)me32.modBaseAddr; } } while (Module32Next(hModuleSnap, &me32)); CloseHandle(hModuleSnap); return 0;
}
|
读取其他程序的内存
因为我们要读取的是其他程序的内存,所以用指针等直接访问地址的方式行不通。我们需要借助其他的方式。首先我们需要获取其他进程的权限。
1
| HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, gamepid);
|
在整个程序的末尾不要忘记关闭进程。有借有还才是好习惯()。
因为读取内存的操作较为频繁,并且读取的数据类型比较多。所以我们采用c++的模板函数来写一个内存读取的函数。
1 2 3 4 5 6 7 8
| template<typename ReadType> ReadType ReadGamememory(DWORD addr) { ReadType buff; SIZE_T readSz; ReadProcessMemory(hProcess, (LPVOID)addr, &buff, sizeof(ReadType), &readSz); return buff; }
|
这样,我们只需要使用下面的方式就可以很容易的读取到我们的数据了。例如:
1 2 3 4 5 6 7 8 9
|
selfItem = ReadGamememory<EntityItem>(processModuleBase + entityListOffset); myhealth = ReadGamememory<int>(selfItem.entityObj + healthOffset); myx = ReadGamememory<float>(selfItem.entityObj + locationOffset); myy = ReadGamememory<float>(selfItem.entityObj + locationOffset + sizeof(float)); myz = ReadGamememory<float>(selfItem.entityObj + locationOffset + 2 * sizeof(float));
|
读取其他人的信息也是同理。只是在遍历链表的时候,把EntityItem换成对方的就可以了。
视角矩阵用这一函数也可以很轻松的读取。
将世界坐标换算成屏幕坐标
有了他人的坐标和视角矩阵,我们就可以算出他人在我们屏幕上所应该显示的位置了。用下面的算法即可。
PS:这个算法让我折腾了好久,中途换了好几个都没有成功,一直以为是视角矩阵找的不对,结果后面才发现是算法错误。来来回回折腾了好几个小时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ViewScreen WorldToScreen(Person_Struct Ps, float ViewMatrix[4][4]) {
ViewScreen Rs; float w = ViewMatrix[3][0] * Ps.X + ViewMatrix[3][1] * Ps.Y + ViewMatrix[3][2] * Ps.Z + ViewMatrix[3][3]; if (w > 0.001) { float fl1DBw = 1 / w; Rs.X = (1920 / 2) + (0.5f * ((ViewMatrix[0][0] * Ps.X + ViewMatrix[0][1] * Ps.Y + ViewMatrix[0][2] * Ps.Z + ViewMatrix[0][3]) * fl1DBw) * (1920) + 0.5f); Rs.Y = (1080 / 2) - (0.5f * ((ViewMatrix[1][0] * Ps.X + ViewMatrix[1][1] * Ps.Y + ViewMatrix[1][2] * Ps.Z + ViewMatrix[1][3]) * fl1DBw) * (1080) + 0.5f); return Rs; } else { return{ 0,0 }; }
}
|
确定要绘制矩形的大小和位置
算法如下,大致是利用两人之间的位置和人物模型的身高算出屏幕坐标。Rect为结构体,存储x,y,height,width四个能确定矩形的变量。
1 2 3 4 5 6 7 8 9 10 11 12
| Rect getRect(ViewScreen Rs,Person_Struct selfItem,Person_Struct elItem) { double M = sqrt(pow((selfItem.X - elItem.X), 2) + pow((selfItem.Y - elItem.Y), 2) + pow((selfItem.Z - elItem.Z), 2)) / 30; int H = 950 / M * 2; int W = 400 / M * 2; Rect rect; rect.x = Rs.X - W / 2; rect.y = Rs.Y - H; rect.height = H; rect.width = W; return rect; }
|
至此,我们在内存部分的工作就圆满完成了!
关于方框的绘制
源码中的renderer.h和renderer.cpp是用于绘制方框的代码。
大致思路是先用Overlay这个类,在游戏窗口上创建一个和游戏窗口等大小的透明窗口对象。再通过Dx_renderer这一个类来进行绘制。
目前我所了解到的绘制思路主要有下面几种
- GDI外部绘制:不易被检测,占用CPU很小,但是绘制到游戏里面会闪烁,原因是游戏FPS刷新频率与gdi绘制频率不同。
- GDI双缓冲:不会使方框闪烁,但是占用CPU极高
- GDI窗口绘制:绘制效率快而且不易被检测,内存占用很小。但是窗口不会自己刷新,需要自己写一个刷新窗口。
- D3Dhook绘图:通过hook DirectX设备接口函数来绘制方框,这种绘制方式不占CPU,不闪烁,但是易被检测。
因为不考虑防作弊方面的内容,并且恰好找到了相关的资料,所以采用了第四种方式绘图。