CS2外置透视辅助实现

CS2外置透视辅助实现

完整源代码:https://github.com/Kvancy/CS2-ESP-Cheat

先思考一个问题,如果要做透视,我们需要什么?

  1. 首先是敌人的世界坐标,有了敌人世界坐标我们才能进行转换和绘制;

  2. 其次是敌人和队友的标识符,有个这个我们才能过滤掉友军,只针对敌人进行绘制

  3. 还有一个就是血量,通过判断敌人是否存活来进行过滤掉死去的敌人,不对死去的进行绘制

按照惯例,做外挂功能的第一步要先找到基址和偏移,哪些量的基址和偏移?上面分析的这三个至少要有吧

基址寻找

再思考一个问题,关于坐标,敌军友军的标识符,血量的内存布局可能是怎么样的?

根据C++面向对象编程中的内存布局,我们能做一些猜测

  1. CS2这么大的游戏,大概率是面向对象编程
  2. 根据编程经验,游戏世界往往会存在一个或多个抽象类,而我们要研究的角色,可能只是一个子类
  3. 坐标,血量,标识符可能不尽是角色类的特有成员,内存布局可能存在较大差异

揣着这些猜测,开始寻找基址和偏移

进入练习模式打开控制台(~),输入如下指令

  1. sv_cheats ${0/1}$; //舞弊方式,0表明关掉,1表明开启(任意挪动指令必须开启舞弊方式)
  2. bot_stop ${0/1}$; //bot终止行为,0表明关掉BOT逐渐主题活动,1表明开启BOT终止主题活动
  3. mp_roundtime_defuse {n}$ ;//设置每回合的时间
  4. mp_restartgame 1 //重启该回合

打开CE针对角色血量进行搜索,定位到血量地址,找到如下可能的地址。

image-20240816132931465

利用二分法锁定内存地址的值,打开控制台输入 hurtme 1指令,观察哪一个地址锁定后血量不再下降,或者用修改数据的方法看看哪个是真实地址,哪些是随着真实地址变动的“虚假地址”。

image-20240816133110712

找到真实血量内存地址。

image-20240816133526942

进行指针扫描,设置最大偏移地址5560和最大偏移层数5,经验所得,大多数游戏可以用这两个值找到,找不到的时候适当调整即可。

image-20240816133810072

找到294个指针路径,为了进一步筛选,继续hurt me 修改血量,通过指针扫描器->重新扫描内存->要查找的数值,设置当前血量值,重新扫描。

image-20240816134615515

得到235个指针路径,同理可以进行多次筛选,最终测得233个指针路径。

image-20240816134723096

此时关闭游戏(记得提前保存好ptr文件,一般扫描后就保存了),重新打开游戏。再次重新扫描内存,进一步筛选得到40个指针路径

image-20240816221716190

可以看到第四层偏移全是2A8,对应的是血量在所属类的偏移值,到这我们基本就可以利用其中之一作为自己角色血量的基址和偏移了

这里选择最后一条

血量偏移
0x2A8
0xC0
0x8
0x10
0x50
“server.dll”+01343108

我们继续找人物坐标的基址和偏移,因为在三维世界里,二维坐标X,Y的方向不是很好确认,但是Z坐标一般都是一样的,往上走,Z坐标增加,往下走,Z坐标减小,有些游戏可能会反过来,但是不影响,只是稍微更麻烦一点。只要找到了三个坐标其中之一,另外两个也就在附近了(存放在一个结构体中),这里我们找Z坐标。

我们到这个地点,有个上下坡的地方,方便修改Z坐标。

image-20240816200004467

接着就是一顿CE搜索,我这里最终筛到300多个地址,实在是筛选不下去了,都像是Z坐标,接着按照二分法,一半锁定一半不锁定,去找一个地址,锁定住之后角色跳不起来,筛选到如下地址

image-20240816202311521

尝试修改数据测试真实性,修改成-20000

image-20240816203020181

这tm给我干哪来了我擦了,准备下包!修改一下Z坐标回到地面

image-20240816204316307

擦,修改多了,完了,掉下去就死了,寄。。。

但是现在基本确认了这个地址是真实Z的坐标,其他的只是因变量,对这个地址进行指针扫描。扫描到93条指针路径,进一步按之前步骤筛选,找到一个能用的

image-20240816205421898

接着我们在这个地址观察一下附近是否会出现X,Y坐标,发现前面两个就是X,Y坐标,基本确认了我们角色的基址和偏移

image-20240816210704596

因为这个只是我们的坐标,其他角色的坐标还没去搜索,挨个搜索肯定不现实,那还有什么办法去快速找到其他角色的基址和偏移呢?

根据我们的一点点开发经验可以猜测游戏里面大概有这么一份代码模型

1
2
3
4
5
6
7
8
9
10
11
class Game
{
int time;//随便举的例子
Player player[10];
}
class Player
{
float blood;
int TeamNum;
Point3D pos;
}

根据这个猜想,只要我们获得了Player的内存大小,我们就可以根据我们角色的地址枚举出其他角色的地址,从而获取其他角色的位置属性。嘶,但是这个角色类的内存大小貌似不好求。我又想到一个方法,如果角色内存大小不是很大的话,我们可以在CE中通过在我们的坐标附近去寻找,是否还有像坐标的地址,如果找到了,那么很可能是其他角色的坐标,我们就可以直接两个坐标地址相减获取角色内存大小,浅试一下。

找了一下,并没有找到,显然这招对这游戏并不是一个好方法,可能CS2是用类指针数组去存十个人物地址的,也可能是用链表去存的,这方法还是太笨了。只能想另一种方式了。

还记得我们最开始的C++内存布局揣测么,那既然我们已经找到了基地址和五层偏移,那就意味着我们角色的地址很可能就在这个基地址的相对四层或者三层(或者更少)偏移计算得到,那我们假设,游戏的十个角色是被new出来的,也就是用类指针数组存储十个人物地址的,那我们的角色地址找到之后,偏移8个字节(64位的游戏),那是不是就是其他角色的类指针?再试一次,如下是我们之前找到的基地址和偏移

image-20240816220135032

试着修改第一层到第四层的偏移值,修改+8。

image-20240816220248133

到第二层偏移+8时,刚好对应浮点数-120,符合地图的Z坐标范围,点开内存看一眼之前是不是X,Y坐标

image-20240816220401734

好像是,让机器人动起来试一试

image-20240816220500499

没错了!继续+8或者-8定位另外八个角色的第二层偏移。

直到第二层偏移为60时,内存指向空,所以十个人物的角色第二层偏移是10,18,20,28,30,38,40,48,50,58(十六进制),刚好对应十个人物角色

至此,人物位置分析完成,获取的基地址和偏移为

坐标偏移
0x128
0x38
0x70
0x10
0x50
“server.dll”+01343108

突然忘了,其他人物血量还没定位,但是这样一看,其他人物的血量地址也出来了,因为血量的前两层偏移和坐标前两层偏移相同,也只需要在第二层偏移(m+8*n)即可。

继续第三个地址寻找,队伍标识符,按经验推测,会将敌军还是友军置为bool类型,只需要判断01就可以区分保卫者和潜伏者,但是我在控制台输入的时候,他会告诉我这些信息。

image-20240816222533854

貌似就是队伍标识符了,但是我们海狮要确定这个的基地址和偏移,根据之前两个基地址和偏移,我们可以进行推测基地址和前两个偏移值都是相同的,只要获取后面的偏移即可。继续用CE进行搜索吧,这里要利用CS2的这个功能去转换阵营,要不然没法CE搜索

image-20240816223153811

先假设潜伏者为2,保卫者为3,进行CE搜索,找到这些地址,

image-20240816223504653

再尝试修改这些数据,修改一次攻击一下友军,看一下提示是否想同,直到出现这个画面

image-20240816232138115

我穿着保卫服却显示我在潜伏者,并且视保卫者为敌人,对这个地址进行指针扫描

进行指针扫描,找到前两层偏移一样的并进行记录
image-20240816234452411

发现前两层偏移并没有一样的,如果用这些基地址和偏移的话,我们就还得去找其他角色地址的偏移,那太麻烦了,根据这个最后的偏移都是314,试着将我们得到的两个基地址和偏移的最后一个偏移分别改成314看看是否对应结果3,发现带入血量的最后一个偏移时结果对应。

image-20240816234746538

看来血量和队伍标识符是同属一个子类的,所以队伍标识符的基地址和偏移我们拿到了,这里做一下记录

队伍标识符偏移
0x314
0xc0
0x08
0x10
0x50
“server.dll”+01343108

接着进行视图矩阵的搜索,搜索方法也很简单,只要搜索矩阵中16个浮点数的其中之一就可以了。我们可以针对某个值进行搜索,这个值会在-1.0-1.0之间(也可能稍微超过一点点),当视角朝向天空是,该值趋近于1.0,朝向地面时,该值趋近于-1.0,根据这个我们可以控制角色看向天空,然后搜索0.8-1.2之间的数值,同理朝向地面,我们搜索-0.8–1.2之间的数值,还可以变动视角搜索变动的值,我这里就是用这种方法进行搜索视图矩阵的。

image-20240817173447606

image-20240817173608316

两次搜索就能筛选到40000数值,接着筛选。最后搜索到还剩1200个地址,

image-20240817174309790

但是我们看见了绿色的基址,把这些基址复制下来一一查看内存布局,看看是否有矩阵特征。

image-20240817175606456

看这三个矩阵,随着视角都是会变动的,但是都是不对的,比如第一个矩阵,有些数据保持不变,不显红色,排除;第二个矩阵,只是一个3*4的矩阵,排除;第三个矩阵,第一行和第二行的前三个数据不符合矩阵数据规范,超过了1.0,排除。一直到这个内存地址

image-20240817175928546

发现数据刚好符合规范并且是4*4的矩阵,记录下该地址

视图矩阵偏移
client.dll+19CB4D0

到这基址和偏移寻找就完成了,接着开始写我们的透视辅助代码

代码实现

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "kWindow.h"
#include "kGame.h"
#include "kPlayerPawn.h"
#define isWorldwide 1
#if isWorldwide
#define WINDOWNAME "Counter-Strike 2"
#else
define WINDOWNAME "反恐精英:全球攻势"
#endif
kWindow* window = nullptr;
kGame* game = nullptr;
kRender* render = nullptr;

int main()
{
window = new kWindow(WINDOWNAME);
game = new kGame(TEXT("cs2.exe"), window->getRect());
window->msgLoop(game);
}

msgLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void kWindow::msgLoop(kGame* game)
{
kRender* render = new kRender(hWnd);
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else {
doCheat(render,game);
}
}
}

doCheat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void kWindow::doCheat(kRender* render, kGame* game)
{
game->refreshData();
game->showAllPlayer();
render->beginRender();
for (size_t i = 0; i < 10; i++)
{
if (game->m_playPawn[i].getTeamNum() == game->m_playPawn[0].getTeamNum())
continue;
if (!game->m_playPawn[i].isAlive())
continue;
point2D pos = game->world2screen(game->getPlayerPawnPos(i), game->getMatrix());
point2D posHead = game->world2screen(game->m_playPawn[i].getPosHead(), game->getMatrix());
render->drawRectangle(pos.x, (pos.y + posHead.y) / 2, 60, posHead.y - pos.y);
}
render->endRender();
}

refreshData

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
30
31
32
33
34
35
36
bool kGame::refreshData()
{
auto server = cs2->get_module_handle(TEXT("server.dll"));
auto client = cs2->get_module_handle(TEXT("client.dll"));
DWORD64 player = 0;
if (!cs2->read<DWORD64>(server + 0x1343108, player)) return false;
if (!cs2->read<DWORD64>(player + 0x50, player)) return false;

DWORD64 TeamNum = 0;
for (size_t i = 0; i < 10; i++)
{
if (!cs2->read<DWORD64>(player + 0x10 + 0x08*i, TeamNum)) return false;
if (!cs2->read<DWORD64>(TeamNum + 0x08, TeamNum)) return false;
if (!cs2->read<DWORD64>(TeamNum + 0xC0, TeamNum)) return false;
if (!cs2->read<int>(TeamNum + 0x314, m_playPawn[i].m_iTeamNum)) return false;
}

DWORD64 Pos = 0;
for (size_t i = 0; i < 10; i++)
{
if (!cs2->read<DWORD64>(player + 0x10 + 0x08 * i, Pos)) return false;
if (!cs2->read<DWORD64>(Pos + 0x70, Pos)) return false;
if (!cs2->read<DWORD64>(Pos + 0x38, Pos)) return false;
if (!cs2->read<point3D>(Pos + 0x120, m_playPawn[i].m_pPos)) return false;
}

DWORD64 Health = 0;
for (size_t i = 0; i < 10; i++)
{
if (!cs2->read<DWORD64>(player + 0x10 + 0x08 * i, Health)) return false;
if (!cs2->read<DWORD64>(Health + 0x08, Health)) return false;
if (!cs2->read<DWORD64>(Health + 0xC0, Health)) return false;
if (!cs2->read<int>(Health + 0x2A8, m_playPawn[i].m_iHealth)) return false;
}
if (!cs2->read<Matrix4x4>(client + 0x19CB4D0, m_mViewMatrix)) return false;
}

效果展示

image-20240817180725647