一、编程前准备
刚开始是预备知识,如果熟悉的话,可以直接跳到第二部分阅读
在 Windows API 中,SetTimer 函数用于创建一个定时器,并在指定的时间间隔后触发一个定时器消息。以下是关于 SetTimer 函数的介绍:
功能:创建一个定时器,并在指定的时间间隔后触发定时器消息。
参数:
- hWnd:指定接收定时器消息的窗口的句柄。
- nIDEvent:指定定时器的标识符。在定时器消息中使用该标识符来区分不同的定时器。
- uElapse:指定定时器触发的时间间隔,以毫秒为单位。
- lpTimerFunc:指向定时器回调函数的指针。当定时器触发时,系统将调用该回调函数。
 case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            
            // 获取当前时间
            SYSTEMTIME sysTime;
            GetLocalTime(&sysTime);
            
            // 将时间转换为字符串
            char timeString[64];
            sprintf(timeString, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
            
            // 绘制时间文本
            TextOut(hdc, 10, 10, timeString, strlen(timeString));
            
            EndPaint(hwnd, &ps);
            break;
        }要在窗口中使用计时器(Timer)来定期绘制时间,可以按照以下步骤进行操作:
1、在窗口类中添加一个计时器标识符:在窗口类的定义中,添加一个计时器标识符作为类的成员变量,用于标识计时器的ID。
class WindowClass
{
private:
    static const UINT_PTR TIMER_ID = 1; // 计时器标识符
    // 其他成员变量和方法
};
2、在窗口的创建过程中启动计时器:在窗口的创建过程中,通过调用 SetTimer 函数来启动计时器。这个函数将设置一个定时器,每隔一定时间触发一个 WM_TIMER 消息。 
HWND hwnd = CreateWindow(/* 窗口参数 */);
SetTimer(hwnd, TIMER_ID, 1000, NULL); // 在这里启动计时器,每隔1秒触发一次 WM_TIMER 消息
3、处理 WM_TIMER 消息:在窗口的消息处理函数中,处理 WM_TIMER 消息,并在该消息中绘制时间。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        // 其他消息处理代码
        
        case WM_TIMER:
        {
            if (wParam == TIMER_ID) // 根据计时器标识符判断是哪个计时器触发的消息
            {
                // 获取当前时间
                SYSTEMTIME sysTime;
                GetLocalTime(&sysTime);
                
                // 将时间转换为字符串
                char timeString[64];
                sprintf(timeString, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
                
                // 绘制时间文本
                HDC hdc = GetDC(hwnd);
                TextOut(hdc, 10, 10, timeString, strlen(timeString));
                ReleaseDC(hwnd, hdc);
            }
            
            break;
        }
        
        // 其他消息处理代码
        
        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    
    return 0;
}
另外,不要忘记在窗口销毁时停止计时器,可以在 WM_DESTROY 消息中调用 KillTimer 函数来停止计时器的触发。
case WM_DESTROY:
    KillTimer(hwnd, TIMER_ID); // 停止计时器
    PostQuitMessage(0);
    break;
二、扫雷找数据
1、获取雷区的数据
首先准备本次实验需要用到的工具:
如果大家需要扫雷的资源,可以私信我获取噢~😐😐😐

附加到扫雷的进程:

首次扫描未知的初始值,类型选择字节

点击首次扫描,发现有10W+的搜索结果,,,然后毫无疑问我们需要过滤掉一部分数据:
点击第一个格子,再次扫描变动的数据
 、
、
点击旁边的格子,扫描不变的数据,再次扫描

重新开始,如果数字如果发生变化就选择变动的数值,再次扫描
 
 
然后继续点击几个格子,不是地雷的话就可以选择未变动的值,再次扫描
 
 
很快就找到了我们所需要的雷区的数据
 
 
浏览这块区域所在的内存位置,观察一下里面都是什么数据

可以手动测试一下:

 
经过几轮尝试之后,不难发现:
"0x0f"表示不是地雷
"0x8f"表示是地雷
"0x0e"表示小旗子
"0x10"表示边界
"40"表示空格
"41-49"表示数字
2、获取雷区的高度和宽度
注意:这里说的高度和宽度是不计算无关数据的,就是真实的高、宽占多少个格子
比如对于初级模式就是9*9:
 
 
切换为中级、高级,分别搜索,很快就可以找到高度所在内存:
 
 
 
 
同理,还可以找到宽度和地雷数量 :
 
 
3、寻找存储时间的数据
我们找存储时间内存的目的,就是观察谁修改了它,具体来说就是哪条指令把这块内存的数据inc了,这样我们通过把这句指令nop掉,就可以实现时间停止的功能,从而实现0秒扫雷😂😂😂
 、
 、
其实搜索起来十分简单,只要一直追那个变化的数据就好了
 
 
 
 
这样我们就找到了inc时间的指令,位于0x1002ff5的位置,把他nop掉,时间就静止了~
 
 
但是我们一开始点击格子的时候,它还是会自动设置时间为1,所有我们还需要把这一条指令也nop掉,实现真正的0秒扫雷!!!
我采用的思路是在左键弹起的地方设置一个硬件断点,然后一路f8下去,观察到时间发生变化,就在最近的那个call打个断点继续跟----->
 
 
 
 
它断下来的地方是整个消息处理的头部 !!!
 
 

看到修改全局的内存,高度怀疑是这里修改时间,经过测试果然如此:
 
 
跟进去发现果真调用了绘制函数!!!

所以我们最终确定,设置时间的地址位于0x1003830位置处!
三、编程思路梳理:
1、获取扫雷窗口句柄,然后获得pid
2、nop掉时间inc的函数,实现时间静止
3、读取雷区有效数据
4、模拟鼠标给窗口发消息
5、释放掉申请的内存和句柄
四、扫雷辅助源码
#include<iostream>
using namespace std;
#include<Windows.h>
#define increaseTimeAddr 0x01002FF5
#define setTimeAddr      0x01003830
#define mineStartAddr    0x01005340
int main() {
    //打开扫雷进程
    HWND hWnd = FindWindow(nullptr, L"扫雷");
    if (hWnd != nullptr)
    {
        DWORD dwProcessId;
        GetWindowThreadProcessId(hWnd, &dwProcessId);
        HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
        if (hProcess == nullptr)
        {
            cout << "fail to find the winmine process" << endl;
            CloseHandle(hProcess);
            return -1;
        }
        //测试是否正确打开扫雷进程,如果关闭了,就说明句柄是对的
        //TerminateProcess(hProcess, 0);
        //把时间增加的指令nop掉,实现0秒扫雷
        DWORD oldProtect = 0;
        SIZE_T nBytesWritten1 = 0;
        SIZE_T nBytesWritten2 = 0;
        bool bRet1, bRet2;
        BYTE replaceBuf[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
        VirtualProtectEx(hProcess, (LPVOID)increaseTimeAddr, 6, MEDIA_READ_WRITE, &oldProtect);
        bRet1=WriteProcessMemory(hProcess, (LPVOID)increaseTimeAddr, replaceBuf, sizeof(replaceBuf), &nBytesWritten1);
        bRet2=WriteProcessMemory(hProcess, (LPVOID)setTimeAddr, replaceBuf, sizeof(replaceBuf), &nBytesWritten2);
        VirtualProtectEx(hProcess, (LPVOID)increaseTimeAddr, 6, oldProtect, &oldProtect);
        if (bRet1 == FALSE || bRet2 == FALSE || nBytesWritten1 <= 0 || nBytesWritten2 <= 0) {
            cout << "修改时间失败!\n";
            CloseHandle(hProcess);
            return -1;
        }
        //读取整个雷区的数据
        //设置读取雷区的最大字节数
        DWORD maxSize = 832;
        char* pByte = NULL;
        SIZE_T nBytesRead = 0;
        pByte = (char*)malloc(sizeof(BYTE) * maxSize);
        bool bReadData = ReadProcessMemory(hProcess, (LPVOID)mineStartAddr, pByte, maxSize, &nBytesRead);
        if (!bReadData) {
            CloseHandle(hProcess);
            delete pByte;
            return -1;
        }
        
        //打印一下观察是否读取正确
        /*for (int i = 0; i < maxSize;i++) {
            printf("%x  ", pByte[i]);
        }*/
        //获取当前雷区的大小
        DWORD sizeAddr = 0x01005330;
        DWORD dwHeight = 0, dwWidth = 0;
        ReadProcessMemory(hProcess, (LPVOID)(sizeAddr + 4), &dwWidth, sizeof(DWORD),0);
        ReadProcessMemory(hProcess, (LPVOID)(sizeAddr + 8), &dwHeight, sizeof(DWORD), 0);
        //读取当前雷区的有效数据
        char* pCurByte = NULL;
        pCurByte = (char*)malloc(sizeof(BYTE) * dwHeight * dwWidth);
        int h = 0;
        int index = 0;
        while (1) {
            if (pByte[index] == 0x10 && pByte[index + 1] != 0x10 && pByte[index + dwWidth + 1] == 0x10) {
                for (int j = 0; j < dwWidth; j++) {
                    pCurByte[dwWidth * h + j] = pByte[index + 1];
                    index++;
                }
                h++;
                if (h >= dwHeight)
                    break;
            }
            index++;
        }
        //模拟鼠标点击格子
        for (int i = 0; i < dwWidth*dwHeight; i++) {
            if (i % dwWidth == 0) {
                cout << endl;
            }
            printf("%x ", *(pCurByte+i));
        }
        int x1 = 0, y1 = 0;
        int x = 0, y = 0;
        for (int i = 0; i < dwHeight * dwWidth; i++) {
            if (*(pCurByte+i) != 0xffffff8f) {
                x1 = i % dwWidth;
                y1 = i / dwWidth;
                x = x1 * 16 + 16;
                y = y1 * 16 + 61;
                SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG(x, y));
                SendMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELONG(x, y));
            }
           
        }
        free(pByte);
        free(pCurByte);
        pByte = NULL;
        pCurByte = NULL;
        CloseHandle(hProcess);
    }
    printf("???就这???\n");
    printf("???就这???\n");
    printf("???就这???\n");
    printf("???就这???\n");
    system("pause");
	return 0;
}五、扫雷辅助效果
扫雷窗口最大是24*30

留下你的鼎鼎大名😂😂😂🤣
 
当然还要有适当的凡尔赛:
 
 
好了,今天的逆向辅助实战就到这里了,如果大家喜欢的话,可以私信我,去更新你们想要的游戏噢~喜欢的话就多多点赞、收藏、关注吧!!!🧡🧡🧡🤞🤞



















