无意中看到了re大佬jojo的帖子https://0xffff.one/d/769,也觉得这学期CSAPP似乎已经看了不少(不是,便买了本加解密朝圣一下。学了半个月,感觉实在得找些东西练下手,有点知识输出,这两天便打算破解一下扫雷玩玩。
分了三步走:
1.先废除扫雷的雷数与时间记录
2.寻找雷区,实现自动标记雷的位置
3.一键通关
0x01报废时钟
扫雷左上角显示所剩雷数,右上角显示已用时间,我们能不能让他们失效呢?
首先上CE:
先找雷数,
现在雷数为10,通过CE首次扫描找出一部分地址。
为了再次扫描缩小范围,我们标两面旗使雷数减少至8
再次扫描,此时CE只剩下一个结果即为雷数的内存地址
通过首次扫描与再次扫描确定了存储雷数的内存地址在0x01005194
再找时间,
因为时间在不停的变化,我们需要在OD上挂起线程
现在时间停了下来,此时时间为13,CE便可首次扫描。
第二次再恢复挂起线程,此时时间为26
我们便再可用CE两次扫描确定存储时间的内存地址0x0100579C (第二次再恢复挂起线程即可)
现在我们转到OD的内存窗口确认上两个内存位置
数值确实与雷数与时间相等,这两个地址看来就是我们要找的。
为了不让这两个数值变化,我们需要找到改变这两个数值的代码并删除,我们便可在这两个内存位置设下断点
(此为雷数,时间一样)
设完断点后,F9运行程序,我们在扫雷上右键标一面旗,雷数便即将被更改,触发断点
如图所示,第一行代码为将eax中的值加到地址0x01005194的值, eax的值显示为0xFFFFFFFF,即为-1,由此可见,这行代码即为雷数的改写代码。
我们将其删除,用nop覆盖
处理完了雷数,我们现在处理时间
同样在存储时间的地址0x0100579C上下内存断点,找到改写的代码位置
第一行代码将0x100579C的值+1,显然就是改写时间的代码,我们也将其改为nop删除。
现在我们已经将两个功能都废除了,把改写后的代码复制到可执行文件后运行
发现无论标多少雷,雷数依然不会改变,时间也仍然静止,功能成功废除
0x02寻找雷区,实现自动排雷
我们不免想到,这个雷区使怎么生成的呢,雷区又是存储在哪里呢?
我们每次启动游戏,雷区都是随机生成的,那么我们自然会想到程序会调用随机函数rand来生成雷区,那么程序生成雷区的代码段,必然会调用rand函数。换句话说,只要找到了rand,便找到了雷区的位置。
我们用IDA查看导入表确认
果然有rand函数
现在找到调用rand函数的代码位置
双击函数进入IDA VIEW-A,右键函数名查找交叉引用
点ok跳转至引用函数,其位置在0x0100347C,我们把它改名为rand_caller
我们解读一下代码
cdq
:将双字扩展为四字, 此处将EAX中的32位值扩展为64位,并分成两部分储存在EDX:EAX中。
idiv [esp+arg_0]
: 有符号除法操作,被除数储存在EDX:EAX, 除数为参数arg_0, 结果商存储在EAX, 余数存储在EDX
mov eax, edx
: 将存储在EDX中的余数放到EAX中,以便return。
可以看出此段代码的作用是,将rand函数生成的随机数取模运算,使最终返回的数值不大于参数arg_0。
我们继续查询rand_caller的交叉应用,看看是谁调用了这个函数.
发现有两处相隔很近的调用,我们跟随查看
发现是位于0x010036C7的代码段调用的,我们同样进行分析 : (我把第二个push的位置调整了一下)
loc_10036C7: //这段代码起始于0x10036C7
push dword_1005334 // 将0x1005334处的数值推入栈中作为rand_caller的参数
call rand_caller // 调用rand_caller
mov esi, eax // 将rand_caller的返回值存入ESI中
inc esi // 将ESI中的返回值+1
push dword_1005338 // 将0x1005338处的数值推入栈中作为rand_caller的参数
call rand_caller // 再次调用rand_caller
inc eax // 将返回值+1
mov ecx, eax // 将返回值存入EAX
shl ecx, 5 // 将ECX中的返回值乘 2^5 = 32
// 此时ESI中存的是第一次rand_caller的返回值加1. ECX中是第二次的返回值加1乘32
test byte_1005340[ecx+esi], 80h // test将80h 与 基址 0x1005340加上偏移[ecx + esi] 的地址上的值做AND比对
jnz short loc_10036C7
//若值的第8位为1,则返回这段代码的头部重新运行,若为0,则继续执行下一块代码。
下一块代码(若值的第八位为0则执行此处):
shl eax, 5 // 应该是汇编产生的冗余代码
lea eax, byte_1005340[eax+esi] // 将上述值的地址存入EAX
or byte ptr [eax], 80h // 将此值的第八位设为1
dec dword_1005330 // 将地址0x1005330上的值减1,此值应该为循环变量
jnz short loc_10036C7 // 若变量不为0,则返回上块代码的头部(0x10036C7)
经过思考,这段代码的作用应该是设置雷区,雷区应该是存储在内存中的一个数组中,将随机生成指定范围的数作为偏移,以0x1005340作为基址置雷。
但这里需要注意一个地方,虽然0x1005340为基址,但偏移的最小值不是为0,即0x1005340并不是第一个地雷所在位置!
1 \le ESI < dword\_1005334 + 1
32 \le ECX < (dword\_1005338 ) * 32 + 1
如上,此为ESI与ECX的取值范围(分析可得),则ESI+ECX \ge 33, 第一个地雷的位置应该是0x1005340 + 33 = 0x1005340 + 0x21 = 0x1005361, 这就是我们要找的雷区位置!!
打开OD,F9运行后,到内存位置0x1005361看看
是这样的玩意
为了验证,我们得在上面分析过的建雷区位置0x10036C7下个断点,重新运行游戏,到达后摁F8逐句运行,观察内存的变化(此处略),发现确实是这段代码在这个位置设下雷区。
未完