首页
社区
课程
招聘
[原创]热血江湖游戏逆向(部分)
发表于: 2020-5-25 22:03 13792

[原创]热血江湖游戏逆向(部分)

2020-5-25 22:03
13792

外挂编写(一)

学习案例:热血江湖

visual studio 2019" class="anchor" href="#使用环境:visual studio 2019">使用环境:Visual Studio 2019

逆向工具集" class="anchor" href="#学习使用的工具:逆向工具集">学习使用的工具:逆向工具集

注:更新日志:

  • 1.0:最新版人物信息基址:0x02C166D8;最新版背包存放基址:0x02E3B3E4
  • 2.0:最新版物品使用call 为0x00838480
  • 3.0:
    • 人物基址:0x02C176D8‬;
    • 背包存放基址:0x02E3C3E4;
    • 游戏主窗口基址:0x01195F88;
    • 使用物品的CALL:0x008384F0;
    • 人物动作基址:0x02E3CD58;
    • 动作使用的CALL:0x007139E0;
    • 所有对象数组:0x2E64A28;
    • 怪物基址:0x427FBA0
    • 角色对象基址:0x427FBA0
  • 4.0:最新版人物基址:
    • 游戏人物的基址:0x02C166D8
    • 游戏背包的基址存放地址:0x02E3B3E4
    • 添加背包物品使用CALL 的地址:0x008384A0
    • 游戏主窗口基址:0x01194F88
    • 怪物列表基址:0x427EBA0
    • 人物动作基址:0x02E3BD58
    • 人物动作使用的CALL 的基址:0x00713990
    • 角色对象的基址:0x427EBA0
    • 所有对象的数组:0x2E63A28
    • 攻击武功基址:0x02E3CF64
    • 辅助武功基址:0x02E3CF68
    • 添加技能到快捷栏的基址:0x00849780
    • 选中物品基址:0x0125255C
    • 快捷栏基址:0x02E3CF08
    • 寻路基址:0x2E63A24
    • 仓库基址:0x02E4492C
    • 装备基址:0x02E3B3E0
    • 打开NPC菜单CALL:0x007D5130
    • 商店基址:0x02E65600
    • 购买/出售 物品的基址:0x1195000
    • 购买/出售 物品的CALL:0x004F8380
    • 存取多个物品的基址:0x1195000
    • 存取多个物品的CALL:0x004F8380
    • 存取指令:
      • 存放:0xEB1C5211
      • 取出:0x00000000
    • 操作类型:
      • 存取指令:0x008C0094
      • 购买/出售指令:0x00880092

人物属性数据分析

注:

  • 一般游戏开发的时候,相关的数据都是放到一个结构或者是一个类中,那么这些数据的内存地址相距的比较近;

  • 一般内存地址使用CE工具逆向出来后表示为Client.exe+278A75C,表示软件地址加上偏移量为其基址

HP值和MP值

  • 生命值PH: Client.exe+278A75C = 02B8A758
  • 内功值MH: Client.exe+278A75C = 02B8A75C

金币值

  • 进行商品的买卖实现金币值的变动
  • 基址:Client.exe+278A7BC = 02B8A7BC

其他人物属性分析

  • OD软件的使用
    • 使用dd 02B8A758 查找到人物属性基址块
  • 人物属性值以及对应的内存地址
  • 基址 02B8A6D8
    • +0:人物名字
    • +80:生命值(红/HP)
    • +84:内功值(蓝/MP)
    • +88:愤怒值
    • +8C:最大生命值
    • +90:最大内功值
    • +94:最大值愤怒值
    • +98:当前经验值
    • +A0:升级到下一级要的经验值
    • 势力
    • +36:名声
    • +34:一字节空间表示等级
    • +35:一字节空间表示 几转
    • +AC:历练
    • 制造
    • 熟练度
    • 灵兽持有
    • 精力
    • +C8:攻击
    • 武器命中
    • +CC:防御
    • 武器防御
    • +D0:命中
    • 对人战斗
    • +D4:回避
    • 对怪攻击
    • 武功回避
    • 对怪防御
    • +B0:心
    • +B4:气
    • +B8:体
    • +BC:魂
    • +E4:金币值
  • 气功值分析
    • 气功点数:基址 + F0 = 02b8a7c8
    • 第num个气功的点数:(一个字节)02B8A6D8+0f0+4*num
    • 可能是第num个气功的ID(没有就为0):02B8A6D8+0f0+4*num+2

通过注入游戏进程读取游戏内数据

  • 注入DLL

    • 创建MFC DLL

      创建MFC_DLL

  • 在添加窗口后,需要进行配置,才能在动态链接库注入后显示窗口

    • 为窗口添加Class

    • 修改MFC_DLL.cpp的代码

      • ```C++
        // MFC_DLL.cpp: 定义 DLL 的初始化例程。
        //

        #include "pch.h"
        #include "framework.h"
        #include "MFC_DLL.h"

        // 包含含有主窗口的class1的头文件
        #include "CMainDialogWnd.h"

  #ifdef _DEBUG
  #define new DEBUG_NEW
  #endif


  BEGIN_MESSAGE_MAP(CMFCDLLApp, CWinApp)
  END_MESSAGE_MAP()


  // CMFCDLLApp 构造

  CMFCDLLApp::CMFCDLLApp()
  {
      // TODO:  在此处添加构造代码,
      // 将所有重要的初始化放置在 InitInstance 中
  }


  // 唯一的 CMFCDLLApp 对象

  CMFCDLLApp theApp;


  // CMFCDLLApp 初始化

  // 定义全局的窗口变量
  CMainDialogWnd* PMainDialog;

  BOOL CMFCDLLApp::InitInstance()
  {
      CWinApp::InitInstance();

      // 添加显示窗口的代码
      // 创建对象,划分空间
      PMainDialog = new CMainDialogWnd;
      //DoModal 是以阻塞的方式来运行
      PMainDialog->DoModal();
      // 释放空间
      delete PMainDialog;
      return TRUE;
  }

  ```
  • 使用注入工具讲编译生成的DLL 注入到游戏进程中

    • 注:DoModal() 函数是以阻塞的方式去执行的,所以会造成线程阻塞

    • 解决方式:将DoModal() 放到新的线程去执行

    • 实现窗口关闭后自动释放DLL

    使用代码实现动态连接库的注入

  • 使用到的windows API

    • HWND FindWindow(lpClassName, lpWindowNAme)
      • 通过类名指针或窗口名指针获取窗口句柄
    • DWORD GetWindowThreadProcessId(hwnd(窗口句柄), lpdwProcessId)
      • 获取窗口线程句柄的ID(lpdwProcessId)
    • HANDLE WINAPI OpenProcess(dwDesiredAccess(访问权限), bInheritHandle, dwProcessId)
      • 开启并创建一个本地进程
    • LPVOID WINAPI VirtualAllocEx(hProcess, lpAddress, dwSize, flAllocationType, flProtect)
      • 分配内存空间
    • BOOL WINAPI WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten)
      • 向内存中写入数据
    • HANDLE WINAPI CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId)
      • 为进程创建一个线程
    • DWORD WINAPI WaitForSingleObject(hHandle, dwMilliseconds)
      • 等待单个对象执行后再进行操作
    • BOOL WINAPI CloseHandle(hObject)
      • 关闭句柄
    • BOOL WINAPI VirtualFreeEx(hProcess, lpAddress, dwSize, dwFreeType)
      • 释放内存空间
    • BOOL WINAPI CloseHandle(hObject)
      • 关闭句柄
#### 代码
#include <iostream>
#include <windows.h>
#define GameClassName "D3D Window"
#define DllPath "D:\\c_work\\MFC_DLL\\Debug\\MFC_DLL.dll"

using namespace std;


void InjectDll() {

    DWORD pid = 0;
    HANDLE hProcess = NULL;
    LPDWORD lpAddr = NULL; // 获取远程分配成功的地址
    DWORD size = NULL;
    HANDLE threadHandle = NULL;

    // 获取游戏窗口句柄
    HWND GameH = FindWindow((LPCTSTR)GameClassName, NULL);
    if (GameH != 0) {
        //句柄获取成功
        // 获取进程PID

        GetWindowThreadProcessId(GameH, &pid);
        if (pid != 0) {
            // PID 获取成功
            // 获取进程句柄
            // 开启所以权限打开进程

            hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE ,pid);
            if (hProcess != NULL) {
                // 打开进程成功
                // 分配内存空间,写入动态链接库的全路径名
                //D:\\c_work\\MFC_DLL\\Debug\\MFC_DLL.dll

                lpAddr = (LPDWORD)VirtualAllocEx(hProcess, NULL, 256, MEM_COMMIT, PAGE_READWRITE);
                if (lpAddr != NULL) {
                    // 地址分配成功, 写入DLL 的全路径

                    WriteProcessMemory(hProcess, lpAddr, DllPath, strlen(DllPath) + 1, &size);
                    if (size >= strlen(DllPath)) {
                        threadHandle = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddr, NULL, NULL);
                        // 等待注入DLL 的线程执行完再执行下一步(等待的进程句柄, 等到多少毫秒)
                        WaitForSingleObject(threadHandle, 0xFFFFFFFF);
                        // 关闭线程
                        CloseHandle(threadHandle);
                        // 释放进程
                        VirtualFreeEx(hProcess, lpAddr, 256, MEM_DECOMMIT);
                        // 关闭句柄
                        CloseHandle(hProcess);


                        // 清除内存数据

                    }
                    else {
                        cout << "写入DLL 失败" << endl;
                    }
                }
                else {
                    cout << "地址分配失败" << endl;
                }

            }
            else {
                cout << "打开进程失败" << endl;
            }
        }
        else {
            cout << "获取PID 失败" << endl;
        }
    }
    else {
        cout << "获取窗口句柄失败" << endl;
    }

}

int main()
{

    // 添加注入DLL 代码
    cout << "开始注入DLL" << endl;
    InjectDll();
    cout << "注入DLL结束" << endl;

}
  • 步骤
    1. 获取窗口对应的进程的PID
    2. 根据PID 获取进程
    3. 获取游戏进程的PID
    4. 根据PID 获取进程
    5. 在目标进程分配内存空间,方便写入DLL 全路径
    6. 将DLL 全路径写入到目标进程
    7. 远程注入DLL
    8. 等待目标进程执行完成
    9. 释放进程空间
    10. 关闭线程句柄

整合游戏内数据

  • 整数

    • QWORD 类型变量 nq前缀 //8字节 无符号整数 不能表示负数
    • DWORD 类型变量 nd前缀 //4字节 无符号整数 不能表示负数
    • WORD 类型变量 nw前缀 //2字节 无符号整数 不能表示负数
    • BYTE 类型变量 nb前缀 //1字节 无符号整数 不能表示负数
    • int 带符号类型 ni前缀 //4字节 带符号整数 可表示正负数
    • _int64 带符号整型 ni64 //8字节 带符号整数 不能表示负数
    • UINT 类型变量 ui前缀 // 无符号整数 一般是4字节
    • //浮点数
    • float 单精度浮点数 fl前缀
    • double 双精度浮点数 fd前缀
  • 字符串

    • char*和char [] sz前缀 //PCHAR szp
  • CString str前缀
  • 结构名 T开头全大写
  • 类名 C开头单词首字大写

整合游戏数据步骤

  1. 新建解决方案GameData

  2. 创建头文件BaseGame.h和 StructGame.h


    • BaseGame.h
    #pragma once
    
    // 游戏人物的基址
    #define BaseRole 0x02B8A6D8
    

    • StructGame.h
    #pragma once
    #include <windows.h>
    typedef unsigned __int64 QWORD;
    // 游戏结构以及偏移量的管理
    
    typedef struct TROLE_PROPERTY {
    
        // +0:人物名字
        char* szRoleName;
        // + 80:生命值(红 / HP)
        DWORD ndRoleHP;
        // + 84:内功值(蓝 / MP)
        DWORD ndRoleMP;
        // + 88:愤怒值
        DWORD ndRoleAnger;
        // + 8C:最大生命值
        DWORD ndRoleMaxHP;
        // + 90:最大内功值
        DWORD ndRoleMaxMP;
        // + 94:最大值愤怒值
        QWORD nqRoleMaxAnger;
        // + 98:当前经验值
        QWORD nqRoleExprienceNow;
        // + A0:升级到下一级要的经验值
        DWORD ndRoleExperienceNext;
        // + 36:名声
        char* szReputation;
        // + 34:一字节空间表示等级
        BYTE nbClassValue;
        // + 35:一字节空间表示 几转
        BYTE nbJZ;
        // + AC:历练
        DWORD ndExprience;
        // + C8:攻击
        DWORD ndAttack;
        // + CC:防御
        DWORD ndDefense;
        // + D4:回避
        DWORD ndAvoid;
        // + B0:心
        DWORD ndHeart;
        // + B4:气
        DWORD ndGas;
        // + B8:体
        DWORD ndBody;
        // + BC:魂
        DWORD ndSoul;
        // + E4:金币值
        QWORD nqMoney;
        // 气功
        DWORD ndQg[32];
    
        TROLE_PROPERTY* GetData();
    
        char* GetRoleName();
    
    }_TROLE_PROPERTY;
    
  1. 创建源文件StructGame.cpp


    • StructGame.cpp

    ```c++
    #include "StructGame.h"
    #include "BaseGame.h"
    TROLE_PROPERTY* TROLE_PROPERTY::GetData()
    {

    // 添加异常处理
    try {
        // +0:人物名字
        szRoleName = (char*)BaseRole;
        // + 80:生命值(红 / HP)
        ndRoleHP = (DWORD)(BaseRole + 0x80);
        // + 84:内功值(蓝 / MP)
        ndRoleMP = (DWORD)(BaseRole + 0x84);
        // + 88:愤怒值
        ndRoleAnger = (DWORD)(BaseRole + 0x88);
        // + 8C:最大生命值
        ndRoleMaxHP = (DWORD)(BaseRole + 0x8c);
        // + 90:最大内功值
        ndRoleMaxMP = (DWORD)(BaseRole + 0x90);
        // + 94:最大值愤怒值
        nqRoleMaxAnger = (QWORD)(BaseRole + 0x94);
        // + 98:当前经验值
        nqRoleExprienceNow = (QWORD)(BaseRole + 0x98);
        // + A0:升级到下一级要的经验值
        ndRoleExperienceNext = (DWORD)(BaseRole + 0xA0);
        // + 36:名声
        szReputation = (char*)(BaseRole + 0x36);
        // + 34:一字节空间表示等级
        nbClassValue = *(BYTE*)(BaseRole + 0x34);
        // + 35:一字节空间表示 几转
        nbJZ = *(BYTE*)(BaseRole + 0x35);
        // + AC:历练
        ndExprience = (DWORD)(BaseRole + 0xac);
        // + C8:攻击
        ndAttack = (DWORD)(BaseRole + 0xc8);
        // + CC:防御
        ndDefense = (DWORD)(BaseRole + 0xcc);
        // + D4:回避
        ndAvoid = (DWORD)(BaseRole + 0xd4);
        // + B0:心
        ndHeart = (DWORD)(BaseRole + 0xb0);
        // + B4:气
        ndGas = (DWORD)(BaseRole + 0xb4);
        // + B8:体
        ndBody = (DWORD)(BaseRole + 0x8c);
        // + BC:魂
        ndSoul = (DWORD)(BaseRole + 0xbc);
        // + E4:金币值
        nqMoney = (QWORD)(BaseRole + 0xe4);
    
        for (int i = 0; i < 32; i++) {
            ndQg[i] = *(BYTE*)(BaseRole + 0xf0 + 4 * (i + 1));
        }
    }
    catch (...) {
        // 处理所有的异常
        OutputDebugStringA("读取人物数据异常\r\n");
    }
    
   return this;

}

 

// 获取角色的名称、
char* TROLE_PROPERTY::GetRoleName() {
return GetData()->szRoleName;
}

### VS2019 导入静态链接库(lib)、配置输出路径的方式

* 配置链接库路径

  * 配置 附加包含目录

    ![](784K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6i4K6u0W2L8r3!0D9K9g2)9J5k6h3&6W2N6q4)9J5c8U0t1H3x3e0W2Q4x3V1j5H3y4#2)9J5c8U0p5J5i4K6u0r3y4h3b7J5z5o6u0S2y4K6p5H3k6U0V1#2k6U0V1K6y4o6p5^5i4K6u0W2M7r3&6Y4i4K6t1&6

  * 配置 添加库目录

    ![](16cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6i4K6u0W2L8r3!0D9K9g2)9J5k6h3&6W2N6q4)9J5c8U0t1H3x3e0W2Q4x3V1j5H3y4#2)9J5c8U0p5J5i4K6u0r3y4h3b7J5z5o6u0S2j5e0g2U0j5e0f1^5z5o6M7#2z5o6b7H3i4K6u0W2M7r3&6Y4i4K6t1&6

* 配置编译输出路径

  * 修改输出目录

    ![](610K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6i4K6u0W2L8r3!0D9K9g2)9J5k6h3&6W2N6q4)9J5c8U0t1H3x3e0W2Q4x3V1j5H3y4#2)9J5c8U0p5J5i4K6u0r3y4h3b7J5z5o6u0T1x3K6q4V1y4o6V1J5y4e0x3#2z5e0l9$3i4K6u0W2M7r3&6Y4i4K6t1&6



### 游戏中的物品使用
* 在游戏中,对应的物品都会有一个结构/类,包含了物品的一些信息

* 使用物品实际上调用了应该CALL

##### 以金疮药为例

* 寻CALL 的过程

  * 使用CE工具找到对象地址指针

  * 去查看访问改指针的地址

  * 使用OD 工具对这些地址进行动态调试

  * 远程注入代码(使用金疮药)

  * ```汇编
    push 1
    push 1
    push 0
    mov ecx, 21DF06D0
    call 00838470
背包数据的分析
  • 背包在游戏中一般会写成应该结构体/类来存放物品对象
  • 物品对象在背包中使用数组的形式存在
  • 汇编中数组的访问方式一般是 数组基址 + 4 * i(' i '为数组下标)
  • 查找背包数组基址:
    • 找到背包的物品格
    • 反复讲里面的物品拿出/放入
    • 使用CE工具进程分析
  • 结果
    • 存放背包基址的内存空间:0x02DAF3E4
    • 第num 个格子的数据获取
      • 背包基址+num\4+0x43C
      • 注:0x43 是偏移量
      • 物品对象指针 + 0x64 = 物品名字
      • 物品对象指针 + 0xf9 = 对物品的描述
      • 物品对象指针 + 0xC4C = 物品剩余数量

封装背包数据

封装背包结构体

// 物品结构
typedef struct TBACKPACK_GOODS {

    // *物品对象指针 + 0x64 = 物品名字
    char* szGoodsName;
    // * 物品对象指针 + 0xf9 = 对物品的描述
    char* szGoodsIntro;
    // * 物品对象指针 + 0xC4C = 物品剩余数量
    DWORD ndGoodsNum;

} _TBACKPACK_GOODS;

// 背包结构
typedef struct TGOODSLIST_PROPERTY {
    _TBACKPACK_GOODS mtGoodsList[nGoodsNum];

    // 对数据的初始化
    TGOODSLIST_PROPERTY* getData();
}_TGOODSLIST_PROPERTY;

实现初始化方法(getDate())

TGOODSLIST_PROPERTY* TGOODSLIST_PROPERTY::getData()
{

    // 通过获取背包基址对每样物品进行分析

// *物品对象指针 + 0x64 = 物品名字
#define GOODSNAME 0x64
// * 物品对象指针 + 0xf9 = 对物品的描述
#define GOODSINTRO 0xf9
// * 物品对象指针 + 0xC4C = 物品剩余数量
#define GOODSNUM 0xc4c

    // 背包公式: ndBaseAddr + num*4 + 0x43c
    try {
        // 读取背包基址
        DWORD ndBase = *(DWORD*)(BaseBackpack);
        // 第一个物品的地址
        DWORD ndFirstGoodsBase = ndBase + 4 * 0 + 0x43c;
        // 第一个物品的对象
        DWORD ndObj = NULL;
        for (int i = 0; i < nGoodsNum; i++) {
            ndObj = *(DWORD*)(ndFirstGoodsBase + 4 * i); // 取出第i个对象的地址
            if (ndObj == NULL) {
                // 如果读取数据为0===> 背包这一格没有物品
                this->mtGoodsList[i].ndGoodsNum = 0;
                continue;
            }
            // 读取物品的名字
            this->mtGoodsList[i].szGoodsName = (char*)(ndObj + GOODSNAME);
            // 读取物品的介绍
            this->mtGoodsList[i].szGoodsIntro = (char*)(ndObj + GOODSINTRO);
            // 读取物品的剩余数量
            this->mtGoodsList[i].ndGoodsNum = *(DWORD*)(ndObj + GOODSNUM);

        }
    }
    catch (...) {
        // 处理所有异常
        OutputDebugStringA("读取背包数据异常\r\n");
        MessageBox(NULL, "读取背包数据异常(StructGame)", "Error", MB_OK);
    }

    return this;
}

调试调用

void CMainDialogWnd::OnBnClickedButton1()
{
    TROLE_PROPERTY role;
    TROLE_PROPERTY* r = role.GetData();
    TGOODSLIST_PROPERTY goods;

    TRACE("GameDebug:我的调试信息\r\n");
    TRACE("GameDebug: 人物名=%s\r\n", r->GetRoleName());
    TRACE("GameDebug: 人物等级=%d\r\n", r->nbClassValue);
    TRACE("GameDebug: 人物名声=%s\r\n", r->szReputation);
    TRACE("GameDebug: 人物血量HP=%d//%d\r\n", r->ndRoleHP, r->ndRoleMaxHP);
    TRACE("GameDebug: 人物内功MP=%d//%d\r\n", r->ndRoleMP, r->ndRoleMaxMP);
    TRACE("GameDebug: 人物愤怒值=%d\r\n", r->ndRoleAnger);
    TRACE("GameDebug: 人物金币=%d\r\n", r->nqMoney);
    TGOODSLIST_PROPERTY* g = goods.getData();
    try {
        for (int i = 0; i < nGoodsNum; i++) {
            if (g->mtGoodsList[i].ndGoodsNum == 0) {
                continue;
            }
            TRACE("GameDebug: 人物第%d格数据:%s\r%s\r%d\r\n", i,
                g->mtGoodsList[i].szGoodsName,
                g->mtGoodsList[i].szGoodsIntro,
                g->mtGoodsList[i].ndGoodsNum
            );
        }
    }
    catch (...) {
        MessageBox(TEXT("读取背包数据异常(Dialog)"), TEXT("Error"), MB_OK);
    }


    // 进行数据修改
}

文件结构

背包物品的使用

之前Call 的分析

push 背包物品下标
push 1
push 0
mov ecx, 背包基址
call 00838470

封装函数

UseGoods(char* szGoodsName)
{
    // 若存在则使用它
    return 1;
}

背包物品使用代码

  • 定义基址
// 添加背包物品使用CALL 的地址 ===> 通过背包物品下标进行物品的使用
#define BaseCall_UseGoodsForIndex 0x00838470
  • 定义结构
// 背包结构
typedef struct TGOODSLIST_PROPERTY {
    // 背包列表
    _TBACKPACK_GOODS mtGoodsList[nGoodsNum];

    // 对数据的初始化
    TGOODSLIST_PROPERTY* getData();

    // 使用背包物品
    int UseGoodsForIndex(DWORD ndIndex);

    // 通过名字查询下标,存在返回下标,不存在返回FALSE
    int GetGoodsIndexByName(char* szGoodsName);

    // 根据物品的名字进行使用
    int UseGoodsForName(char* szGoodsName);
}_TGOODSLIST_PROPERTY;
  • 实现方法
// 通过物品下标使用物品
int TGOODSLIST_PROPERTY::UseGoodsForIndex(DWORD ndIndex) {

    try {
        // 使用内联汇编
        __asm {
            mov eax, ndIndex
            push eax
            push 1
            push 0
            // 读取背包地址
            mov ecx, [BaseBackpack]
            mov eax, BaseCall_UseGoodsForIndex
            call eax
        }
    }
    catch (...) {
        OutputDebugStringA("物品使用异常");
    }

    return TRUE;

}

int TGOODSLIST_PROPERTY::UseGoodsForName(char* szGoodsName)
{
    // 查找物品的下标
    DWORD ndIndex = this->GetGoodsIndexByName(szGoodsName);
    if (ndIndex != -1) {
        this->UseGoodsForIndex(ndIndex);
        return TRUE;
    }
    return FALSE;
}

int TGOODSLIST_PROPERTY::GetGoodsIndexByName(char* szGoodsName) {
    // 遍历整个背包,看是否存在该物品
    TGOODSLIST_PROPERTY* g = this->getData();// 初始化背包结构
    for (int i = 0; i < nGoodsNum; i++) {
        // 比较字符串,判断该物品是否存在
        if (strcmp(szGoodsName, g->mtGoodsList[i].szGoodsName) == 0) {
            return i;
        }
    }

    return -1;
}
  • 调用方法,实现物品的使用
if (g->UseGoodsForName("回城符(泫勃派)")) {
        TRACE("GameDebug: 使用 回城符(泫勃派) 成功");
    }
  • 文件结构

编写自定义的DbgPrintMine方法用于打印格式化调式信息

// DbgPrintMine.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
using namespace std;

// 定义变参函数
void DbgPrintMine(char* pszFormat, ...) {
#ifdef _DEBUG
    // 如果在DEBUG 版本下才执行以下代码
    // 定义list
    va_list argList;
    // 初始化list
    va_start(argList, pszFormat);
    // 定义字符串缓冲区
    char szBufFormat[0x1000];

    // 定义调试前缀
    char szBufFormat_Game[0x1008] = "Game:";


    // 获取参数 va_arg(list, paramType)
    /*int i = va_arg(argList, int);
    int j = va_arg(argList, int);
    char* szK = va_arg(argList, char*);*/


    vsprintf_s(szBufFormat, pszFormat, argList);
    strcat_s(szBufFormat_Game, szBufFormat);
    OutputDebugStringA(szBufFormat_Game);


    // 清除list
    va_end(argList);
#endif

}


int main()
{
    DbgPrintMine((char*)"%d, %d, %s\n", 1, 2, "贾谨荣");
    system("pause");
}

注:多线程访问数据造成异常的原因以及解决方式

造成异常的原因:

  • 游戏主线程与外挂线程同时访问共享数据区域,造成程序异常
  • 让两个线程依次使用共享数据或者将注入线程到主线程

模拟游戏主线程和辅助线程同时执行

  • 代码
// 定义变参函数
void DbgPrintMine(char* pszFormat, ...) {
#ifdef _DEBUG
    // 如果在DEBUG 版本下才执行以下代码
    // 定义list
    va_list argList;
    // 初始化list
    va_start(argList, pszFormat);
    // 定义字符串缓冲区
    char szBufFormat[0x1000];

    // 定义调试前缀
    char szBufFormat_Game[0x1008] = "Game:";


    // 获取参数 va_arg(list, paramType)
    /*int i = va_arg(argList, int);
    int j = va_arg(argList, int);
    char* szK = va_arg(argList, char*);*/


    vsprintf_s(szBufFormat, pszFormat, argList);
    strcat_s(szBufFormat_Game, szBufFormat);
    OutputDebugStringA(szBufFormat_Game);


    // 清除list
    va_end(argList);
#endif

}

DWORD g_ndGameData[10] = { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
DWORD* g_pndGameData[10];

void UseGoods(char* szGoodsName) {

    for (int i = 0; i < 10; i++) {
        DbgPrintMine("%s, %d\r\n", szGoodsName, *g_pndGameData[i]);
        Sleep(1 * 1000);
    }
    return;
}

DWORD WINAPI GameMainThreadProc(LPVOID lpData) {
    while (TRUE) {
        // 初始化内存
        memset(g_pndGameData, NULL, sizeof(g_pndGameData));
        for (int i = 0; i < 10; i++) {
            g_pndGameData[i] = g_ndGameData + i; // &g_ndGameData[i]
            Sleep(1000);
        }
        // 物品使用的CALL
        UseGoods("游戏主线程");
    }
}

DWORD WINAPI GameMyThreadProc(LPVOID lpData) {
    while (TRUE) {
        // 初始化内存

        UseGoods("外挂线程:222");
        Sleep(1 * 1000);
    }
}

// 游戏主线程
void CDataExceptionTestDlg::OnBnClickedButton1()
{
    // TODO: 在此添加控件通知处理程序代码
    CreateThread(NULL, NULL, GameMainThreadProc, NULL, 0, NULL);
}

// 外挂线程
void CDataExceptionTestDlg::OnBnClickedButton2()
{
    // TODO: 在此添加控件通知处理程序代码
    CreateThread(NULL, NULL, GameMyThreadProc, NULL, 0, NULL);
}
  • 异常

解决方式

  • 将程序注入到主线程

  • 使用临界区

    • 1

    • 2、代码

      ```c++

      // 定义变参函数
      void DbgPrintMine(char* pszFormat, ...) {
      #ifdef _DEBUG

      // 如果在DEBUG 版本下才执行以下代码
      // 定义list
      va_list argList;
      // 初始化list
      va_start(argList, pszFormat);
      // 定义字符串缓冲区
      char szBufFormat[0x1000];
      
      // 定义调试前缀
      char szBufFormat_Game[0x1008] = "Game:";
      
    // 获取参数 va_arg(list, paramType)
    /*int i = va_arg(argList, int);
    int j = va_arg(argList, int);
    char* szK = va_arg(argList, char*);*/


    vsprintf_s(szBufFormat, pszFormat, argList);
    strcat_s(szBufFormat_Game, szBufFormat);
    OutputDebugStringA(szBufFormat_Game);


    // 清除list
    va_end(argList);
#endif

}

// 定义临界区;
CRITICAL_SECTION lpCriticalSection;

DWORD g_ndGameData[10] = { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
DWORD* g_pndGameData[10];

void UseGoods(char* szGoodsName) {
    // 进入临界区
    EnterCriticalSection(&lpCriticalSection);

    for (int i = 0; i < 10; i++) {
        DbgPrintMine("%s, %d\r\n", szGoodsName, *g_pndGameData[i]);
        Sleep(1 * 100);
    }
    // 离开临界区
    LeaveCriticalSection(&lpCriticalSection);

    return;
}

DWORD WINAPI GameMainThreadProc(LPVOID lpData) {
    while (TRUE) {
        // 进入临界区
        EnterCriticalSection(&lpCriticalSection);

        // 初始化内存
        memset(g_pndGameData, NULL, sizeof(g_pndGameData));
        for (int i = 0; i < 10; i++) {
            g_pndGameData[i] = g_ndGameData + i; // &g_ndGameData[i]
            Sleep(1000);
        }
        // 离开临界区
        LeaveCriticalSection(&lpCriticalSection);

        // 腾出有点时间片给外挂线程使用
        Sleep(1 * 1000);
        // 物品使用的CALL
        UseGoods("游戏主线程");
    }
}

DWORD WINAPI GameMyThreadProc(LPVOID lpData) {
    while (TRUE) {
        // 初始化内存

        UseGoods("外挂线程:222");
        Sleep(1 * 1000);
    }
}

// 游戏主线程
void CDataExceptionTestDlg::OnBnClickedButton1()
{
    // TODO: 在此添加控件通知处理程序代码
    CreateThread(NULL, NULL, GameMainThreadProc, NULL, 0, NULL);
}

// 外挂线程
void CDataExceptionTestDlg::OnBnClickedButton2()
{
    // TODO: 在此添加控件通知处理程序代码
    CreateThread(NULL, NULL, GameMyThreadProc, NULL, 0, NULL);
}


void CDataExceptionTestDlg::OnBnClickedOk()
{
    // TODO: 在此添加控件通知处理程序代码
    CDialogEx::OnOK();
}


void CDataExceptionTestDlg::OnBnClickedButton3()
{
    // TODO: 在此添加控件通知处理程序代码
    // 初始化临界区
    InitializeCriticalSection(&lpCriticalSection);
}

```

将代码注入游戏的主线程

  • 关键词

    SetWindowsHooksExa UnhookWindowsHookEx CWPSTRUCT
    

定义方法

#pragma once

// HookGameMainThread.h
#define MSG_USEGOODSFORNAME 1 //使用物品的消息种类

// 挂载主线程
DWORD HookMainThread();

// 卸载主线程
DWORD UnHookMainThread();

DWORD msgUseGoodsForName(char* szpName);

实现方法

// HookGameMainThread.cpp
#include "StructGame.h"
#include "HookGameMainThread.h"

HHOOK g_hhkGame;
const DWORD MyMsgCode = RegisterWindowMessageA("MyMsgCode");
// 回调函数
LRESULT CALLBACK GameWndProc(
    int nCode,
    WPARAM wParam,
    LPARAM lParam
) {
    CWPSTRUCT* lpArg = (CWPSTRUCT*)lParam;
    if (nCode == HC_ACTION) {
        if (lpArg->hwnd == GetGameWndHandle() && lpArg->message == MyMsgCode) {
            DbgPrintMine((char*)("消息传到    %s\r\n"), lpArg->lParam);
            switch (lpArg->wParam)
            {
            case MSG_USEGOODSFORNAME: {
                TGOODSLIST_PROPERTY goods;
                TGOODSLIST_PROPERTY* g = goods.getData();
                if (g->UseGoodsForName((char*)lpArg->lParam)) {
                    DbgPrintMine((char*)("使用 %s 成功"), lpArg->lParam);
                }
            }; break;
            default:
                break;
            }
            return 1;
        }
    }
    return CallNextHookEx(g_hhkGame, nCode, wParam, lParam);
}
DWORD HookMainThread() {

    HWND hGame = GetGameWndHandle();
    DWORD ndThreadId = GetWindowThreadProcessId(hGame, NULL);
    if (ndThreadId != 0) {
        // 安装钩子
        g_hhkGame = SetWindowsHookEx(WH_CALLWNDPROC, GameWndProc, NULL, ndThreadId);
    }
    return 1;
}


DWORD UnHookMainThread() {
    UnhookWindowsHookEx(g_hhkGame);
    return 1;
}

DWORD msgUseGoodsForName(char* szpName) {
    // 传递消息(句柄、自定义的注册消息、自定义消息类别、消息内容(字符串))
    SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_USEGOODSFORNAME, (LPARAM)szpName);
    return 1;
}

调用方法

// 连接主线程
void CMainDialogWnd::OnBnClickedButton2()
{
    // TODO: 在此添加控件通知处理程序代码
    HookMainThread();
}


void CMainDialogWnd::OnBnClickedButton3()
{
    // TODO: 在此添加控件通知处理程序代码
    UnHookMainThread();
}


void CMainDialogWnd::OnBnClickedButton4()
{
    // TODO: 在此添加控件通知处理程序代码
    msgUseGoodsForName("金创药(小)");
}

文件结构

运行效果

分析怪物列表

  • 分析思路:从怪物明显的属性入手:名字、血量

    1*4+427EBA0 //怪物列表基址(1-5)

    +8 种类/2E:怪物

    +354 显示血条

    +C 怪物选中参数

    +5f4 怪物血量

    +5f8 怪物等级

    +360 怪物名字

    +1060 怪物位置X

    +1068 怪物位置Y

    +3C0 怪物生命状态 0活/1死

    [0427EBA0] //角色对象指针

    +8 //角色分类31人物/2E

    +18 //角色名字

封装怪物对象属性

  • 定义基址

    // 怪物列表基址
    #define BaseMonseterList 0x427EBA4
    
  • 定义结构

    // 怪物结构
    typedef struct TMonseterObj {
        //+5f4 怪物血量
        DWORD ndHp;
        //+ 5f8 怪物等级
        DWORD ndLevel;
        //+ 360 怪物名字
        char* szMName;
        //+ 1060 怪物位置X
        float flX;
        //+ 1068 怪物位置Y
        float flY;
        //+ 3C0 怪物生命状态 0活 / 1死
        BOOL IsDead;
    }_TMonseterObj;
    
    // 怪物列表
    #define MONSETERNUM 20
    typedef struct TMonseterList {
    
        _TMonseterObj tMonList[MONSETERNUM];
    
        // 初始化
        TMonseterList* getData();
    
        // 打印信息
        BOOL dbgPrintMsg();
    }_TMonseterList;
    
  • 实现结构方法

    TMonseterList* TMonseterList::getData()
    {
        DWORD ndObj = NULL;
        //memset(this, 0, sizeof(TMonseterList));
        try
        {
            for (int i = 0; i < MONSETERNUM; i++) {
                ndObj = *(DWORD*)(BaseMonseterList + 4 * i);
                if (ndObj == 0) {
                    this->tMonList[i].ndLevel = 0;
                    continue;
                }
                // 怪物名字
                this->tMonList[i].szMName = (char*)(ndObj + 0x360);
                // 怪物血量
                this->tMonList[i].ndHp = *(DWORD*)(ndObj + 0x5f4);
                // 怪物等级
                this->tMonList[i].ndLevel = *(DWORD*)(ndObj + 0x5f8);
                // 怪物位置X
                this->tMonList[i].flX = *(float*)(ndObj + 0x1060);
                // 怪物位置Y
                this->tMonList[i].flY = *(float*)(ndObj + 0x1068);
                // 怪物生命状态
                this->tMonList[i].IsDead = *(BOOL*)(ndObj + 0x3c0);
            }
        }
        catch (...)
        {
            // 处理所有的异常
            DbgPrintMine((char*)"读取怪物数据异常");
        }
        return this;
    }
    
    BOOL TMonseterList::dbgPrintMsg() {
        for (int i = 0; i < MONSETERNUM; i++) {
            if (tMonList[i].ndLevel == 0) {
                continue;
            }
            DbgPrintMine((char*)("%s,等级:%d级;血量:%d;当前位置X:%f Y:%f;生命状态:%d"),
                tMonList[i].szMName,
                tMonList[i].ndLevel,
                tMonList[i].ndHp,
                tMonList[i].flX,
                tMonList[i].flY,
                tMonList[i].IsDead);
        }
        return TRUE;
    }
    
  • 在HOOK 内定义测试方法

    #define MSG_TEST 2 // 测试使用消息
    
    // 测试怪物
    DWORD msgTest(LPVOID lpData);
    
  • 实现

    ```c++
    // HookGameMainThread.cpp
    #include "StructGame.h"
    #include "HookGameMainThread.h"

    HHOOK g_hhkGame;
    const DWORD MyMsgCode = RegisterWindowMessageA("MyMsgCode");
    // 回调函数
    LRESULT CALLBACK GameWndProc(

    int nCode,
    WPARAM wParam,
    LPARAM lParam
    

    ) {

    CWPSTRUCT* lpArg = (CWPSTRUCT*)lParam;
    if (nCode == HC_ACTION) {
        if (lpArg->hwnd == GetGameWndHandle() && lpArg->message == MyMsgCode) {
            DbgPrintMine((char*)("消息传到    %s\r\n"), lpArg->lParam);
            switch (lpArg->wParam)
            {
            case MSG_USEGOODSFORNAME: {
                TGOODSLIST_PROPERTY goods;
                TGOODSLIST_PROPERTY* g = goods.getData();
                if (g->UseGoodsForName((char*)lpArg->lParam)) {
                    DbgPrintMine((char*)("使用 %s 成功"), lpArg->lParam);
                }
            }; break;
    

    ////////////////////////////////////////////////////////////////////////

            case MSG_TEST: {
                TMonseterList tMonList;
                TMonseterList* ptMonList = tMonList.getData();
                ptMonList->dbgPrintMsg();
            }; break;
            default:
                break;
            }
            return 1;
        }
    }
    return CallNextHookEx(g_hhkGame, nCode, wParam, lParam);
    

    }
    DWORD HookMainThread() {

    HWND hGame = GetGameWndHandle();
    DWORD ndThreadId = GetWindowThreadProcessId(hGame, NULL);
    if (ndThreadId != 0) {
        // 安装钩子
        g_hhkGame = SetWindowsHookEx(WH_CALLWNDPROC, GameWndProc, NULL, ndThreadId);
    }
    return 1;
    

    }

DWORD UnHookMainThread() {
UnhookWindowsHookEx(g_hhkGame);
return 1;
}

 

DWORD msgUseGoodsForName(char* szpName) {
// 传递消息(句柄、自定义的注册消息、自定义消息类别、消息内容(字符串))
SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_USEGOODSFORNAME, (LPARAM)szpName);
return 1;
}

 

////////////////////////////////////////////////////////////////////////
DWORD msgTest(LPVOID lpData)
{
SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_TEST, (LPARAM)lpData);
return 0;
}

* 控件调用

  ```c++

  void CMainDialogWnd::OnBnClickedButton5()
  {
      // TODO: 在此添加控件通知处理程序代码
      msgTest(NULL);
  }
  • 目录结构

    img

分析动作数组(攻击和打坐)

  • 思路:
    • 通过选中的对象逆向回溯出动作的数组
    • 通过动作对象访问逆向回溯到攻击CALL 附近
    • 封包断点bp WSASend
  1. 通过选中动作,利用CE 查找基址

  2. 使用OD 分析访问内存信息,得到基址

    0082D8F2    8D8CB7 3C040000 LEA ECX,DWORD PTR DS:[EDI+ESI*4+43C]
    

    动作公式:[02e3bd58]+43c+4*0

  3. 找动作的CALL

    • 使用CE 分析动作对象的调用访问

    • 得到一下信息

      // 攻击
      008530CE - 6A 01 - push 01
      008530D0 - E8 0B4AFBFF - call Client.exe+407AE0
      008530D5 - 8B 8C B7 3C040000  - mov ecx,[edi+esi*4+0000043C] <<
      008530DC - 85 C9  - test ecx,ecx
      008530DE - 74 62 - je Client.exe+453142
      
      008544B9 - 83 BF 34160000 35 - cmp dword ptr [edi+00001634],35
      008544C0 - 75 20 - jne Client.exe+4544E2
      008544C2 - 8B 84 B7 3C040000  - mov eax,[edi+esi*4+0000043C] <<
      008544C9 - 85 C0  - test eax,eax
      008544CB - 74 15 - je Client.exe+4544E2
      
    • 使用OD分析得动作CALL为

      mov edi, [02E3CD58]
      mov esi, 下标
      MOV EAX,DWORD PTR DS:[EDI+ESI*4+43C]
      mov ecx, [eax+0x54]
      push ecx
      CALL 007139E0
      

封装动作数组功能

  • 封装动作对象
  • 封装动作对象列表
  • 封装使用对象功能函数

封装

  1. 封装基址

    // 人物动作使用的CALL 的基址
    #define BaseActionCall 0x00713970
    
  2. 封装结构

    // 动作对象的结构
    typedef struct TActionObj {
    
        // 对象名字
        char* szpName;
        // 调用CALL 的参数
        DWORD ndActionID;
    
    }_TActionObj;
    
    // 动作对象数组
    #define ActionNum 18
    typedef struct TCActionList {
    
        // 定义动作数组
        _TActionObj tList[ActionNum];
    
        // 初始化
        TCActionList* getData();
    
        // 打印信息
        BOOL TestActionMsg();
    
        // 使用动作通过下标
        BOOL UseActionByIndex(DWORD ndIndex);
    
        // 使用动作通过名字
        BOOL UseActionByName(char* szpName);
    
    }_TCActionList;
    
  3. 实现结构方法

    ```c++
    TCActionList TCActionList::getData()
    {
    //dc [[02e3bd58]+ 43c + 4
    0] + 64
    //+ 64 动作名字

    //[[02e3bd58]+ 43c + 4 * 0] + 54
    //+ 54 调用CALL参数

    DWORD ndFirstObj = 0;
    DWORD ndObj;
    try {

       ndFirstObj = (*(DWORD*)(BaseActionList))+0x43C;
       for (int i = 0; i < ActionNum; i++) {
           ndObj = *(DWORD*)(ndFirstObj + 4 * i);
           if (ndObj == NULL) {
               tList[i].ndActionID = 0;
               continue;
           }
           tList[i].szpName = (char*)(ndObj + 0x64);
           tList[i].ndActionID = *(DWORD*)(ndObj + 0x54);
       }
    

    }
    catch (...) {

       DbgPrintMine((char*)("内存读取异常"));
    

    }

    return this;
    }

    BOOL TCActionList::TestActionMsg()
    {
    for (int i = 0; i < ActionNum; i++) {

       if (tList[i].ndActionID == 0) {
           continue;
       }
       DbgPrintMine((char*)("动作名:%s, 动作ID:%X"),
           tList[i].szpName,
           tList[i].ndActionID);
    

    }
    return TRUE;
    }

DWORD getObjByIndex(char szpName) {
TCActionList tList;
TCActionList
ptList = tList.getData();
for(int i = 0; i < ActionNum; i++) {
if (strcmp(szpName, ptList->tList[i].szpName) == 0) {
return i;
}
}
return -1;
}

 

BOOL UseAction(DWORD ndIndex) {
TCActionList tList;
TCActionList ptList = tList.getData();
DWORD ndPrarm = ptList->tList[ndIndex].ndActionID;
try {
__asm {
mov ecx, ndPrarm
push ecx
mov eax, BaseActionCall
call eax
}
}
catch (...) {
DbgPrintMine((char
)("动作使用失败"));
return FALSE;
}
return TRUE;
}

 

BOOL TCActionList::UseActionByIndex(DWORD ndIndex)
{
if (UseAction(ndIndex)) {
MessageBeep(0);
return TRUE;
}
return FALSE;
}

 

BOOL TCActionList::UseActionByName(char* szpName)
{
DWORD ndIndex = getObjByIndex(szpName);
if (ndIndex != -1) {
if (UseAction(ndIndex)) {
MessageBeep(0);
return TRUE;
}
}
return FALSE;
}

4. 添加消息类型

   ```c++
   #define MSG_ACTIONTEST 3 //测试动作
  1. 在主线程内调用结构体方法

    case MSG_ACTIONTEST: {
                    TCActionList* ptLIst = tList.getData();
                    ptLIst->TestActionMsg();
                    //ptLIst->UseActionByIndex(1);
                    ptLIst->UseActionByName((char*)("攻击"));
                }; break;
    
  2. 发送消息到主线程

    DWORD testActionMsg(LPVOID lpData) {
        SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_ACTIONTEST, (LPARAM)lpData);
        return 0;
    }
    
  1. 绑定控件,执行方法

    void CMainDialogWnd::OnBnClickedButton6()
    {
        // TODO: 在此添加控件通知处理程序代码
        testActionMsg(NULL);
    }
    
  • 文件结构

    img

选怪功能实现

  • 实现怪物选中
  • 可能情况:
    • 选怪变量被赋值
    • 怪物是否被选中的属性

选怪功能相关地址

  • 玩家:

    [2E63A24] //存放的玩家对象的地址

    +3428 玩家是否被选中

  • 怪物:

    [2E63A24]+1A64

    选中怪物时,传入怪物的选中ID

    没选中怪物时,值为0xFFFF

选怪功能的封装

计算怪物与玩家的距离

自动打怪函数封装

需要特征码可联系


[培训]科锐逆向工程师培训第53期2025年7月8日开班!

收藏
免费 2
支持
分享
最新回复 (17)
雪    币: 45
活跃值: (2840)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
看这写法 有点像郁金香 教的
2020-5-25 22:50
1
雪    币: 3400
活跃值: (850)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
话说为什么游戏有这么多的基地址(全局变量),是因为这些数据使用频率高,定义成全局变量就不用传参了么?
2020-5-26 00:04
0
雪    币: 525
活跃值: (3238)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
4
X-Blades 看这写法 有点像郁金香 教的
老司机了
2020-5-26 03:10
1
雪    币: 259
活跃值: (283)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666
2020-5-26 09:29
0
雪    币: 2235
活跃值: (1360)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6
直接搬代码了
2020-5-26 09:29
0
雪    币: 1918
活跃值: (6765)
能力值: ( LV7,RANK:118 )
在线值:
发帖
回帖
粉丝
7
老郁金香了
2020-5-26 11:21
0
雪    币: 7065
活跃值: (6626)
能力值: ( LV5,RANK:65 )
在线值:
发帖
回帖
粉丝
8
写了这么多,辛苦了,支持一下!
2020-5-26 12:31
1
雪    币: 2232
活跃值: (1346)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
写得 很详细。 支持一下。
2020-5-26 14:36
0
雪    币: 345
活跃值: (429)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
热血江湖 升级 真难升
2020-5-26 15:09
0
雪    币: 351
活跃值: (22)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
老司机 有合成强化相关的嘛?
2020-5-26 17:16
0
雪    币: 73
活跃值: (923)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
我想问下:
push xx
push xx
call YY
现在的程序,基本上每次启动的时候,模块基址都是不一样的,也就意味着YY这个值,可能发生变化,请问,在传入了参数之后,如何能保证正确的调用这个YY呢?
2020-6-8 17:04
0
雪    币: 191
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
kongfubull 我想问下: push xx push xx call YY 现在的程序,基本上每次启动的时候,模块基址都是不一样的,也就意味着YY这个值,可能发生变化,请问,在传入了参数之后,如何能保证正确的 ...
一般在一些简单的游戏里,YY是不会改变的,它这里面的地址就是游戏程序的本身,在热血江湖我之前做过,是以游戏程序本身为基准的地址,YY是不会变的,这涉及到一些虚拟地址的概念。要是复杂点的游戏会是模块+偏移的方式出现,这时需要拍摄系统快照获取游戏模块的地址,然后加上你找到的偏移,就能正确得出YY,当然偏移是不会改变的。
2020-6-9 02:29
0
雪    币: 73
活跃值: (923)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
mb_ruejewdo 一般在一些简单的游戏里,YY是不会改变的,它这里面的地址就是游戏程序的本身,在热血江湖我之前做过,是以游戏程序本身为基准的地址,YY是不会变的,这涉及到一些虚拟地址的概念。要是复杂点的游戏会是模块+偏 ...
多谢回复指点,后来我想明白了,我以为是call YY这条指令的地址。
2020-6-9 11:23
0
雪    币: 20
活跃值: (27)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
kongfubull 多谢回复指点,后来我想明白了,我以为是call YY这条指令的地址。

如果在游戏领空里的地址他是不会变化的,如果地址在其他模块里就会变化,比如CF 

最后于 2020-6-9 23:56 被明天去要饭编辑 ,原因:
2020-6-9 23:53
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
你好
2022-11-30 12:44
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
你好
2022-11-30 12:44
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
楼主怎么联系您?
2022-11-30 12:51
0
游客
登录 | 注册 方可回帖
返回