这是一个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 ,点击之后跳出一个新的窗口,这个教学软件是一个新的进程。点击左上角查找进程,附着到教学软件进程。

image.png

image.png

在教学进程第一步,直接点击NEXT,进入到第二关。

Step Two

第二关的任务是学习内存扫描,寻找精确数值。我们有100生命值,每次点击Hit me,我们就会损失一定的生命值,通关目标是获得5000的生命值。

image.png

思路也很清晰了,首先搜索存在于内存中的生命值地址,直接对该地址进行写入,达到修改生命值的效果。提示中说明了该数值是4 Byte。

下拉栏选择Exact Value(精确数值)和4 Byte,点击 First Scan。会扫描出很多结果,我们目前还不知道哪一个表示了生命值,我们可以通过进一步搜索确定范围。

image.png

image.png

点击Hit me,会扣除生命值变为99,我们可以在上一步搜索结果之上,进一步缩小结果。将值修改为99,之后点击Next Scan。之后就会确定哪一个是生命值了。如果还有太多的结果,可以重复以上步骤,进一步Hit Me,修改生命值,再次继续搜索。

双击左侧结果,会把结果添加到下侧栏位暂存。双击数值99会弹出一个修改框,将数据改成1000,就会发现可以点击下一关了。

image.png

Step Three

这次是未知初始值,我们不知道生命值是多少。同理,我们可以选择 New Scan,Unknown initial value。

image.png

之后对于获得的结果,重复上一步的流程,点击Hit me,之后进一步搜索。区别是这一次搜索类型改成Decreased Value。获取内存位置之后修改成5000过关。

image.png

Step Four

这一关内容是搜索不同的数值类型,分别搜索flaot和double。之后步骤跟之前一样,不断搜索缩小范围即可。将数值修改成5000以上,过关。

image.png

Step Five

这一步教你修改代码,每次点击Change Value,会修改数值。我们不想让它改数值,可以通过patch代码来实现。将修改数值的代码全部改成nop即可。

image.png

首先运用之前的技巧,找到对应数据内存位置,将目标地址添加到下边。右键点击,选择找到什么写向这个地址 ,弹出一个新的框,再次点击 change value。

image.png

新出现的窗口中会弹出一行代码,这表示这行代码访问了对应地址,点击代码,选择右边反汇编。

这里 mov [eax],edx,表示将edx内容保存在内存中eax值的地址,很明显,eax中保存的就应该是数值的地址。每一次修改数值,都会调用这行代码,如果直接将这行代码全都替换成nop,那数值就不会修改了。之后点击change value数值不会变,next亮了,可以下一关了。

image.png

image.png

另外,对于代码的修改都会保存在Advance Options,方便将代码修改回去。

image.png

Step Six

指针!

考虑下边代码片段,该片段初始化了一个int指针,之后向其分配了一个新的动态内存。很明显,每次运行该代码片段,valueptr 指向的位置都会发生变化。也就是说,如果我们重启游戏,我们之前使用的方法找到的地址都会失效,因为new了一个新的地址,旧的地址不起作用了。

int *valueptr = NULL;
valueptr = new int();

对于这个问题,我们可以使用使用指针来解决这个问题。还是上述的代码片段,valueptr 是硬编码在代码中,也就是说,每次启动游戏的时候,valueptr 对于游戏的基址偏移是固定的,所以如果我们能够找到valueptr 的位置,那我们就能获取valueptr 的值,进一步获取valueptr 指向的位置。这样我们就绕过了new的地址不一致的问题。

使用之前学到的技巧,找到value的地址,找到什么写向这个地址 ,结果应该如下。

image.png

image.png

edx中保存的应该就是value的地址,那么我们就要找到这个地址保存在哪个位置中。New Scan,勾选HEX,将value的地址填入,搜索结果如右侧所示,绿色表示该地址相对基址是固定的,也就是我们找到的固定不变的指针位置。双击,添加到下侧Cheat Table。

image.png

双击address,复制指针的地址。

image.png

点击添加地址,勾选pointer选项,将指针的地址贴到红框位置。可以看出经过解指针之后,已经获得了数据。点击OK,这会在Cheat Table中添加一条新的记录。

image.png

image.png

勾选左侧选择框,将值改为5000,将之后点击change pointer,下一关。

image.png

image.png

Step Seven

代码注入,每次点击hit me,会扣一点血,要求我们修改代码,每次点击加两点血。

利用之前的知识,找到血量地址,添加监控,点击 什么写入了这个地址 ,再次点击hit me,弹出一行汇编,选中汇编代码,点击右侧show disassembler

image.png

观察汇编,这里意思是,将内存中一个地址保存的数据减1,这个地址存在ebx中,并且是加上偏移量得来的。

选中该行汇编代码,点击tools,选择下拉框的Auto Assemble。点击 template,AOB injection。点击之后会打开一个AOB脚本。

image.png

image.png

下边是生成的脚本,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血,过关。

image.png

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。

image.png

image.png

这个基址的地址应该保存在内存的另一个地址中,直接在内存中搜索。勾选hex,直接搜,之后添加到cheat table,这就是上一层的指针位置。

image.png

重复上边的步骤,右键点击新添加的指针位置,选择 什么访问了这个地址 。change value,捕获到新的汇编代码。esi保存的基址地址,这里没有偏移量,说明该指针是上层结构体的第一歌成员,那偏移量就是0。

image.png

仿照上一步的行为,在内存中找保存基址的变量位置。

image.png

image.png

接下来都一样了,我简单记录一下截图。可以进行对比

image.png

image.png

image.png

下一步的地址为0x0188DAB4,偏移量为0xC,搜索0x188DAA8

image.png

最后一步找到了相对程序基址固定的地址。

这里有一个偏移量对照表,基本格式是 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。

image.png

改成5000,change pointer,过关。

image.png

Step Nine

这关是这样的,有四个玩家,player 1,2是我们队伍,3,4是对面队伍。我们想要想一个办法,只扣对面的血,不扣自己的血

我们不能像之前一样,直接把扣血的代码patch成nop,因为这样会使大家都不口血了,我们要找到一个方法来区分我方和对方的玩家,扣血的时候检查一下,如果是我方就不扣血,是对方就扣血。

image.png

我们猜测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结构体的基址。

image.png

这里血量对结构体偏移量为4。

image.png

打开内存查看器,反编译结构体,把0x7278428作为基址地址。 (Dave血量地址 - 0x4)

image.png

image.png

点击上方 Structures,define new structures。这就会自动反汇编结构体。可以看见用户名称的字符串,用户的血量,等等。对于其他三个player执行相同的步骤。左上角File,可以添加用户,添加组。

image.png

image.png

我们看出,偏移量为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代码的运作基址,辅助思考。汇编部分其实也不难,就掌握几个简单的命令即可。