概述
Minesweeper(扫雷)是一款由微软于1992年发行的小游戏。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。该游戏在推出后预装到Windows系统中,随后逐渐风靡全球,经久不衰。
本次分析主要是对XP版本的扫雷游戏中官方预置的彩蛋进行分析。该彩蛋已知的触发行为是:关闭输入法,先依次输入XYZZY五个字母,随后按下shift键即开启作弊。作弊开启后,当指针划过游戏区时,屏幕左上角的像素点颜色可以提示玩家该区域是否存在雷。若为黑色像素点,则玩家指针所指的区域即为雷,反之亦然。
本次分析将采用动态分析和静态分析相结合的方式,旨在确定该官方彩蛋的触发方式、作弊功能具体的实现方式等内容,并尝试确定是否有其他开启彩蛋的方法。
确定分析位置
首先打开OllyDbg,对软件进行加载。运行后观察函数相关调用,寻找与彩蛋有关的线索。已知彩蛋开启后的表现是在桌面左上角绘制一个黑色像素点,过程中可能需要调用相关绘制函数。
确认该程序在01002158
地址处调用了gdi32.SetPixel
函数,该函数可以用于将设备上下文中指定坐标处的像素设置为指定颜色。该调用前后还有获取和释放设备上下文的调用。
对其下断点,调试运行,触发彩蛋条件并尝试作弊,确认是否与该调用有关。经确认,在开启彩蛋后,指针划过扫雷游戏区能够触发断点,且继续运行可以发现屏幕左上角像素点颜色发生变化。
继续运行程序,可以发现最终进入了消息处理循环之中。
可以确认,该窗体应用程序采用了经典的基于消息的事件驱动的程序设计模式。总体流程为:系统发生事件->Windows把事件翻译成消息,然后放入消息队列->应用程序接收到消息,并放在Msg记录中->窗口过程响应该消息并处理。
在本部分主要确认了程序彩蛋中具体的作弊调用位置(地址01002158
),确认了该程序是基于Windows基于消息的事件驱动的窗体应用程序,且确定了消息处理主进程(地址01002379
)。下面将基于这些信息展开进一步的分析和讨论。
彩蛋作弊流程分析
使用IDA Pro分析工具打开可执行文件,跳到SetPixel
所在位置(地址01002158
),反编译后观察逻辑。
可以看到此处上方有一个标签LABEL_102
。该部分为主要的作弊逻辑。具体逻辑简述如下:首先对鼠标指针指向的扫雷游戏区的坐标进行计算(通过v4
变量,在上方通过消息lParam
获得),得到扫雷游戏区的逻辑坐标,然后判断是否合法。若逻辑坐标合法,读取对应该位置对应的内存,判断若内存值<0时绘制黑色像素点,>=0时绘制白色像素点。接下来执行到return语句,该部分处理流程结束。以下是作弊部分的流程图。
本部分主要对彩蛋的作弊流程进行了分析。该部分内容首先依赖了上面部分已经保存了的lParam
参数以获得鼠标的指向位置,并利用计算转换为了逻辑坐标,最后依照内存中的值(相关内存中的值在<0时为雷)在设备上下文中绘制像素点。过程中巧妙地利用了位运算提升了计算效率,节约了机器资源。
彩蛋触发流程分析
继续阅读代码段,追踪作弊主要逻辑所在函数的调用,发现该函数在窗体运行函数中被作为WNDCLASSW
结构的lpfnWndProc
成员,即指向窗口过程的指针。必须使用CallWindowProc
函数来调用窗口过程。
很显然,包含作弊主要逻辑的函数为窗口消息的响应函数,故将sub_1001BC9
重命名为EventProess
函数。
在注册事件处理函数后,该函数还会进行创建窗口、判断等操作,随后进入事件处理循环。事件循环结束后,进入后处理环节(用于销毁环境、释放资源等),最后该函数结束。
在事件到来时,窗体运行函数会将消息先转换后,通过DispatchMessage
自动地将有关事件消息转发到刚刚注册的EventProcess
函数。由于其功能,将注册响应函数的sub_10021F0
函数重命名为RunWindow
。而RunWindow
又进一步被start
函数(程序起始点)所调用。以下是窗体运行时的相关流程示意图。
进一步对EventProcess
事件处理函数进行分析,会发现整体上是一个大型if-elif-else形式/switch-case形式的结构,接收到翻译后的消息后,按照Msg
内容,分配到不同的逻辑中进行处理,并按需使用消息中所含的wParam
和lParam
。
进一步对逻辑进行整理,要使得EventProcess
事件处理函数在处理事件时能够判断是否开启作弊流程、是否跳转到作弊流程,需要满足几个条件,如图和注解所示。(为清晰展示,其它无关逻辑被简化,不再在图中体现。请按照注解顺序观看本图。)
-
注解1:Msg==0x200时,事件名
WM_MOUSEMOVE
,描述为“移动鼠标”。Y分支即当事件处理函数接收到鼠标移动事件时执行对应操作。 -
注解2:Msg==0x100时,事件名
WM_KEYDOWN
,描述为“按下一个键”。Y分支即当事件处理函数接收到按下键盘事件时执行对应操作。 -
注解3:在
WM_KEYDOWN
事件前提下,wParam
事件参数代表着按下了哪个键。由于本次分析主要关注Shift
键(见注解5),其余判断逻辑由于无关,均省略。 -
注解4:此处经编辑后的源代码如图所示。
alreadyInputDigits
为变量A,password
为触发彩蛋序列(地址01005034),wParam
在事件前提下代表用户按下的键。本段主要逻辑为:逐次对比用户的输入是否符合password
序列,若符合序列则变量A+1,不符合则将变量A置0。最终正确输入序列后变量A应为5。由于采用了事件编程,这里巧妙地利用了int型变量A(alreadyInputDigits
)来存储匹配序列的状态。同时,结合ollydbg动态分析查看
password
处(地址01005034)的序列信息,可以看到XYZZY
序列,与预计的触发序列一致。 -
注解5:在事件前提下,
wParam
若为0x10
则代表用户本次按下了Shift
键。 -
注解6:若按下了
Shift
键后,变量A大于等于5时,代表要么已经输入完毕还未开启(A5),或是已经被开启(A11)。 -
注解7:此处巧妙利用异或运算,当输入完毕时还未开启(A5),执行
XOR 0x14u
操作后A变为11;而当已开启时(A11),执行XOR 0x14u
操作后A变为5。 -
注解8:
wParam
二进制第四位是否为1,在源代码中操作如图。若
wParam & 8 != 0
该语句为真则执行Y分支跳转到作弊主要流程。由于有注解1的移动鼠标事件前提,其事件的wParam
参数用于指示各种虚拟键是否已按下。 在这种情况下,只有MK_CONTROL
键(值为0x0008
)能够满足该条件。故本次后门彩蛋分析发现了一个新的触发方式,在输入触发序列后,还没有按下Shift
键开启前,按住Ctrl
键不放使用鼠标指向雷区可以实现同样的作弊功能。经过实验,用这种新发现的方式也成功地触发了彩蛋作弊功能。 -
注解9:当变量A>5时,代表着作弊功能已被开启(A==11),直接跳转进入彩蛋作弊主要流程。
本部分主要对彩蛋触发的流程进行了详细分析。彩蛋主要是通过对事件的响应来实现对应的操作。而实现的过程中采用了许多巧妙的方法,如使用变量A存储用户彩蛋字符的输入状态,节约内存空间。进一步地,开发者还采取了异或计算的技巧,更巧妙地复用了该变量状态,使得一个变量能够为整个功能提供支撑,极大的节约了运行内存资源。可以说,整个彩蛋的触发和执行过程主要依赖于变量A的状态。最后,通过分析还找到了一种新型的采用Ctrl
键触发彩蛋的方法。
总结
至此,针对XP版扫雷游戏的后门彩蛋分析就基本完成了。
在调试与分析扫雷二进制文件的过程中,不难发现颇有早期程序员简约、节约的风格。其中调用的一部分win32 API有一些已经不再推荐使用,或有更新的实现,但旧版仍被保留了下来,让XP时代的程序在Win11时代仍能运行;程序逻辑中有一些设计和技巧十分巧妙(也不排除是编译器自动优化的结果),如利用位运算简洁计算所指的游戏区逻辑位置,利用单变量存储用户输入状态以及利用异或计算来复用变量的操作都十分令人惊叹。
本次分析利用了动态调试与静态调试结合的方法,工具主要使用了OllyDbg以及IDA Pro。从彩蛋的表现入手,利用动态调试确定其调用的win32 API、地址信息以及程序的设计模式,随后对二进制文件进行静态分析,阅读程序逻辑,理顺事件处理函数与逻辑,最终形成本次报告。除分析了常规的利用Shift
键开启彩蛋作弊的触发流程、事件处理流程以及作弊的核心代码之外,还发现了一个全新的利用Ctrl
键的触发方式。