这是一个cheat engine自带教学的通关实例。我曾经多次学习Cheat Engine但是都没能成功,但是历史是螺旋上升的,经过一段时间的螺旋之后,我对于内存汇编指令流等等的概念认识不断加深,终于来到了上升阶段。
至此,我终于完成了曾经多次尝试并未完成的Cheat Engine教学实例。
这里是一个write up,如果能被搜索引擎检索到的话,帮助想学习Cheat Engine的人过关。
Before start
Cheat Engine包含了两个教学,其中内容包含了内存数据检索,数据修改,多级指针检索,结构体反汇编,汇编修改等知识,非常有趣。这里的方法并不是唯一的方法,并且其中存在出于教学原因,刻意简化的流程,比如说多级指针检索。但是同样的出于教学原因,我将描述的方法是可以稳定完成任务,加深理解的方法。之前多次学习也是因为这步和其他write up的检索结果不一样,导致无法继续。
Cheat Engine Tutorial
Step one
点击 Help
,转到 Cheat Engine Tutorial
,点击之后跳出一个新的窗口,这个教学软件是一个新的进程。点击左上角查找进程,附着到教学软件进程。
在教学进程第一步,直接点击NEXT,进入到第二关。
Step Two
第二关的任务是学习内存扫描,寻找精确数值。我们有100生命值,每次点击Hit me,我们就会损失一定的生命值,通关目标是获得5000的生命值。
思路也很清晰了,首先搜索存在于内存中的生命值地址,直接对该地址进行写入,达到修改生命值的效果。提示中说明了该数值是4 Byte。
下拉栏选择Exact Value(精确数值)和4 Byte,点击 First Scan。会扫描出很多结果,我们目前还不知道哪一个表示了生命值,我们可以通过进一步搜索确定范围。
点击Hit me,会扣除生命值变为99,我们可以在上一步搜索结果之上,进一步缩小结果。将值修改为99,之后点击Next Scan。之后就会确定哪一个是生命值了。如果还有太多的结果,可以重复以上步骤,进一步Hit Me,修改生命值,再次继续搜索。
双击左侧结果,会把结果添加到下侧栏位暂存。双击数值99会弹出一个修改框,将数据改成1000,就会发现可以点击下一关了。
Step Three
这次是未知初始值,我们不知道生命值是多少。同理,我们可以选择 New Scan,Unknown initial value。
之后对于获得的结果,重复上一步的流程,点击Hit me,之后进一步搜索。区别是这一次搜索类型改成Decreased Value。获取内存位置之后修改成5000过关。
Step Four
这一关内容是搜索不同的数值类型,分别搜索flaot和double。之后步骤跟之前一样,不断搜索缩小范围即可。将数值修改成5000以上,过关。
Step Five
这一步教你修改代码,每次点击Change Value,会修改数值。我们不想让它改数值,可以通过patch代码来实现。将修改数值的代码全部改成nop即可。
首先运用之前的技巧,找到对应数据内存位置,将目标地址添加到下边。右键点击,选择找到什么写向这个地址
,弹出一个新的框,再次点击 change value。
新出现的窗口中会弹出一行代码,这表示这行代码访问了对应地址,点击代码,选择右边反汇编。
这里 mov [eax],edx,表示将edx内容保存在内存中eax值的地址,很明显,eax中保存的就应该是数值的地址。每一次修改数值,都会调用这行代码,如果直接将这行代码全都替换成nop,那数值就不会修改了。之后点击change value数值不会变,next亮了,可以下一关了。
另外,对于代码的修改都会保存在Advance Options,方便将代码修改回去。
Step Six
指针!
考虑下边代码片段,该片段初始化了一个int指针,之后向其分配了一个新的动态内存。很明显,每次运行该代码片段,valueptr 指向的位置都会发生变化。也就是说,如果我们重启游戏,我们之前使用的方法找到的地址都会失效,因为new了一个新的地址,旧的地址不起作用了。
int *valueptr = NULL;
valueptr = new int();
对于这个问题,我们可以使用使用指针来解决这个问题。还是上述的代码片段,valueptr 是硬编码在代码中,也就是说,每次启动游戏的时候,valueptr 对于游戏的基址偏移是固定的,所以如果我们能够找到valueptr 的位置,那我们就能获取valueptr 的值,进一步获取valueptr 指向的位置。这样我们就绕过了new的地址不一致的问题。
使用之前学到的技巧,找到value的地址,找到什么写向这个地址
,结果应该如下。
edx中保存的应该就是value的地址,那么我们就要找到这个地址保存在哪个位置中。New Scan,勾选HEX,将value的地址填入,搜索结果如右侧所示,绿色表示该地址相对基址是固定的,也就是我们找到的固定不变的指针位置。双击,添加到下侧Cheat Table。
双击address,复制指针的地址。
点击添加地址,勾选pointer选项,将指针的地址贴到红框位置。可以看出经过解指针之后,已经获得了数据。点击OK,这会在Cheat Table中添加一条新的记录。
勾选左侧选择框,将值改为5000,将之后点击change pointer,下一关。
Step Seven
代码注入,每次点击hit me,会扣一点血,要求我们修改代码,每次点击加两点血。
利用之前的知识,找到血量地址,添加监控,点击 什么写入了这个地址
,再次点击hit me,弹出一行汇编,选中汇编代码,点击右侧show disassembler
。
观察汇编,这里意思是,将内存中一个地址保存的数据减1,这个地址存在ebx中,并且是加上偏移量得来的。
选中该行汇编代码,点击tools,选择下拉框的Auto Assemble。点击 template,AOB injection。点击之后会打开一个AOB脚本。
下边是生成的脚本,enable表示脚本工作的时候执行的代码,disable表示取消代码之后,将原本代码替换回去。aob脚本表示在软件中寻找一段独一无二的段落,作为定位特征。注释掉code中的sub,在newmem中添加add。将修改之后代码添加到mewmem位置。
[ENABLE]
aobscanmodule(INJECT,Tutorial-i386.exe,83 AB A4 04 00 00 01) // should be unique
alloc(newmem,$1000)
label(code)
label(return)
newmem:
add dword ptr [ebx+000004A4],02
code:
// sub dword ptr [ebx+000004A4],01
jmp return
INJECT:
jmp newmem
nop 2
return:
registersymbol(INJECT)
[DISABLE]
INJECT:
db 83 AB A4 04 00 00 01
unregistersymbol(INJECT)
dealloc(newmem)
点击保存,这脚本会被保存在下侧Cheat Table处。启用之后发现每次点击会+2血,过关。
Step Eight
多级指针!
考虑下列代码片段。这里面healptr就是一个多级指针,根据上一节的知识,main中基本所有的内容都是new出来的,但是可以根据world指针一层一层寻址,找到person中的health值。同理,如果我们能够确定person的health的地址,不断回溯,从health到person到group到world,world偏移是固定的,这样就能每次都找到health的值了。
struct World
Group *group = NULL;
}
struct Group {
Person *person= NULL;
}
struct Person {
int *id = NULL;
int *health = NULL;
}
int main() {
int healptr = NULL;
World *world = new World;
world->group = new Group;
world->group->person = new Person;
world->group->person->health = new int();
healptr = world->group->person->health;
// 这里相当于 healptr = (world->group->person) + 4
// health可以替换成 指针保存的地址之后加上偏移量
*healptr = 1;
}
首先找到value的地址,之后按照第六步的内容,点击什么向这个地址写
,获取汇编代码。esi内保存的是结构体的基址,18是针对基址的偏移量。所以基址应该是value的地址减去0x18
。即0x18E6B78。
这个基址的地址应该保存在内存的另一个地址中,直接在内存中搜索。勾选hex,直接搜,之后添加到cheat table,这就是上一层的指针位置。
重复上边的步骤,右键点击新添加的指针位置,选择 什么访问了这个地址
。change value,捕获到新的汇编代码。esi保存的基址地址,这里没有偏移量,说明该指针是上层结构体的第一歌成员,那偏移量就是0。
仿照上一步的行为,在内存中找保存基址的变量位置。
接下来都一样了,我简单记录一下截图。可以进行对比
下一步的地址为0x0188DAB4
,偏移量为0xC
,搜索0x188DAA8
最后一步找到了相对程序基址固定的地址。
这里有一个偏移量对照表,基本格式是 ptr -> address + offset = new address
,主要关注的是offset。这里分别是0xC,0x14,0x0,0x18。
018E6B90 = Value = 2871
0188DB38 -> 18E6B78 + 18 = 018E6B90
0190DD34 -> 0188DB38 + 0 = 0188DB38
0188DAB4 -> 190DD20 + 14 = 0190DD34
"Tutorial-i386.exe"+2566E0 -> 188DAA8 + 0C = 0188DAB4
ptr -> address + offset = new address
点击监控框右上角的Add Address Manually
,勾选pointer,填入固定地址,将按照顺序分别填入offset,点击ok。
改成5000,change pointer,过关。
Step Nine
这关是这样的,有四个玩家,player 1,2是我们队伍,3,4是对面队伍。我们想要想一个办法,只扣对面的血,不扣自己的血。
我们不能像之前一样,直接把扣血的代码patch成nop,因为这样会使大家都不口血了,我们要找到一个方法来区分我方和对方的玩家,扣血的时候检查一下,如果是我方就不扣血,是对方就扣血。
我们猜测player结构体可能是下边这样。
struct Player {
int id;
char *name;
int health;
int teamnumber;
}
那对于C代码来说,修改之后的代码可能是这样的。通过区分teamnumber来改变指令流
int hit(Player* player) {
if(player -> teamnumber == 1)
player -> health = player -> health - 1;
else
DO Nothing.
}
我们只要把上述的 if 部分改成汇编代码注入即可了。
首先,运用前边学到的只是,寻找Eric的血量地址。使用 查找什么向这个地址写
寻找到player结构体的基址。
这里血量对结构体偏移量为4。
打开内存查看器,反编译结构体,把0x7278428作为基址地址。 (Dave血量地址 - 0x4)
点击上方 Structures,define new structures。这就会自动反汇编结构体。可以看见用户名称的字符串,用户的血量,等等。对于其他三个player执行相同的步骤。左上角File,可以添加用户,添加组。
我们看出,偏移量为0x10的位置是团队标记,可以根据这个区分玩家所属。按照之前学到的方法,在修改血量的地方进行AOB代码注入。别忘了保存到Cheat Table
[ENABLE]
aobscanmodule(INJECT,Tutorial-i386.exe,89 43 04 D9 EE) // should be unique
alloc(newmem,$1000)
label(code)
label(return)
newmem:
// 在这里插入新的代码
// 如果 ebx+10 的地方不是1 那就会跳转到code位置 正常执行原来的逻辑
cmp [ebx+10],1
jne code
// 如果 这里是1 那我们将修改血量的代码去掉 主要要把原来的代码补全 比如这里的fldz
fldz
// 跳转到结束位置
jmp return
code:
mov [ebx+04],eax
fldz
jmp return
INJECT:
jmp newmem
return:
registersymbol(INJECT)
[DISABLE]
INJECT:
db 89 43 04 D9 EE
unregistersymbol(INJECT)
dealloc(newmem)
完结
这就是全部了,谢谢阅读。
多级指针可能令人困惑,但是可以正向思考,联系C代码的运作基址,辅助思考。汇编部分其实也不难,就掌握几个简单的命令即可。