Step 1:选择进程
点击左上角”Select a process to open”, 然后选择 “Tutorial i386”。
Step2: 扫描确定的值
选择Exact Value Scan
因为这里的Health是整数,所以是4bytes。我们使用4bytes搜索health。最后修改为1000即过关。
Step3: 无法确定初始值的扫描
- 请确定你点击了New Scan, 因为新的一关需要新的搜索
- 选择Scan Type: Unknown Initial Value
- 选择Value Type: 4 bytes 或者 All
- 通过多次扣血,Scan Type设置为Decreased Value By,搜索扣除的血值
- 定位内存地址,修改为5000,过关
Step4: 浮点数扫描
- 题目告诉你Health是float类型,Ammo是double类型。
- 按照Scan type: exact value scan,Value Type: float/double进行搜索
Step5: 寻找Code
有时候Value的地址是会变动的(原因有可能程序内并没有initialize这个值,这个值可能是中途malloc的)。这个时候就需要找到对应的Code,使得Code失效,最终结果就是我们的数值不受这个code改变了。
举个例子:有可能血量在每次刷图的时候重新malloc出来,那么之前地址就无效。这个时候找到Code并Nop掉就可以不修改血量达到无敌状态。
- 首先搜索到改变的Value的地址
- 右键这个地址,选择 Find out what writes to this address。
- 再次改变这个Value, code finder会找出改变这个value的instruction。
- 点击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指针所指的值去。
过关步骤:
- 查找到value的地址后,右键Find out what write to the address。再次点击change value,得到instruction
- 我这里显示的如下图所示
rdx因为被[]包住了,所以是pointer
将eax中的值放到rdx这个pointer指向的地方(也就是014DA240,也就是pointer的值)中,我们继续找pointer的地址。
- 我们在search bar左边勾上Hex,Scan type为Exact Value,查询pointer的值,以至于查询到pointer的地址。
- 我搜出的结果为100306AD0, 这里绿色代表static,代表不会变动的地址,也就是pointer的地址。这就代表我们找到存放这个pointer的最终归属
在C++ Code里面对应的代码:
1 | //这个health相当于值 |
Step7: Code injection (代码注入)
- 首先搜索Health也就是血量值存在的地址
- 使用 Find out what write to the address 查询到修改health的instruction
- 查询到如图的instruction
改变health的instruction为sub [rsi + 000007E0], 01: 其中sub
是减法,rsi
是pointer,000007E0
是offset,01
就是从pointer指向的地方减去1。
- Code injection代码注入。步骤为show in disassembler -> Auto Assemble(Ctrl + A) -> Template -> Code Injection。之后就会出现Code injection的template。我们修改为下图即可
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这个指针指向地方的值。
- 重复Step6 中查找Pointer的方法, 改变数值,然后FInd out what
write
to the address. - 我查到的第一个指针的值为: base address为
0161C830
offset 为18
。所以第一重Base Pointer的值为0161C830
。接下来我们需要寻找第一个base pointer.
1 | 0161C848(value addr) -> 2024(value) |
- 查询到第一个base pointer为
01663ED0
.接下来需要FInd out whatacess
the adress,这里不查询write的原因是仅仅value变动不会导致pointer的值发生变动。
1 | 0161C848(value addr) -> 2024(value) |
查询到
mov rsi,[rsi]
。我们可以看到执行完
mov rsi,[rsi]
之后,rsi
中的值正好是1st base pointer里面的值。所以第二次直接读取了刚才查询到的pointer的值,所以offset为0。再直接一点说明就是[]方框里面的和肯定就是1st base pointer的adress。因为你查询的是what acess this address.
1 | 0161C848(value addr) -> 2024(value) |
- 接下来我们继续深入下一个pointer, 查询到这个pointer的地址为
015F6C18
。
1 | 0161C848(value addr) -> 2024(value) |
再次查询what acess the address。查询到
mov rsi,[rsi+18]
。说明了rsi+18
这个指针读取了015F6C18
这个地址所以这一次offset为0x18
1
2
3
40161C848(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
40161C848(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个
01601CE8
和0161C7C0
。这个时候我们同时对两个address查询what access the address。我们发现当value改变时,只有0161C7C0
这个地址被读取了。所以0161C7C0
为3rd base pointer1
2
3
40161C848(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)0161C7C0
被读取时的instruction为mov rsi,[rsi+10]
。所以这一层的offset为10,rsi + 10
就是0161C7C0
这个地址1
2
3
4
50161C848(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
50161C848(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
50161C848(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 | Address = Value = ? |
第八关总结:
举个例子对应第八关
1 | int health = new int(new int(new int(new int(100)))); |
最终的Base Address我猜测其实就是Main函数的地址,Main函数里的地址是相对稳定的,不是Dynamic的
第九关:Shared Code(共用的Function)
有时候我们修改code的时候,发现不仅我们自己无敌了,连敌人都无敌了。所以这个时候我们就需要找到是哪个object调用了这个function。
基本知识:
assembly的if…else…语法: cmp…jnz{成功的code}jump{失败的code}
1 | 00401017 cmp eax, [ebp+var_8] ; if x=y, the cmp will set the ZF to 1 |
理解什么时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
通过多少查询,所有的血量值都是base address之后的0x8的位置。这里同样可以更加确定player和enemy是同一个class的subclass。
接下来打开strcture dissect, 打开方式为Memory View->Tools->Data/Structure Dissect
然后我们把4个object的base address放进去对structure进行拆解分析:
猜解完毕后出现如下结果:
分析一下
1 | offset = 0: 指针,很可能是VTable pointer |
所以在Code Injection是时候分析一下offset = 14是否等于1。Code Injection如下图: