逆向尝试-FPS游戏方框透视下-程序大致思路及重点分析
2021-10-16 # 逆向

关于源码

所有的源码已经提交到了我的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); //获取最高权限

在整个程序的末尾不要忘记关闭进程。有借有还才是好习惯()。

1
CloseHandle(hProcess);

因为读取内存的操作较为频繁,并且读取的数据类型比较多。所以我们采用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
//processMouduleBase:client.dll的基址
//entityListOffset 实体链表的偏移量
//healthOffset 生命值偏移量
//locationOffset 坐标偏移量
selfItem = ReadGamememory<EntityItem>(processModuleBase + entityListOffset);//读取自身节点
myhealth = ReadGamememory<int>(selfItem.entityObj + healthOffset);//读取自身生命值
myx = ReadGamememory<float>(selfItem.entityObj + locationOffset);//读取自身x坐标
myy = ReadGamememory<float>(selfItem.entityObj + locationOffset + sizeof(float));//读取自身y坐标
myz = ReadGamememory<float>(selfItem.entityObj + locationOffset + 2 * sizeof(float));//读取自身z坐标

读取其他人的信息也是同理。只是在遍历链表的时候,把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 };
}

}
//ViewScreen 和 Person_Struct均为结构体。分别存储了屏幕坐标的x,y值和人的x,y,z坐标值
//1920和1080为屏幕的分辨率,可以根据实际情况修改。

确定要绘制矩形的大小和位置

算法如下,大致是利用两人之间的位置和人物模型的身高算出屏幕坐标。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,不闪烁,但是易被检测。

因为不考虑防作弊方面的内容,并且恰好找到了相关的资料,所以采用了第四种方式绘图。