CS1.6破解笔记

​ 记得去年的这个时候上大二,去给23级的学弟学妹作纳新演讲,当时和hcj坐在第一排看学长演示破解环节来着,辉哥演示了破解植物大战僵尸的一系列操作,然后我就随口跟hcj打趣说,明年我就弄个CS1.6的破解(bushi),这颗子弹飞了一年,也是正中我的眉心了,确实也不是很难的一个事情,但是我也是认认真真学了不少东西,跟着网上的现有资料,一步步复现出了这个东西,记录一下捏~

0x01 无限金钱

​ 其实在今年清明假期的时候,留学校了没出去玩,当时研究过一两天,记得当时就什么也不管,上来就开始用CE找机制,看变化找基址,后面给显示在屏幕上的目标数值的基址找到了,傻傻地以为这就是真正控制进程中的目标内容的基址,后面才反应过来这个事儿,也是因为技术太抽象踩上大坑了。。后面就忙别的事儿去了,直到这学期开始纳新,说是需要搞点活儿,我就又才弄这个东西。

​ ok,正文开始,首先我们都知道,运行一个应用程序的时候,程序的可执行文件被加载到内存的某个地址空间。这个地址空间是程序在运行时独占的虚拟地址空间,程序并不知道具体的物理内存地址。通俗来说就是每次加载一个进程的时候,会给它分配一个总的基地址,这个基地址映射在真正的物理地址的某处位置,然后程序中的各个变量以及函数的地址也同样会被加载到这个所谓的虚拟地址,而程序中的各种繁琐的变量是根据什么来确定它该呆的地方呢?如果随意加载到任意内存位置的话,那么多的内容该怎么确保不充分呢?所以这里面存在有一些不变的偏移量,根据加载程序之后的基址来加上不变的偏移值,一步一步计算出每一次存储对应变量的真实内存地址。

​ 所以破解的流程就是使用CE一层层地去寻找对应变量的偏移量,从而层层下去一直到程序的基址。

​ 首先来找一下游戏中代币的基址,启动CE,然后附上进程,先直接搜索16000这个数值,默认该值为一个int类型所存储起来的,故类型选择4字节,然后首次搜索之后看到的绿色地址为基址

image-20241002161353905

​ 为了进一步确定该变量的实际地址具体为哪一个,我们花费一些代币,然后再次搜索数量值来确定地址,可见变化为10250的有两个地址,一开始我傻傻以为第一个绿色的基址就是我要找的,然后开心地给这个地址锁死了,快乐地开始消费,结果后面钱就不够了,但是我游戏界面的数额却一直都没有变化,后面左思右想才知道,原来表示游戏代币金额的有两个地址,刚刚的这个绿色基址是用来表示游戏界面上的显示金额的地址,也就是说锁死这个地址不变也就只能是控制我们看到的游戏界面的数额不变,并不是实际用来计算游戏商店中消费的金额代币,那么这样的话,下面的白色的地址即是用来存储一层偏移内容的地址。

image-20241002161509427

​ 由于这是最后一层,且这个地址里面存储的值确切地为我们的游戏代币的值,所以给它加到下面之后,选择找出是什么改写了这个地址选项,然后进入游戏再消费一波

image-20241002161642102

然后跟进这个汇编指令查看详情,由下图可知,很明确的最后一层偏移的值为1CC,同时我们再取出此时寄存器ESI中的值,该值是一个动态的地址值,既是控制代币数量的上一层地址的值

image-20241002161745316

复制出这层地址,然后重新搜索

image-20241002162503469

首先排除连续存在的这种地址存储目标内容的可能,下图中先查看最后一个地址,此处选择是什么访问了这个地址,因为这里不再是修改代币的数量内容的过程了,而是当这个游戏进程跑起来之后,它需要实时地去监控这一条地址链上的值,看是否对最终存放代币数量的那个地址进行了修改访问,而此处最后一个偏移已经被拿下,我们需要一层层往回推,所以此处应该选择访问

image-20241002162555128

观测这几个地址之后发现只有倒数第二个地址10336BF8有实时地去访问行为,故第二层偏移为7C,像上面那样继续往下找下去

image-20241002163258941

image-20241002163525467

10336B7C复制出来之后继续扫描,此时你就会发现我们找到绿色的基址了,首先这两个绿色的基址,代表两个东西,一个是游戏界面中的游戏代币的实时数额,另一个就是我们正在寻找的游戏进程加载之后存储游戏代币的真正基址的值

image-20241002163604495

给其中一个拉下去之后添加我们之前寻找到的偏移,就会发现加载出来的值和我们此时的金钱数相同

image-20241002164146598

然后锁死这个地址之后购买一堆武器发现金钱并没有掉值,所以证明我们寻找正确

image-20241002164420518

上述对于金钱基址寻找的例子同样适用于其他数值的搜寻,例如hp、主武器子弹、护甲等等,此处就不一一寻找了,将该过程应用到代码中的片段贴出如下:

1
2
3
4
5
6
7
8
9
10
11
12
void changeMoney()
{
vector<unsigned int> offsets;
offsets.push_back(0x11069BC);
offsets.push_back(0x7C);
offsets.push_back(0x1CC);
uintptr_t addr = FindDMAAddy(BaseAddress,offsets);
//写入实际的money
WriteProcessMemory(handle, (LPVOID)addr, &money, sizeof(money), 0);
//写入显示的money
WriteProcessMemory(handle, (LPVOID)(BaseAddress+0x61B9FC), &money, sizeof(money), 0);
}

0x02 透视实现

实现完锁钱和锁血一系列操作之后,寻思搞一搞透视,感谢mz1大佬的文章和视频,讲得很通透,直接去hook opengl这个库中用于渲染遮挡物的字节码处,替换一下,只能说相当巧妙,写成了一个动态链接库,用注入器注入到目标进程即可,具体核心部分代码内容如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
BOOL setInlineHook(DWORD dwOldAddr, DWORD dwNewAddr) {
BOOL bRet = FALSE;
dwRetAddress = dwOldAddr + PATCH_LENGTH; // hook结束以后的返回地址
BYTE byJmpCode[PATCH_LENGTH] = { 0xe9 };
DWORD dwOldProtect;

// 之前的字节码
static BYTE byOriginalCode[PATCH_LENGTH] = { 0 };
static BOOL bHookFlag = FALSE;

// 测试用
char buf[1024] = { 0 };


// 1. 初始化byJmpCode
memset(&byJmpCode[1], 0x90, PATCH_LENGTH - 1);

// 2. 存储跳转地址
*(DWORD*)&byJmpCode[1] = (DWORD)dwNewAddr - (DWORD)dwOldAddr - 5;

// 3. 备份被覆盖的code
memcpy(byOriginalCode, (LPVOID)dwOldAddr, PATCH_LENGTH);


// 4. 开始patch
if (!bHookFlag)
{
// hook
VirtualProtect((LPVOID)dwOldAddr,
PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 这个是成功执行的


memcpy((LPVOID)dwOldAddr, byJmpCode, PATCH_LENGTH); // 这里拷贝的时候有问题,会出现不可写的问题


VirtualProtect((LPVOID)dwOldAddr, PATCH_LENGTH, dwOldProtect, 0);
bHookFlag = TRUE;
bRet = TRUE;
}
else {
// 取消hook
VirtualProtect((LPVOID)dwOldAddr,
PATCH_LENGTH, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy((LPVOID)dwOldAddr, byOriginalCode, PATCH_LENGTH);
VirtualProtect((LPVOID)dwOldAddr, PATCH_LENGTH, dwOldProtect, 0);
bHookFlag = FALSE;
bRet = TRUE;
}

return bRet;
}

char szNewText[] = "Hook!";
void __declspec(naked) MyglBegin(GLenum mode) {
// GL_TRIANGLE_STRIP 0x0005
// GL_TRIANGLE_FAN 0x0006
__asm {
// 1. 保存寄存器
pushad;
pushfd;
// 2. 修改数据

// 判断mode 原[esp+8] 现在的[esp+32+4+4]
mov eax, dword ptr[esp + 32 + 4 + 4]; // eax=mode
cmp eax, 5;
jz disable;
cmp eax, 6;
jz disable;
jmp back_to_func;
}
disable:
// 调用glDisable
glDisable(GL_DEPTH_TEST);
__asm {

back_to_func:
// 3. 恢复寄存器
popfd;
popad;
// 4. 执行覆盖的代码
mov edi, edi;
push ebp;
mov ebp, esp;
// 5. 返回执行
jmp dwRetAddress;
}
}

具体效果:

image-20241002170033786