Step 1:选择进程

点击左上角”Select a process to open”, 然后选择 “Tutorial i386”。

Step2: 扫描确定的值

  1. 选择Exact Value Scan

  2. 因为这里的Health是整数,所以是4bytes。我们使用4bytes搜索health。最后修改为1000即过关。

Step3: 无法确定初始值的扫描

  1. 请确定你点击了New Scan, 因为新的一关需要新的搜索
  2. 选择Scan Type: Unknown Initial Value
  3. 选择Value Type: 4 bytes 或者 All
  4. 通过多次扣血,Scan Type设置为Decreased Value By,搜索扣除的血值
  5. 定位内存地址,修改为5000,过关

Step4: 浮点数扫描

  1. 题目告诉你Health是float类型,Ammo是double类型。
  2. 按照Scan type: exact value scan,Value Type: float/double进行搜索

Step5: 寻找Code

有时候Value的地址是会变动的(原因有可能程序内并没有initialize这个值,这个值可能是中途malloc的)。这个时候就需要找到对应的Code,使得Code失效,最终结果就是我们的数值不受这个code改变了。

举个例子:有可能血量在每次刷图的时候重新malloc出来,那么之前地址就无效。这个时候找到Code并Nop掉就可以不修改血量达到无敌状态。

  1. 首先搜索到改变的Value的地址
  2. 右键这个地址,选择 Find out what writes to this address。
  3. 再次改变这个Value, code finder会找出改变这个value的instruction。
  4. 点击Replace(作用等效于Nop掉这个code),那么change value 那个按钮的功能就失效了。

Step6: Pointers指针

指针的值可能时刻改变。比如重新malloc。但是不管pointer的值怎么变,总有一个最终pointer是static(固定)的。所以不管pointer的值如何变,只要找到最终pointer,就可以修改数值。

基本知识:

RAX是64bit的Register, EAX是32bit的Register。

在64位程序中,instruction中的eax就是rax。

在汇编中 mov [rdx],eax 相当于将eax的值放到rdx指针所指的值去。

过关步骤:

  1. 查找到value的地址后,右键Find out what write to the address。再次点击change value,得到instruction
  2. 我这里显示的如下图所示

Step6_0.png

rdx因为被[]包住了,所以是pointer

将eax中的值放到rdx这个pointer指向的地方(也就是014DA240,也就是pointer的值)中,我们继续找pointer的地址。

  1. 我们在search bar左边勾上Hex,Scan type为Exact Value,查询pointer的值,以至于查询到pointer的地址。
  2. 我搜出的结果为100306AD0, 这里绿色代表static,代表不会变动的地址,也就是pointer的地址。这就代表我们找到存放这个pointer的最终归属

在C++ Code里面对应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这个health相当于值
int health = 100;
//这个myhealth是一个pointer里面是class的地址,(myhealth也是base pointer): 对应 100306AD0
//new int(health)相当于把血量放到一个class里面,class创建的时候会分配一个地址。也就是说我们搜索到的地址就是
//class里面的地址,也就是血量pointer的值:对应:014DA240。因为class里面第一个member就是health,所以offset为0。
//当offset为0时,base pointer的值也就等于health的地址,也就是014DA240
int* myhealth = new int(health);

//每次new之后相当于分配了新地址
//所以每次new之后,pointer的值变了
//但是pointer的地址(myhealth的地址)没有变
myhealth = new int(health);
myhealth = new int(health);
myhealth = new int(health);
myhealth = new int(health);

Step7: Code injection (代码注入)

  1. 首先搜索Health也就是血量值存在的地址
  2. 使用 Find out what write to the address 查询到修改health的instruction
  3. 查询到如图的instruction

Step7_0

改变health的instruction为sub [rsi + 000007E0], 01: 其中sub 是减法,rsi是pointer,000007E0是offset,01就是从pointer指向的地方减去1。

  1. Code injection代码注入。步骤为show in disassembler -> Auto Assemble(Ctrl + A) -> Template -> Code Injection。之后就会出现Code injection的template。我们修改为下图即可

Step7_1.png

Step8: Multiple pointers (多重指针)

access 和 write的区别: write查询只寻找谁改变了存储数值的操作,for example,mov [rsi+18],eax。是eax的值放到rsi+18这个指针指向的地方,相当于对pointer指向的地方进行了修改。access查询的是both write and read,它不仅查询改变value的操作,还查询读取value的操作,for example, mov ecx,[rsi+18] 相当于ecx这个寄存器读取了rsi+18这个指针指向地方的值。

  1. 重复Step6 中查找Pointer的方法, 改变数值,然后FInd out what write to the address.
  2. 我查到的第一个指针的值为: base address为 0161C830 offset 为18。所以第一重Base Pointer的值为0161C830。接下来我们需要寻找第一个base pointer.
1
2
0161C848(value addr) -> 2024(value)
((1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
  1. 查询到第一个base pointer为01663ED0 .接下来需要FInd out what acess the adress,这里不查询write的原因是仅仅value变动不会导致pointer的值发生变动。
1
2
0161C848(value addr) -> 2024(value)
(01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
  1. 查询到mov rsi,[rsi]

    Step8_0.png

    我们可以看到执行完mov rsi,[rsi]之后,rsi中的值正好是1st base pointer里面的值。所以第二次直接读取了刚才查询到的pointer的值,所以offset为0。

    再直接一点说明就是[]方框里面的和肯定就是1st base pointer的adress。因为你查询的是what acess this address.

1
2
3
0161C848(value addr) -> 2024(value)
(01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
((2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
  1. 接下来我们继续深入下一个pointer, 查询到这个pointer的地址为015F6C18
1
2
3
4
0161C848(value addr) -> 2024(value)
(01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
(015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
((3rd base pointer)->(base addr)) + (offset) = 015F6C18(2ed base pointer)
  1. 再次查询what acess the address。查询到mov rsi,[rsi+18]。说明了rsi+18这个指针读取了015F6C18这个地址

    Step8_1.png

    所以这一次offset为0x18

    1
    2
    3
    4
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    ((3rd base pointer)->(base addr)) + 18(offset) = 015F6C18(2ed base pointer)

    我们减法,计算出这一层base addr的地址为015F6C18 - 0x18 = 15F6C00

    我们有

    1
    2
    3
    4
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    ((3rd base pointer)->015F6C00(base addr)) + 18(offset) = 015F6C18(2ed base pointer)

    接下来查询3rd base pointer的address,查到可能有2个01601CE80161C7C0。这个时候我们同时对两个address查询what access the address。我们发现当value改变时,只有0161C7C0这个地址被读取了。所以0161C7C0为3rd base pointer

    1
    2
    3
    4
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    (0161C7C0(3rd base pointer)->015F6C00(base addr)) + 18(offset) = 015F6C18(2ed base pointer)
  2. 0161C7C0被读取时的instruction为mov rsi,[rsi+10]。所以这一层的offset为10, rsi + 10就是0161C7C0这个地址

    1
    2
    3
    4
    5
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    (0161C7C0(3rd base pointer)->015F6C00(base addr)) + 0x18(offset) = 015F6C18(2ed base pointer)
    ((4rd base pointer)->(base addr)) + 0x10(offset) = 0161C7C0(3rd base pointer)

    所以我们可以算出这一层的base addr为0161C7C0 - 0x10=161 C7B0‬

    1
    2
    3
    4
    5
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    (0161C7C0(3rd base pointer)->015F6C00(base addr)) + 0x18(offset) = 015F6C18(2ed base pointer)
    ((4rd base pointer)->0161C7B0(base addr)) + 0x10(offset) = 0161C7C0(3rd base pointer)

    我们查询4rd base pointer的address为100306B00。显示绿色。 Bingo,我们找到了最终pointer为100306B00 ("Tutorial-x86_64.exe"+306B00)

    1
    2
    3
    4
    5
    0161C848(value addr) -> 2024(value)
    (01663ED0(1st base pointer)->0161C830(base addr)) + 0x18(offset) = 0161C848(value addr)
    (015F6C18(2ed base pointer)->01663ED0(base addr)) + 0(offset) = 01663ED0(1st base pointer)
    (0161C7C0(3rd base pointer)->015F6C00(base addr)) + 0x18(offset) = 015F6C18(2ed base pointer)
    (100306B00(4rd base pointer)->0161C7B0(base addr)) + 0x10(offset) = 0161C7C0(3rd base pointer)

第八关的Template如下,

1
2
3
4
5
6
7
8
9
Address = Value = ?

base ptr -> address + offset4 = address

base ptr -> address + offset3 = address

base ptr -> address + offset2 = address

static base -> address + offset1 = address

第八关总结:

举个例子对应第八关

1
2
3
int health = new int(new int(new int(new int(100))));
//其中每一个new的class相当于一个占用了内存中的一个空间,其中的数值在class中的某个member里面,所以要通过class的base address + member的offset来获取地址
//但是我们可以看到health的地址是肯定不会变的。

最终的Base Address我猜测其实就是Main函数的地址,Main函数里的地址是相对稳定的,不是Dynamic的

第九关:Shared Code(共用的Function)

有时候我们修改code的时候,发现不仅我们自己无敌了,连敌人都无敌了。所以这个时候我们就需要找到是哪个object调用了这个function。

基本知识:

assembly的if…else…语法: cmp…jnz{成功的code}jump{失败的code}

1
2
3
4
5
6
7
8
9
00401017        cmp    eax, [ebp+var_8]            ; if x=y, the cmp will set the ZF to 1
0040101A jnz short loc_40102B ; jump if ZF not set (if x!=y)
0040101C push offset aXEqualsY_ ; "x equals y.\n"
00401021 call printf
00401026 add esp, 4
00401029 jmp short loc_401038
0040102B loc_40102B:
0040102B push offset aXIsNotEqualToY ; "x is not equal to y.\n"
00401030 call printf

理解什么时Virtual Function以及什么是VTable.

文章大概意思是:Virtual Function <- VTable(是一个class) <- VPointer(是一个class)。virtual function是dynamic dispatch因为virtual function是在runtime的时候被调用(因为存在Vtable这个东西)。但是非virtual function是在编译的时候就确定了某个类的function,这种叫做static dispatch。Vtable Pointer隐藏在class的第一个member位置,也就是offset = 0的位置

首先我们知道这一关的players和enemies都共享一个function来扣血,不同的class会有不同的表现,这个时候就要使用virtual function

但是这一关明显是share code,那么扣血的function很可能没有override。我们只需要分析class的structure就ok。分析出不同class的参数不同来达到对不同class执行不同function的目的。

首先找到4个血量的地址,然后通过find out what write to the address找到对应object的base address(血量地址 - offset)。我这里的offset是0x8

Step9_0.png

通过多少查询,所有的血量值都是base address之后的0x8的位置。这里同样可以更加确定player和enemy是同一个class的subclass。

接下来打开strcture dissect, 打开方式为Memory View->Tools->Data/Structure Dissect

然后我们把4个object的base address放进去对structure进行拆解分析:

Step9_1.png

猜解完毕后出现如下结果:

Step9_2.png

分析一下

1
2
3
4
5
6
7
offset = 0: 指针,很可能是VTable pointer
offset = 8: float, 很明显是health血量
offset = 0xc: 未知
offset = 0x10: 未知
offset = 0x14: 很有可能是Type, Type=1是player, Type=2是enemy
offset = 0x18: 未知
offset = 0x19: String, 名字

所以在Code Injection是时候分析一下offset = 14是否等于1。Code Injection如下图:

Step9_3.png