找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2653

积分

0

好友

371

主题
发表于 10 小时前 | 查看: 0| 回复: 0

声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。

前言

学习任何技术,设定一个清晰的目标至关重要。对于 Windows 游戏安全入门而言,经典的扫雷游戏无疑是一个绝佳的起点。尽管它已经被无数人分析透彻,但亲自动手探索一番,仍然是深入理解内存修改与注入技术的最佳实践。我们可以将目标定为编写一个具备“时间暂停”、“透视”、“一键扫雷”等功能的辅助工具。

本文将使用 Cheat Enginex32dbg(或 ollydbg)以及 Visual Studio 2019 作为主要工具,通过分析扫雷游戏,一步步完成这个“妙妙小工具”。若你在学习过程中遇到难题,欢迎到 云栈社区 与众多开发者一同交流探讨。

扫雷游戏分析

游戏数据在内存中表现为具体的地址,因此我们的首要任务是定位这些关键地址。首先打开 Cheat Engine 修改器。

修改时间 -> 时间暂停

扫雷计时器显示的是一个精确数值,因此我们可以使用“精确数值”扫描。游戏开始前计时器为0,所以我们首先扫描0。

Cheat Engine扫描时间初始值界面

让时间变化后,选择“值介于...两者之间”类型再次扫描,并输入一个大概范围(例如10到20)。

Cheat Engine二次扫描时间地址界面

最终可以定位到时间的内存地址为 0x100579c,在CE中显示为 winmine.exe+579C

找到时间内存地址

我们发现这个数据可以直接通过基址加固定偏移访问。接着,我们对该地址使用“找出是什么改写了这个地址”功能,可以得到一条具体的汇编指令及其地址。

定位改写时间的汇编指令

时间地址:0x100579c

修改表情 - 状态分析

修改游戏中央的笑脸表情,实际上是控制游戏状态(重新开始、进行中、胜利、失败)。分析发现,表情状态由两个变量(4字节)控制,它们的状态值在0和1之间切换。
首先,在游戏进行时,扫描数值 1

扫描表情状态值1

然后点击表情按钮重新开始游戏,此时状态改变,扫描数值 0

扫描表情状态值0

重复此过程,可以定位到其中一个表情控制地址:0x1005164 (winmine.exe+5164)。

找到表情内存地址

但将其修改为2或3,表情并无新变化。这说明控制胜利和失败状态的可能是其他地址。通常,相关功能的代码和变量在内存中是连续存放的。因此,我们可以浏览该地址附近的内存区域。

浏览相关内存区域

观察内存数据,尝试修改 0x1005164 - 4 = 0x1005160 处的值为3,发现表情变成了戴墨镜的胜利状态。

浏览内存发现相邻地址

但请注意,这只是改变了显示状态,并不意味着游戏真正通关。 我们可以通过 Cheat Engine 查看该地址的各种数值解释。

查看内存数值格式

表情控制地址:0x10051600x1005164

透视 - 显示雷区

思考:游戏结束时(踩雷或胜利)会自动显示所有雷的位置。因此,我们可以通过动态调试,观察是哪个函数调用后触发了这一显示逻辑。

首先在程序入口点 (EntryPoint) 下断点,然后单步跟踪。

在程序入口点下断点

通过多次调试和观察程序流程(例如跟踪消息循环)。

调试跟踪程序流程

最终发现,函数 0x2F80 是负责处理“踩雷”并显示所有地雷的关键函数。

定位到关键的踩雷显示函数

一键扫雷

在拥有“透视”功能后,手动完成一局游戏直至胜利(点开最后一个非雷格子),并在此过程中通过调试器观察胜利时的函数调用链。

首先,在游戏即将胜利时,观察函数调用。

单步调试胜利判断逻辑

继续跟踪,会发现后续调用了判断和记录相关的函数。

跟踪胜利后的函数调用

后续的函数调用与刷新英雄榜和显示记录有关。

与英雄榜相关的函数

扫雷英雄榜窗口

最终,函数返回到调用点。通过分析可以确定,函数 0x347c 是判断游戏输赢的核心函数。调试发现它由一个参数(0或1)控制结果,这与“透视”功能的实现思路类似,可通过创建带参数的远程线程来调用。

判断游戏输赢的关键函数

编写妙妙小工具

如何实现这个工具?我们选择使用 DLL注入 的方式。而注入方法,这里采用经典的 远程线程注入

前置知识-动态调用DLL与远程线程注入

主要涉及以下几个 Windows API:

LoadLibraryA

加载指定的 DLL 文件并返回其模块句柄。参数是表示 DLL 路径的字符串。

GetProcAddress

获取指定 DLL 中导出函数的地址。第一个参数是模块句柄,第二个参数是函数名,返回值为函数地址。
通过这两个函数,我们可以动态获取并调用任何导出函数。

CreateRemoteThread - 远程线程注入核心

CreateRemoteThread 函数可以在另一个进程(目标进程)的虚拟地址空间中创建一个线程。我们可以让这个新线程执行目标进程中的代码,例如执行 LoadLibraryA 来加载我们的 DLL。
DLL 的主入口点 DllMain 会在特定事件时被调用:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的句柄
                       DWORD  ul_reason_for_call,//调用原因
                       LPVOID lpReserved//隐式加载or显式加载
                     )
{
    switch(ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH://附加到进程上时执行
    case DLL_THREAD_ATTACH://附加到线程上时执行
    case DLL_THREAD_DETACH://从线程上剥离时执行
    case DLL_PROCESS_DETACH://从进程上剥离时执行
        break;
    }
    return TRUE;
}

我们可以在 DLL_PROCESS_ATTACH 分支中编写代码,当 DLL 被加载到目标进程时,这些代码就会自动执行。

远程线程注入的基本步骤是:

  1. 打开目标进程,获取进程句柄。
  2. 在目标进程内分配一块可读可写的内存。
  3. 将我们的 DLL 的完整路径字符串写入这块内存。
  4. 使用 CreateRemoteThread 创建远程线程,线程函数指向 LoadLibraryA,参数指向刚才写入的 DLL 路径地址。
  5. 等待远程线程结束(即 LoadLibrary 返回)。
  6. 释放目标进程中申请的内存。
  7. 关闭打开的句柄。

注入Demo示例:

void Inject(DWORD ProcessId, const char* szPath)
{
    //1.打开目标进程获取句柄
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("进程句柄:%p\n", hProcess);
    //2.在目标进程体内申请空间
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    //3.写入DLL路径
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    //4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    //5.等待返回(loadLibrary返回)
    WaitForSingleObject(hThread, -1);
    //6.释放空间
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    //7.释放句柄
    CloseHandle(hProcess);
    CloseHandle(hThread);
    //返回结果
    AfxMessageBox(L"完成");
}

编写DLL注入器(Loader)

注入器是一个独立的控制台程序,负责查找扫雷进程并将我们的 DLL 注入进去。

#include<windows.h>
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>

DWORD FindProcess()
{
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32;
    pe32 = { sizeof(pe32) };
    BOOL ret = Process32First(hSnap, &pe32);
    while (ret)
    {
        if (!wcsncmp(pe32.szExeFile, L"winmine.exe", 11)) {
            printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);
            return pe32.th32ProcessID;
        }
        ret = Process32Next(hSnap, &pe32);
    }
    return 0;
}

void Inject(DWORD ProcessId, const char* szPath)
{
    //1.打开目标进程获取句柄
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("进程句柄:%p\n", hProcess);
    //2.在目标进程体内申请空间
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    //3.写入DLL路径
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    //4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    //5.等待返回(loadLibrary返回)
    WaitForSingleObject(hThread, -1);
    //6.释放空间
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    //7.释放句柄
    CloseHandle(hProcess);
    CloseHandle(hThread);
}

int main()
{
    DWORD ProcessId = FindProcess();
    while (!ProcessId) {
        printf("未找到扫雷程序,等待两秒再试\n");
        Sleep(2000);
        ProcessId = FindProcess();
    }
    printf("开始注入进程...\n");
    Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");
    printf("注入完毕\n");
}

编写功能DLL

我们使用 MFC DLL 基于对话框(Dialog)的方式编写,并采用静态编译,便于分发。

创建MFC DLL项目

配置在静态库中使用MFC

在资源视图中新建一个 Dialog,设计简单的工具界面。

Mine妙妙小工具界面设计

我们需要在DLL被加载时创建并显示这个对话框。为了避免阻塞 DllMain,我们创建一个新线程来负责界面。

DWORD WINAPI DlgThreadCallBack(LPVOID lp) {
    MineDlg* Dlg;
    Dlg = new MineDlg();
    Dlg->DoModal();
    delete Dlg;
    FreeLibraryAndExitThread(theApp.m_hInstance, 1);
    return 0;
}

// CMineApp 初始化
BOOL CMineApp::InitInstance()
{
    CWinApp::InitInstance();
    ::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);
    return TRUE;
}

时间暂停

之前我们找到了使时间增加的汇编指令 inc [winmine.exe+579C]。通过将该指令替换为 nop(空指令),即可实现时间暂停。

找到需要NOP的指令

编写“时间暂停”和“取消暂停”按钮的事件处理函数:

DWORD GetBaseAddr() {
    HMODULE hMode = GetModuleHandle(nullptr);
    return (DWORD)hMode;
}

void MineDlg::OnBnClickedButton1() // 时间暂停
{
    // TODO: 在此添加控件通知处理程序代码
    auto BaseAddr = GetBaseAddr();
    DWORD TimeOffset = 0x579C;
    DWORD TimeInsOffset = 0x2FF5;
    DWORD InsLen = 6;
    DWORD old;
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
    BYTE INS[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 }; // nop nop nop nop nop nop
    memcpy((void*)(BaseAddr + TimeInsOffset), INS, InsLen);
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}

void MineDlg::OnBnClickedButton2() // 恢复字节即可取消时间暂停
{
    // TODO: 在此添加控件通知处理程序代码
    auto BaseAddr = GetBaseAddr();
    DWORD TimeOffset = 0x579C;
    DWORD TimeInsOffset = 0x2FF5;
    DWORD InsLen = 6;
    DWORD old;
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
    BYTE INS[] = { 0xFF, 0x05, 0x9C, 0x57, 0x00, 0x01 }; // 原指令 inc [winmine.exe+579C]
    memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}

测试效果:

时间暂停功能测试

透视

根据之前的动态调试,函数 0x2F80 是踩雷后显示所有地雷的函数。我们在线程中直接调用这个函数即可实现“透视”效果。这正是 逆向工程 中常用的直接调用内部函数的方法。

void MineDlg::OnBnClickedButton3()
{
    // TODO: 在此添加控件通知处理程序代码
    DWORD ESPOffset = 0x2f80;
    DWORD FuncAddr = GetBaseAddr() + ESPOffset;
    // 创建不带参数的线程
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL);
}

测试效果(红框内为透视出的地雷):

透视功能测试

一键扫雷

与“透视”类似,调用判断输赢的函数 0x347c,并通过参数控制其为“胜利”状态。这里涉及到创建带参数的 远程线程注入

void MineDlg::OnBnClickedButton4()
{
    // TODO: 在此添加控件通知处理程序代码
    DWORD ESPOffset = 0x347C;
    DWORD FuncAddr = GetBaseAddr() + ESPOffset;
    //创建带参数的线程
    struct { int a; } s = { 0 }; // 参数为0可能表示胜利
    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
}

测试效果(触发游戏胜利并破纪录):

一键扫雷功能测试

总结

通过这个完整的“扫雷妙妙工具”项目,我们对 Windows 游戏安全有了初步的实战认识。整个过程强化了软件的逆向思维和动态调试能力:从使用 Cheat Engine 定位关键内存地址和分析数据变化,到使用调试器跟踪程序执行流程、定位关键函数;从理解游戏状态的内存表示,到最终通过远程线程注入技术,将我们的代码植入目标进程并修改其行为。

我们实践了几个关键的 Windows API:OpenProcessVirtualAllocEx/WriteProcessMemoryCreateRemoteThreadLoadLibraryGetProcAddress 等。这些是 Windows 系统编程和 安全 领域的核心知识。学习之路,贵在实践。多写代码,多动手调试,多尝试分析,方能深入理解。共勉。


本文首发于微信公众号,版权归原作者所有。
原文链接:https://mp.weixin.qq.com/s?__biz=MzkxNTIwNTkyNg==&mid=2247549615&idx=1&sn=5de0fec4a85adc4c45c6864eec2c5c56&scene=21#wechat_redirect




上一篇:红队实战必备:15款主流的C2框架工具集详解与对比
下一篇:AI编程助手时代,开发者亟需掌握规范编写与管理技能
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-25 19:24 , Processed in 0.456738 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表