大家好,我是小小明,前面我在《基于概率分析的智能AI扫雷程序秒破雷界世界纪录》一文中的除了用AI算法自动扫雷外,后面还演示了使用内存外挂直接知道答案进行扫雷的方法。
前面我们通过内存外挂可以实现1秒内扫雷完毕,本文通过写内存的API直接修改内存,达到扫雷标识雷的位置的效果:
关于写内存的windowsAPI,大家可以参考下面的链接:
https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
从依赖中可以看该函数来着Kernel32.dll
,我们就可以通过这个系统动态链接库获取到这个方法。
要想直接操作内存,首先需要清楚内存所在的位置,下面演示一下操作方法:
通过OD和CE查找雷区数据内存位置
OD和CE以及win98版扫雷的下载:
链接:https://pan.baidu.com/s/1EptYN6L8mqhlB-qDHccAfA
提取码:1vo4
OD全称叫OllyDBG,CE全称叫Cheat Engine
上文已经通过OD简单演示了如何查找雷区数据内存位置。本文将再演示另一种查找思路,并顺带也演示一下CE。
OllyDbg的简介
下面使用OD的汉化版HawkOD来进行演示。
OllyDbg是一个动态追踪工具,打开后首先在CPU窗格,包括反汇编窗口、寄存器窗口、信息窗口、数据窗口、堆栈窗口。
- 反汇编窗口:显示被调试程序的反汇编代码,包括地址、HEX数据、反汇编、注释
- 寄存器窗口:显示当前所选线程的CPU寄存器内容,点击标签可切换显示寄存器的方式
- 信息窗口:显示反汇编窗口中选中的第一个命令的参数及跳转目标地址、字符等
- 数据窗口:显示内存或文件的内容,右键菜单可切换显示方式
- 堆栈窗口:显示当前线程的堆栈
通过OD查找界面内存位置
在启动HawkOD软件后,打开winmine.exe文件(文件->打开或直接拖动到软件中)。
猜测程序每次在显示界面时都会访问存储雷区数据的内存,而显示界面需要调用到BeginPaint函数,所以我们可以以此函数作出切入点找到雷区的内存位置。
当然,我们需要先找到调用BeginPain的位置。
在反汇编窗口右键鼠标,选择“查找”->“当前模块中的名称”:
键盘上输入“BEGINPAINT”时,能够迅速找到对应的函数:
然后对该函数点击右键选择 在每个参考上设置断点
:
然后查看断点设置页面(也可以点击工具栏的B字母):
双击该断点会进入到反汇编窗口BeginPaint对应位置:
运行程序后,可以看到在BeginPaint和EndPaint之间有一个CALL函数:
选中该行回车或右击该行点跟随:
跟随后去到0x01002AC3位置,发现又存在很多个CALL函数:
这里我们可以逐个函数去分析,但会比较费时间。不过我们在使用扫雷时,发现它的界面并没有闪烁,所以可以根据经验推测很可能使用了 双缓存技术,这是减少分析量的突破口。
双缓存是在缓存中一次性绘制,再把绘制的结果返回在界面上的技术。目的是减少硬件操作,在内存中把需要绘制的图像准备好后,再提交给硬件显示。
于是我们可以去查找双缓存技术的核心函数BitBlt,与前面操作一样,在反汇编窗口右键鼠标,选择“查找”->“当前模块中的名称”,查找BitBlt函数:
找到后,依然给所有该函数的调用上设置断点。
打开断点窗格可以看到有新增两个断点:
在中断处再次点击运行程序会发现跳到了第二个断点,而且该断点正好位于一个两层循环内:
我们可以推测,雷区的数据就是通过一个二维数组来存储(因为我自己做一个扫雷游戏也肯定会这样实现),此处又刚好符合二维数组的遍历,所以此处一定大有文章。
接下来我们详细分析这个双缓存函数绘制过程,先取消0x01002700位置的断点,再给两个循环的其实位置0x010026C9和0x010026DB位置设置断点(设置方法之一是选中该行按下F2):
之后每次点击运行都只有一个格子被绘制:
也可以按F8单步步过动态调试观察其效果。
此时寄存器中的数据为:
其中EBX是基址寄存器,ESI是它的偏移量。
由于每次点击运行,ESI的偏移量都+1,所以可以推测这个EBX基址寄存器就是雷区数据的基址或附近。
选择EBX基址寄存器,然后选择“数据窗口中跟随”:
禁用断点后,更新棋盘到如下状态后,发现内存会跟着变化:
根据这一一对应的关系,很容易就发现规律。
这说明01005360就是雷盘数据的起始位置,在双重for循环我们也能看到EBX的赋值定义:
这就是找到棋盘数据位置的另一种思路。
上文中所使用的思路是通过鼠标左键点击事件查找,有两个小细节没有演示,这里再补充一下。
首先给数据左键添加断点,需要设置windows消息断点,在窗口窗格中,对扫雷点击右键,选择在ClassProc上设置消息断点:
选择202 WM_LBUTTONUP消息,其他默认点击确认:
通过CE查找雷区数据内存位置
CE全称是Cheat Engine,是一款内存搜索修改编辑工具。
首先选择扫雷进程:
然后搜索雷数点首次扫描,修改雷数后,再次扫描:
找到3个地址,可以读取雷数。经过进一步分析,三个地址分别存储了总雷数,实际剩余雷数和显示剩余雷数。
长度和宽度也按照这样的方法查找,不断的修改长度和宽度,查找发生变化的位置。
对于雷区数据,我们可以在首次扫描搜索未知的初始数值:
点击第一个格子后,搜索变动的数值:
然后一直乱点,直到点出雷之后,搜索未变动的数值:
然后再点击笑脸后,扫描变动的数值:
反复重复上面的过程,逐渐缩小搜索范围,最终找到第一个格子所对应的内存位置。
在找到雷区的内存位置我们就可以为所欲为了:
实现一键标雷
首先读取雷盘数据并打印:
import win32con
import win32process
from ctypes import *
import win32gui
kernel32 = cdll.LoadLibrary("kernel32.dll")
ReadProcessMemory = kernel32.ReadProcessMemory
WriteProcessMemory = kernel32.WriteProcessMemory
OpenProcess = kernel32.OpenProcess
CloseHandle = kernel32.CloseHandle
# 扫雷游戏窗口
# class_name, title_name = "TMain", "Minesweeper Arbiter "
class_name, title_name = "扫雷", "扫雷"
hwnd = win32gui.FindWindow(class_name, title_name)
hreadID, processID = win32process.GetWindowThreadProcessId(hwnd)
process = OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, processID)
mine_num, w, h, dwSize = c_ulong(), c_ulong(), c_ulong(), c_ulong()
baseAddr = 0x01005330
ReadProcessMemory(process, baseAddr, byref(mine_num), 4, byref(dwSize))
ReadProcessMemory(process, baseAddr+0x4, byref(w), 4, byref(dwSize))
ReadProcessMemory(process, baseAddr+0x8, byref(h), 4, byref(dwSize))
mine_num, w, h = mine_num.value, w.value, h.value
print(f"宽:{w},高:{h},剩余雷数:{mine_num}")
max_w, max_h = 30, 24
# 外围有一个值为 0x10 的边界,所以长宽均+2
board_type = (c_uint8 * (max_w + 2)) * (max_h + 2)
board = board_type()
dwBaseAddr = baseAddr+0x10
ReadProcessMemory(process, dwBaseAddr, byref(board),
sizeof(board), byref(dwSize))
for y in range(1, h+1):
for x in range(1, w+1):
print(hex(board[y][x])[2:].zfill(2), end=",")
print("\b")
宽:9,高:9,剩余雷数:10
0f,0f,8f,0f,0f,0f,0f,0f,0f
0f,0f,0f,0f,0f,0f,8f,0f,0f
0f,8f,0f,0f,0f,8f,0f,0f,0f
0f,0f,0f,0f,0f,8f,0f,0f,0f
0f,0f,0f,0f,8f,0f,0f,8f,0f
0f,0f,0f,0f,0f,0f,0f,0f,0f
0f,0f,0f,0f,0f,0f,0f,0f,0f
0f,0f,0f,0f,0f,0f,0f,8f,0f
0f,0f,0f,0f,0f,0f,8f,8f,0f
然后将所有表示有雷的8f改成8e表示标红旗:
bClear = c_byte(0x8E)
for y in range(1, h+1):
for x in range(1, w+1):
if board[y][x] != 0x8f:
continue
addr = dwBaseAddr+y*(max_w+2)+x
WriteProcessMemory(process, addr, byref(bClear),
sizeof(c_byte), byref(dwSize))
不过虽然程序的内存改了,却没法马上进行显示,需要对扫雷窗口最小化再重新打开后才会显示红旗位置。
这时可以通过InvalidateRect
函数屏蔽一个窗口客户区的全部或部分区域使窗口进行事件重画。
参考:https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-invalidaterect
rect = win32gui.GetClientRect(hwnd)
win32gui.InvalidateRect(hwnd, rect, True)
CloseHandle(process)
最终效果:
这样我们就在扫雷开始之前已经肉眼知道雷的位置在哪里了。