Joe1sn's Cabinet

【破解】CS2人物实体逆向

如何结合Cheat Engine和逆向工程找到CS2内存中的人物地址

或许我们的公众号会有更多你感兴趣的内容

img

这篇文章发布的时候,关于更多CS2-cheats的代码已经在Github仓库中发布,你可以在下面的链接找到更多CS2的外挂功能

https://github.com/Joe1sn/ExtCheats

游戏环境

Steam上启动的CS2国际服,在设置中加上-insecure参数

image-20240520124758128

在游戏中启用控制台,然后 ` 就可以输入命令了

CS2可以使用CFG文件进行快速的加载,这里我用了一段cfg脚本来进行编写

1
2
3
4
5
6
7
8
9
10
11
12
sv_cheats 1
mp_roundtime 60
mp_roundtime_defuse 60
mp_warmup_end
mp_freezetime 0
mp_maxrounds 30
mp_buytime 99999
bot_stop 1
bot_dont_shoot 1
mp_respawn_on_death_t 1
mp_respawn_on_death_ct 1
mp_restartgame 1

你可以在steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\cfg中防止该文件,然后再游戏中使用exec <不含后缀的文件名>

image-20240520125021129

image-20240520125058628

为了方便调试,可以把屏幕大小改为1280x600

image-20240520125243904

PlayerPawn

简单的CE使用

首先使用准确值找到HP值

image-20240520125402361

在游戏中可以使用hurtme xx来对自身角色造成伤害

image-20240520125441150

image-20240520125456873

这样就找到了几个类似的值,这里有几个要点

  • 由于在真实游戏中我们能控制的只有客户端,没有服务端程序,CS的客户端位于client.dll中,所以我们需要对其进行分析,而且地址最好和该dll相关
  • cs2使用了Valve研发的source2引擎,所以我们可以利用相关开源信息进行查找,比如有人做了cs2偏移的仓库:https://github.com/a2x/cs2-dumper

接着找出有哪些地址访问了这些地址,这里有一个取巧的方法,利用上面的偏移,比如HP的全称是Health Point,那么变量的命名就可能和heal相关,在上面推荐的仓库就可以找到

image-20240520130140134

那么对上面的内存找访问

image-20240520130240365

image-20240520130346862

添加该RCX的值,在 浏览相关内存->工具->解析结构体中

image-20240520130516581

image-20240520130529294

image-20240520130615406

我们发现了一个为C_CSPlayerPawn的结构体,接着我们看访问的代码,有两段

image-20240520140501547

image-20240520140630234

那我们就打开client.dll,分析下这段代码

文件位于steam\steamapps\common\Counter-Strike Global Offensive\game\csgo\bin\win64

第一段可能为虚函数

image-20240520140659150

第二段位于另外一个函数中,我们主要看rsi怎么取到的值,结果发现是这个函数的参数

image-20240520140754947

多看看交叉引用发现引用太复杂,随后放弃

PlayerController

从全局变量找到Controller

继续按照上一面的找搜索到的HP的一堆地址的访问地址,发现

image-20240520135821227

image-20240520135923299

同样的方法我们找到这段代码,然后在IDA中分析

image-20240520131122904

IDA中的基地址从0x180000000开始,加上偏移521BB0就找到了这段代码,然后对该函数的引用分析

image-20240520131226822

发现普遍存在这几个函数

image-20240520131345205

我们先分析下他是这么找到v12的,首先在sub_180697FA0传入了一个v10

先分析sub_180697FA0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall sub_180697FA0(int a1)
{
__int64 v1; // rax
__int64 v2; // rbx
char v3; // al
__int64 v4; // rcx

if ( !off_18172EEE0 )
return 0i64;
if ( a1 < 0 )
return 0i64;
if ( a1 >= *(off_18172EEE0 + 4) )
return 0i64;
v1 = sub_18060A050(qword_18191C5B8, (a1 + 1));
v2 = v1;
if ( !v1 )
return 0i64;
v3 = (*(*v1 + 0x480i64))(v1);
v4 = 0i64;
if ( v3 )
return v2;
return v4;
}

存在全局变量qword_18191C5B8,在CE中分析一下

分析其中的sub_18060A050

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall sub_18060A050(__int64 a1, int a2)
{
__int64 v2; // rcx
_DWORD *v3; // rcx

if ( a2 <= 0x7FFE
&& (a2 >> 9) <= 0x3F
&& (v2 = *(a1 + 8i64 * (a2 >> 9) + 16)) != 0
&& (v3 = (120i64 * (a2 & 0x1FF) + v2)) != 0i64
&& (v3[4] & 0x7FFF) == a2 )
{
return *v3;
}
else
{
return 0i64;
}
}

其中a1为全局变量

image-20240520131727224

a2暂时未知,不过我们可以根据fastcall的传参顺序( rcx,rdx,r8,r9)或者汇编来看a2传递的是什么参数

image-20240520132002915

image-20240520132204382

编写python算法模拟一下,但是条件中内存有指针,那没有两种思路

  • 利用调试器取值
  • 直接都程序内存

这里用第一种,先不管最后一个条件,计算得到v2 = 0000019CA3550808

然后算出v3 = 0x7ffab466e718

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def sub_18060A050():
a1 = 0x000019CA3AD1800
a2 = 1
print(hex(a1 + 8 * (a2 >> 9) + 16))
v2 = 0x000019CA3550808
v3 = 120 * (a2 & 0x1FF) + v2
if (a2 <= 0x7FFE
and (a2 >> 9) <= 0x3F
and v2 != 0
and v3 != 0
# and (v3[4] & 0x7FFF) == a2
):
print(hex(v3))


if __name__ == "__main__":
sub_18060A050()

用CE看一下*v3这个指针的值

image-20240520134938594

那么返回的就是00007FFAB4652500,用CE发现是一个叫做CCSPlayerController的数据结构

image-20240520135116592

看下github上的偏移表

image-20240520135214649

发现有几处关键信息:

image-20240520135330110

这样就可以通过client.dll + 191C5B8加上下标,通过刚才的函数,找到了CCSPlayerController开始的地址

Controller到Pawn

我们继续逆向,根据Controller+0x7E4m_hPlayerPawn,就用CE看看这个地址有无读写,在观察下汇编发现

image-20240520141741112

由于CE捕获到的两段代码十分相近,那么给rax赋值的话rax引用就可能出现,搜索对[rax]访问找到

image-20240520142242702

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    v11 = *a4;                                  // //////////////
if ( *a4 != -1
&& qword_181819538
&& v11 != -2
&& (v12 = *(qword_181819538 + 8 * ((v11 & 0x7FFF) >> 9))) != 0
&& (v13 = (v12 + 120i64 * (v11 & 0x1FF))) != 0i64
&& *(v13 + 4) == v11 )
{
v14 = *v13;
}
else
{
v14 = 0i64;
}
if ( v10 != v14 )
break;
++v5;
++a4;
if ( v5 >= a5 )
return 0i64;
}
return 2i64;
}

其中v11就是我捕获到的mPawn值,这里再次出现了全局变量client.dll+1819538

image-20240520142752026

同之前编写脚本计算地址,然后再对照,这里就不再赘述过程了。

得到从CCSPlayerController.mPawn找到对应CSPlayerPawn的方法,这段代码在项目的CheatsPlayer类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Player::GetPawn() {
DWORD m_hPlayerPawn = GetProcessMem(hProcess, this->PlayerControllerAddr, 2, 0, CSPlayerController::m_hPlayerPawn);

if (this->ClientDLLBase == 0 || this->hProcess == NULL)
return;
DWORD64 entity_list = GetProcessMem(hProcess, this->ClientDLLBase + ClientDLL::C_CSPlayerController, 1, 0);
if (!entity_list)
return;
DWORD64 list_entry = GetProcessMem(hProcess, entity_list + (8 * (this->Index & 0x7FFF) >> 9) + 0x10, 1, 0);
if (!list_entry)
return;
DWORD64 playerPawn = m_hPlayerPawn;
if (!playerPawn)
return;
DWORD64 list_entry2 = GetProcessMem(hProcess, entity_list + 8 * ((m_hPlayerPawn & 0x7FFF) >> 9) + 0x10, 1, 0);
if (!list_entry2)
return;
this->PlayerPawnAddr = GetProcessMem(hProcess, list_entry2 + ClientDLL::C_CSPlayerController_Gap * (m_hPlayerPawn & 0x1FF), 1, 0);
}

Controller的数组

到这里算是总结了吧。之前从全局变量找到Controller,有许多没用的比较和逻辑运算,这些是为了对其和限制大小

找到Controller的就可变为,改代码位于项目的Cheats.cpp中,在Cheats的构造函数中

1
2
3
DWORD64 ListOffsetA = GetProcessMem(this->hProcess, this->ClientDLLBase + ClientDLL::C_CSPlayerController, 1, 0);
DWORD64 v2 = GetProcessMem(this->hProcess, ListOffsetA + 8 * (1 >> 9) + 16, 1, 0);
this->ControllerBase = v2 + ClientDLL::C_CSPlayerController_Gap;

image-20240520143956409