说明
作者学生一个,技术水平有限,而且学习方向也不是破解逆向相关的,有错误的地方还望大佬指点。
教程以交流技术为主要目的,只学技术。
关于教程
有很多东西作者是不懂的,毕竟作者当前水平也是处于[hello world!]级别
学习基础
本篇默认您有 CE 基础,可以将 CE 自带的 Tutorial 轻松破解,文中有很多东西都是用破解 Tutorial 的操作来说明的。
涉及内容
- 查找数值、查找未知数值,以及相关技巧
- 指针查找的技巧
- 代码注入
- 调试技巧
- Cheat Table 的一些功能
教程中会夹杂大量的CE使用技巧以及一些破解的常识,这些技巧和常识的作用可能比使用 CE 的熟练度更大(就像电子竞技中的意识与操作一样)。
使用什么游戏?
版权问题
仅仅分享本文章的链接的话,您可以随意分享。
如果要全文转载的话,本文使用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可证发布,具体注意事项请阅读协议内容(可以参考此协议的中文翻译)。
转载的话最好在下面回帖告诉我一声。
请注意,本文仍有可能更新,全文转载的话可能需要考虑更新的问题。
所需软件
- Ghidra
- Cheat Engine 7.2
- x64dbg(Scylla Plugin)
- Visual Studio
- Processhacker
- BattlefieldV
- ……
基础知识(此处以6.7版本为例,实际应安装7.2)
简介
Cheat Engine:作弊引擎,简称为 CE。
- 通常用于单机游戏的内存数据修改,可以搜索游戏中的内存数据,并进行修改、锁定等操作
- 内置调试器,可以进行反汇编调试、断点跟踪、代码注入等诸多高级功能
- 支持 lua 语言,可以实现自己定义的逻辑功能,而不仅仅是简单的锁定数据。也可以在代码注入的同时注入 lua 插件,使游戏进程与 CE 进程进行交互。 CE 的大部分功能都可以通过 lua 来操作
- 它还支持 D3D Hook 功能,可以在游戏中显示十字准星,也可以绘制功能菜单,同时也可以对 D3D 的调用栈进行跟踪调试
- 自带变速功能,通过 Hook 游戏相关函数改变游戏速度
- 自带了一个 Trainer 的功能,可以将自己在 CE 中实现的功能单独生成一个 exe 文件,并可以使用 lua 创建比默认样式更加复杂的窗体或功能
- 除了 CE 界面上提供的功能,可以 lua 引擎使用更多隐藏功能,具体就要看帮助文档了
安装 CE
(安装过程略)
Cheat Engine 6.7 在 Windows 10 下的默认安装位置为:C:\Program Files (x86)\Cheat Engine 6.7,可以使用其他安装目录,不过记着把程序放在哪了就行(记不住其实也无所谓…),之后可能要到这个目录去找一些东西。使用中文翻译文件
官方翻译文件的下载链接在文章末尾。
首先解压到类似 C:\Program Files (x86)\Cheat Engine 6.7\languages\ch_cn 的文件夹
然后到 CE 的设置中,语言选择 ch_cn
重启 CE 即可打开 CE
最新版的 CE 有两个主程序,一个 32 位的,一个 64 位的,基本没什么区别,不存在其他一些调试器 32 位无法调试 64 位程序的问题,通常直接通过开始菜单 > Cheat Engine 6.7 > Cheat Engine 6.7 打开就可以了,不必区分 32 位或者 64 位。
打开 CE 会触发 UAC(User Account Control,用户账户控制),就是用管理员权限打开咯,单纯的搜索内存是不用这个权限的,只有调试的时候某些特殊的功能才可能用到管理员权限,肯定点“是”啊(如果点否的话,你可以顺便关闭这个帖子了…)界面简介
上面是菜单栏,暂时用不到。
然后有个一直在闪的按钮是进程选择器,旁边两个是打开和保存 CE 的存档文件的按钮,记得随时保存哦,改内存数据很可能导致游戏崩溃的哦,如果调试的话连 CE 也会一起崩溃。
然后是当前选择的进程的名称。
下面是一个进度条,显示搜索进度用的。
右边有个设置按钮。
然后左边是搜索到的内存地址,右边是搜索的类型、内容、条件、内存区域等等,下面有个↘右下箭头,是把选中的地址添加到下方的区域里。
下方称之为 Cheat Table(作弊表),就是我们得到的内存地址、数据,以及自己添加的功能等等。
表格上方三个按钮:显示内存窗口、清空作弊列表、手动添加地址。下方两个按钮:高级选项和附加信息。
内存窗口用于调试,手动添加地址通常是手动添加带指针的地址,高级选项中储存了一些指令,还可以暂停当前进程,附加信息可以附带一些使用方法、作者信息等等,高级选项中的指令与附加信息会一同保存在存档中。
战地V
你们看到的,教程里写的,都是我真实经历过的,但这不代表我仅仅经历了这些,实际付出的心血至少是描述出来的好几倍。教程里只写了尝试成功了的部分,尝试失败了的话只有继续尝试。
我们要做什么形式的外挂?
游戏的目的是打架,要是能提前知道敌人的位置就可以了嘛。
我目前只研究出来了第一个,而且还不是很完善。
外挂是 1% 的灵感加上 99% 的汗水。
我能想到的辅助方法有这些,你们有什么想法可以自己尝试。
搜索健康值(血量)
首先,我们猜测,储存健康值数值的是 Float 类型。
其次,一般来说满血量时健康值为100.0f,但是我们默认按未知的数值处理。
想偷懒也可以,直接看人物血量数值,游戏UI有提示。
好的开始搜索。
使用 Scan Type: Unknown initial value、Value Type: Float 方式进行 First scan。
然后回到游戏,拿手雷丢到身旁,或让敌人打自己几枪。尽量健康值变化大一些,注意不要死亡,避免因为变化太小,导致 CE 忽略了变化量。
如果血量增加就用 CE 搜索 Increased value。
如果血量减少就用 CE 搜索 Decreased value。
经过了数轮的查询,我这里还剩下 若干个数值。这就是当今的游戏引擎,实在烦人,弄出这一堆没用的中间变量,改了也没用,还得自己手动筛选,你如果研究老游戏的时候根本不会出现这么多数值。
此处我剩余18个。一般这其中前16个不会是我们所需要的信息。所以我们从后两个排查。
由于190D9DD90与190D9DDB0相差不大所以应该在一个结构体中
此时我们对190D9DD90右键找出什么访问了这个地址
可以明显的看到血量的偏移是20
RCX=0000000190D9DD70
此时搜索
现在逐个使用右键找出什么访问了这个地址
反复再次扫描 让自己减少健康值,升高健康值
逐个使用右键找出什么访问了这个地址
删除健康值变化后无访问的地址
得到如图
bfv.exe+6B98370 - 40 57 - push rdi
bfv.exe+6B98372 - 48 83 EC 40 - sub rsp,40 { 64 }
bfv.exe+6B98376 - 48 8B 41 58 - mov rax,[rcx+58]
bfv.exe+6B9837A - 48 89 CF - mov rdi,rcx
bfv.exe+6B9837D - 48 85 C0 - test rax,rax
bfv.exe+6B98380 - 0F84 BB020000 - je bfv.exe+6B98641
bfv.exe+6B98386 - 48 8B 08 - mov rcx,[rax]
bfv.exe+6B98389 - 48 85 C9 - test rcx,rcx
bfv.exe+6B9838C - 0F84 AF020000 - je bfv.exe+6B98641
bfv.exe+6B98392 - 48 89 6C 24 60 - mov [rsp+60],rbp
bfv.exe+6B98397 - 48 8D 69 F8 - lea rbp,[rcx-08]
bfv.exe+6B9839B - 48 85 ED - test rbp,rbp
bfv.exe+6B9839E - 0F84 98020000 - je bfv.exe+6B9863C
bfv.exe+6B983A4 - 48 89 5C 24 58 - mov [rsp+58],rbx
bfv.exe+6B983A9 - 48 8B 9D E8020000 - mov rbx,[rbp+000002E8]
mov rax,[rcx+58]
mov rcx,[rax]
lea rbp,[rcx-08] // mov rbp,rcx-08
mov rbx,[rbp+000002E8] // mov rbx,[rcx+000002E0]
void FUN_146b98370(longlong param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4)
{
longlong lVar1;
char cVar2;
longlong lVar3;
code **ppcVar4;
float fVar5;
undefined4 in_XMM6_Da;
undefined4 in_XMM6_Db;
undefined4 in_XMM6_Dc;
undefined4 in_XMM6_Dd;
undefined4 in_XMM8_Da;
float fVar6;
undefined4 in_XMM8_Db;
undefined4 in_XMM8_Dc;
undefined4 in_XMM8_Dd;
float afStackX8 [2];
undefined auVar7 [16];
undefined auVar8 [16];
if (((*(longlong **)(param_1 + 0x58) != (longlong *)0x0) &&
(lVar1 = **(longlong **)(param_1 + 0x58), lVar1 != 0)) && (lVar1 + -8 != 0)) {
ppcVar4 = *(code ***)(lVar1 + 0x2e0);
auVar8 = CONCAT412(in_XMM6_Dd,CONCAT48(in_XMM6_Dc,CONCAT44(in_XMM6_Db,in_XMM6_Da)));
auVar7 = CONCAT412(in_XMM8_Dd,CONCAT48(in_XMM8_Dc,CONCAT44(in_XMM8_Db,in_XMM8_Da)));
if ((ppcVar4 == (code **)0x0) ||
(lVar3 = (**(code **)*ppcVar4)(),
(uint)DAT_144c563aa - (uint)DAT_144c563a8 <
(uint)*(ushort *)(lVar3 + 0x48) - (uint)DAT_144c563a8)) {
ppcVar4 = (code **)0x0;
}
afStackX8[0] = *(float *)(ppcVar4 + 0x22);
lVar3 = *(longlong *)(param_1 + 0x70);
fVar6 = 0.0;
if (afStackX8[0] <= 0.0) {
afStackX8[0] = 0.0;
}
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60((longlong *)(param_1 + 0x70),afStackX8,1,param_4,auVar7,auVar8);
}
afStackX8[0] = (float)thunk_FUN_1466077f0(ppcVar4);
lVar3 = *(longlong *)(param_1 + 0x80);
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60(param_1 + 0x80,afStackX8,1,param_4,auVar7,auVar8);
}
afStackX8[0] = (float)thunk_FUN_1466077c0(ppcVar4);
lVar3 = *(longlong *)(param_1 + 0x78);
if (afStackX8[0] <= fVar6) {
afStackX8[0] = fVar6;
}
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60(param_1 + 0x78,afStackX8);
}
afStackX8[0] = (float)thunk_FUN_1466077c0(ppcVar4);
lVar3 = *(longlong *)(param_1 + 0x88);
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60(param_1 + 0x88,afStackX8);
}
afStackX8[0] = (float)FUN_140970b10(ppcVar4);
lVar3 = *(longlong *)(param_1 + 0x98);
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60(param_1 + 0x98,afStackX8);
}
lVar3 = *(longlong *)(param_1 + 0x90);
afStackX8[0] = *(float *)((longlong)ppcVar4 + 0x114);
if (*(float *)((longlong)ppcVar4 + 0x114) <= fVar6) {
afStackX8[0] = fVar6;
}
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar3 + 0x38))))) {
thunk_FUN_145d5fb60((longlong *)(param_1 + 0x90),afStackX8);
}
afStackX8[0] = *(float *)(ppcVar4 + 0x24);
lVar3 = *(longlong *)(param_1 + 0xb0);
if ((lVar3 != 0) &&
((((byte)*(undefined4 *)(lVar3 + 0x48) & 0x60) != 0x60 ||
(**(float **)(lVar3 + 0x38) != afStackX8[0])))) {
thunk_FUN_145d5fb60((longlong *)(param_1 + 0xb0),afStackX8,
CONCAT71((uint7)(uint3)((uint)afStackX8[0] >> 8),1));
}
cVar2 = thunk_FUN_1466ba870(lVar1 + -8);
if (cVar2 == '\0') {
fVar5 = (float)(**(code **)(*ppcVar4 + 0x170))(ppcVar4);
afStackX8[0] = (float)(**(code **)(*ppcVar4 + 0x178))(ppcVar4);
afStackX8[0] = fVar5 / afStackX8[0];
}
else {
afStackX8[0] = 1.0;
}
lVar1 = *(longlong *)(param_1 + 0xa0);
if (afStackX8[0] <= fVar6) {
afStackX8[0] = fVar6;
}
if (1.0 <= afStackX8[0]) {
afStackX8[0] = 1.0;
}
if ((lVar1 != 0) &&
((((byte)*(undefined4 *)(lVar1 + 0x48) & 0x60) != 0x60 ||
(afStackX8[0] != **(float **)(lVar1 + 0x38))))) {
thunk_FUN_145d5fb60(param_1 + 0xa0,afStackX8,1);
}
}
return;
}
根据尝试 rbp-50应该是局部变量
所以我们将目标锁定到
+2E8或+2E0上,根据老外的命名我们暂且将
跑到ghidra 可以看到ppcVar4 = (code **)(lVar1 + 0x2e0);
现在知道本地健康血量数值=[190D7A4B0+2E8]+20=[190D7A4B8+2E0]+20
继续搜索190D7A4B0
有点多换190D7A4B8
先从地址长的分析 发现没有访问
因此我们将目标锁定在B4AB06E0
找到计数次数最多的
RAX=[[RCX+00001D50]]
longlong GetClientSoldier(longlong param_1)
{
longlong lVar1;
if ((*(longlong **)(&DAT_00001d50 + param_1) != (longlong *)0x0) &&
(lVar1 = **(longlong **)(&DAT_00001d50 + param_1), lVar1 != 0)) {
return lVar1 + -8;
}
return 0;
}
RCX=0000000102E4C050
未完待续。。。。
现在找到了本地健康血量数值=[[[ClientPlayer+1D48]]+2E8]+20=[[[102E4C050+1d50]]+2e0]+20
搜索谁改写了ClientPlayer
首先,由于ClientPlayer在人物活动状态下,一般是不会改变ClientPlayer*的数值。根据习惯猜测
所以,我们用ProcessHack先暂停游戏。
bfv.exe+1629BA5 - 49 8B 85 E0000000 - mov rax,[r13+000000E0]
bfv.exe+1629BAC - 49 33 44 24 30 - xor rax,[r12+30]
bfv.exe+1629BB1 - 48 89 85 C0020000 - mov [rbp+000002C0],rax
bfv.exe+1629BB8 - 4C 8D 85 C0020000 - lea r8,[rbp+000002C0]
bfv.exe+1629BBF - 48 8D 95 48060000 - lea rdx,[rbp+00000648]
bfv.exe+1629BC6 - 49 8D 4D 10 - lea rcx,[r13+10]
bfv.exe+1629BCA - E8 0178FFFF - call bfv.exe+16213D0
bfv.exe+1629BCF - 41 8B 4D 20 - mov ecx,[r13+20]
bfv.exe+1629BD3 - 49 8B 45 18 - mov rax,[r13+18]
bfv.exe+1629BD7 - 4C 8B 85 48060000 - mov r8,[rbp+00000648]
bfv.exe+1629BDE - 4C 3B 04 C8 - cmp r8,[rax+rcx*8]
bfv.exe+1629BE2 - 74 11 - je bfv.exe+1629BF5
bfv.exe+1629BE4 - 49 8B 48 08 - mov rcx,[r8+08]
bfv.exe+1629BE8 - 48 8B 01 - mov rax,[rcx]
bfv.exe+1629BEB - 4C 8B 00 - mov r8,[rax]
bfv.exe+1629BEE - 49 8B D4 - mov rdx,r12
bfv.exe+1629BF1 - FF D0 - call rax
看到xor应该是加密了