大家好,我是小小明,前面我在《基于概率分析的智能AI扫雷程序秒破雷界世界纪录》一文中的除了用AI算法自动扫雷外,后面还演示了使用内存外挂直接知道答案进行扫雷的方法。

前面我们通过内存外挂可以实现1秒内扫雷完毕,本文通过写内存的API直接修改内存,达到扫雷标识雷的位置的效果:

录制_2021_08_11_01_11_07_885

关于写内存的windowsAPI,大家可以参考下面的链接:

https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory

image-20210812212313012

从依赖中可以看该函数来着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窗格,包括反汇编窗口、寄存器窗口、信息窗口、数据窗口、堆栈窗口。

image-20210812220650417

  • 反汇编窗口:显示被调试程序的反汇编代码,包括地址、HEX数据、反汇编、注释
  • 寄存器窗口:显示当前所选线程的CPU寄存器内容,点击标签可切换显示寄存器的方式
  • 信息窗口:显示反汇编窗口中选中的第一个命令的参数及跳转目标地址、字符等
  • 数据窗口:显示内存或文件的内容,右键菜单可切换显示方式
  • 堆栈窗口:显示当前线程的堆栈

通过OD查找界面内存位置

在启动HawkOD软件后,打开winmine.exe文件(文件->打开或直接拖动到软件中)。

image-20210812221738696

猜测程序每次在显示界面时都会访问存储雷区数据的内存,而显示界面需要调用到BeginPaint函数,所以我们可以以此函数作出切入点找到雷区的内存位置。

当然,我们需要先找到调用BeginPain的位置。

在反汇编窗口右键鼠标,选择“查找”->“当前模块中的名称”:

image-20210812222258771

键盘上输入“BEGINPAINT”时,能够迅速找到对应的函数:

image-20210812222401721

然后对该函数点击右键选择 在每个参考上设置断点

image-20210812222537564

然后查看断点设置页面(也可以点击工具栏的B字母):

image-20210812222912503

双击该断点会进入到反汇编窗口BeginPaint对应位置:

image-20210812223042572

运行程序后,可以看到在BeginPaint和EndPaint之间有一个CALL函数:

image-20210812223330413

选中该行回车或右击该行点跟随:

image-20210812223608410

跟随后去到0x01002AC3位置,发现又存在很多个CALL函数:

image-20210812224206821

这里我们可以逐个函数去分析,但会比较费时间。不过我们在使用扫雷时,发现它的界面并没有闪烁,所以可以根据经验推测很可能使用了 双缓存技术,这是减少分析量的突破口。

双缓存是在缓存中一次性绘制,再把绘制的结果返回在界面上的技术。目的是减少硬件操作,在内存中把需要绘制的图像准备好后,再提交给硬件显示。

于是我们可以去查找双缓存技术的核心函数BitBlt,与前面操作一样,在反汇编窗口右键鼠标,选择“查找”->“当前模块中的名称”,查找BitBlt函数:

image-20210812225030159

找到后,依然给所有该函数的调用上设置断点。

打开断点窗格可以看到有新增两个断点:

image-20210812225236662

在中断处再次点击运行程序会发现跳到了第二个断点,而且该断点正好位于一个两层循环内:

image-20210812225726147

我们可以推测,雷区的数据就是通过一个二维数组来存储(因为我自己做一个扫雷游戏也肯定会这样实现),此处又刚好符合二维数组的遍历,所以此处一定大有文章。

接下来我们详细分析这个双缓存函数绘制过程,先取消0x01002700位置的断点,再给两个循环的其实位置0x010026C9和0x010026DB位置设置断点(设置方法之一是选中该行按下F2):

image-20210812231723129

之后每次点击运行都只有一个格子被绘制:

image-20210812231946525

也可以按F8单步步过动态调试观察其效果。

此时寄存器中的数据为:

image-20210812232246520

其中EBX是基址寄存器,ESI是它的偏移量。

由于每次点击运行,ESI的偏移量都+1,所以可以推测这个EBX基址寄存器就是雷区数据的基址或附近。

选择EBX基址寄存器,然后选择“数据窗口中跟随”:

image-20210812232809928

禁用断点后,更新棋盘到如下状态后,发现内存会跟着变化:

image-20210812233534063

根据这一一对应的关系,很容易就发现规律。

这说明01005360就是雷盘数据的起始位置,在双重for循环我们也能看到EBX的赋值定义:

image-20210812234117799

这就是找到棋盘数据位置的另一种思路。

上文中所使用的思路是通过鼠标左键点击事件查找,有两个小细节没有演示,这里再补充一下。

首先给数据左键添加断点,需要设置windows消息断点,在窗口窗格中,对扫雷点击右键,选择在ClassProc上设置消息断点:

image-20210812235447738

选择202 WM_LBUTTONUP消息,其他默认点击确认:

image-20210812235809681

通过CE查找雷区数据内存位置

CE全称是Cheat Engine,是一款内存搜索修改编辑工具。

首先选择扫雷进程:

image-20210811003211767

然后搜索雷数点首次扫描,修改雷数后,再次扫描:

image-20210811003522919

找到3个地址,可以读取雷数。经过进一步分析,三个地址分别存储了总雷数,实际剩余雷数和显示剩余雷数。

长度和宽度也按照这样的方法查找,不断的修改长度和宽度,查找发生变化的位置。

对于雷区数据,我们可以在首次扫描搜索未知的初始数值:

image-20210811004129237

点击第一个格子后,搜索变动的数值:

image-20210811004344710

然后一直乱点,直到点出雷之后,搜索未变动的数值:

image-20210811004557471

然后再点击笑脸后,扫描变动的数值:

image-20210811004639750

反复重复上面的过程,逐渐缩小搜索范围,最终找到第一个格子所对应的内存位置。

在找到雷区的内存位置我们就可以为所欲为了:

实现一键标雷

首先读取雷盘数据并打印:

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)

最终效果:

录制_2021_08_11_01_11_07_885

这样我们就在扫雷开始之前已经肉眼知道雷的位置在哪里了。


本文转载:CSDN博客