本人最近刚刚开始学习逆向分析,突发奇想想弄一个Windows XP扫雷的辅助。发帖前,在论坛上一搜,好多人都做过这个了,还有人做过Windows7 32位,64位的,突然感觉自己好低级……于是我又认真地把windowsXP扫雷的几乎所有汇编代码都分析了一遍,最后做了一个修改扫雷内部代码而实现悬停显示雷的操作。
效果如下:
进入正题,这是我的预计任务
1. 找到回调函数
2. 找到并分析雷表
找到存放时间的位置
3. 分析绘图代码
首先,用OllyDbg打开windows xp的扫雷程序。
一开始如下图所示
显然,是属于程序的入口部分代码,包含有初始化SHE,获取ImageBase,测试PE文件的有效性等等等等。
一:找窗口回调函数
为了实现找到回调函数这一个目标,首先需要知道获取来源。
首先想到WNDCLASSA(或WNDCLASSW)和WNDCLASSEX这两个结构体,WNDCLASSW定义如下
这两个结构体都是用来初始化窗口类的,在RegisterClass中被引用,根据该结构体的定义,在此结构的(+0x04)位置处,就是窗口回调函数的地址。所以,从设置WNDCLASS成员时指定的地址或者从RegisterClass调用时的栈窗口,可以找到WNDCLASS的地址,进而找到lpfnWndProc的值。
RegisterClass一般在主线程的WinMain中被调用,我向下跟踪,来到此处
目前窗口并没有创建,而接下来的第二个函数是exit的调用,标志着程序的退出,因此可以确认,下面的第一个函数,就是程序的WinMain函数。
跟踪进去
在第一个CALL处我进行了查看,发现是设置随机数种子,接下来明显地看到调用了LoadIconW,LoadCursorW,和GetStockObject三个API函数对WNDCLASSW进行设置,往下翻,立刻可以看见RegisterClassW。事实上,hIcon的位置在lpfnWndProc(+0x10)的位置,从LoadIcon的返回值的设置位置(2个地方)
一开始我以为是这里,结果发现1005B18没有写入的代码,接着向下看:
可知ebp-38-10=ebp-48就是回调函数的地址,接下来看到
可以确认,01001BC9就是回调函数的地址了,添加标签为lpfnWndProc
二:
进入窗口回调函数,马上看到一个switch
根据对回调函数的整体浏览,发现对WM_LBUTTONDOWN的处理部分,有将(记住这个地址)1005144处的数据设为0这估计是为了实现左键按下时显示格子0的效果,添加标签为bLButtonUp(Down的非是Up (`へ´*)ノ)
接着对WM_LBUTTONDOWN和WM_LBUTTONUP这两个消息的处理部分下断
在扫雷点击一个格子,在WM_LBUTTONDOWN断下,继续运行,WM_LBUTTONUP自然不会断下,而游戏界面无变化,说明WM_LBUTTONDOWN不进行点击格子的操作
现在大概可以判断,是左键弹起表示了对一个块的点击。
取消WM_LBUTTONDOWN的断点,继续点击,跟踪WM_LBUTTONUP,来到此处
开始是四个判断,根据对数据的分析,1005118应该保存一个POINT结构体,是当前点击的单位坐标,而1005334保存了扫雷区域的单位长度,1005338则是单位宽度。于是添加标签g_xPos,g_yPos,Length,Height
那么此处的作用应该是,判断点击坐标是否在范围之内,否则函数跳到末尾。
接下来则是这部分
一开始,判断了两个位置的数据,不为0则设置间隔为1s的定时器,于是我大胆猜测,这是第一次点击时会调用的代码,设置定时器,进行计时。那么,10057A4和100579C就可能:一个是时间的数据,一个是游戏是否开始的BOOL型数据。
然后让扫雷运行,再分别对10057A4和100579C下硬件写入断点,发现10057A4是每点开一个格子都会断下,并且计数+1,100579C是每秒都会断下,计数+1。那么,10057A4应该是翻开的格子数目,标签其为PressNum,10057C9应该是时间,标签其为Time
接下来的部分如图所示
1003872的跳转一般不会实现,100388B的跳转一般一定实现,第一个跳转是判断1005000处的数据是否为零,通过对此地址设置硬件断点,可以发现,当“新游戏”时,此处的值被设置为1,而游戏结束后,此处的值被设置为16。
第二个跳转发现访问了bLButtonDown这个地址,为0则跳转。这个地址前面见到过,在左键按下的时候被设置成了0,而左键弹起前一定会有按下操作,因此,这个地方一般也会跳过。(也可以写监测程序查看值的变化)
从前三行可以知道读取雷表的方法
例如有雷表BYTE arMine[..],需要读取第r行第c列
第一行将ecx:也就是r,赋给edx
shl相当于<<,第二行是将edx左移五位,五个二进制位就是乘以2^5(=32)
第三行读取
那么可以知道,1005340是雷表的头地址,标签其为arMine,读取方式为arMine[r*32+c]
通过不断地对数据进行更新和检查,可以知道储存规律:
雷表是一个以1个字节为元素长度的数组,0x10为游戏的边界
雷表元素的低四位:
i. 未翻开时,是0xF
ii. 插旗子时,是0xE
iii. 打问号时,是0xD
iv. 翻开时(非雷),是它的数字,是雷则为0xA(不是点击到的)
v. 左键按下时,是0x0
vi. 边界为0x1
雷表元素的高四位:
i. 未翻开时,是雷则为0x8,否则为0x0
ii. 翻开时(非雷),是0x4,是雷则为0x8(不是点击到的)
iii. 边界为0x0
雷表元素是点击到的雷,数据则为0xCC
上面的基本说的都没用(众所周知),接下来才是重头戏,如何实现,把鼠标悬停在一个格子上,如果那个格子有雷,就把雷显示出来(透视的感觉有木有)
首先,需要寻找一个可以安心写代码并能保存到文件的位置,将代码向下翻,找到1004A60的位置,下面都是0数据,可以使用,不过要先判断这个区域是否在扫雷文件内(不然保存不到文件里),RVA=4A60,转为文件FileOffset=3E60,而文件中.text节区是到4000,那么把它标签为InjectCode,于是在此处就可以放心地写入代码了。
首先我在扫雷窗口生成后对WM_PAINT下断,然后在扫雷点开一格,并没有断下,说明重绘函数不会处理左键单击,格子变化之类的游戏进行中的小调整,在这里写入代码就没有用了(我一开始莽,直接在这里写,然并卵)。为了知道贴图的怎么变化的,我在BitBlt下了断,点击一下,来到此处:
发现两个参数分别是我点下的x,y坐标,那么这个函数就是根据一个格的数值来绘图。标签其为UpdateBlockPaint,观察栈,发现:把雷表中的相应元素&0x1F后,作为8字节为元素长度的数组1005A20的索引值,就可以得到相应贴图的HDC,那么,显示的雷是0x8A,&0x1F得0xA,于是位置就是4*0xA+0x1005A20=1005A48,这个地址之后BitBlt中会用到哦~
这个函数的调用最终是起源于回调函数中的:
触发WM_MOUSEMOVE一定来到此处,左中右键按下也可能来到此处,一开始就判断左键是否按下,关于1005140的值,我用监测程序判断了一下
左键按下为1,弹起后为0
接下来是10031D4函数的部分
是为了实现,格子按下的效果,如果说左键按下情况下这次悬停在的格子和上次不同,就更改上次格子为不按下,这次格子为按下,关于PressOn和PressOff两个函数,还原后代码如下(我对IDA结果的类型表示不满意):
测试一下即知效果。
在回调函数中,WM_MOUSEMOVE时左键没按下,跳到此处
刚好可以在这里修改代码(字节刚好为5,强迫症福利),把前面写代码的位置标签为InjectCode,把10020C1处改为
并把10020C6标签为CodeReturn
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)