热血江湖外挂
使用环境:Visual Studio 2019
学习使用的工具:逆向工具集
- 1.0:最新版人物信息基址:0x02C166D8;最新版背包存放基址:0x02E3B3E4
- 2.0:最新版物品使用call 为0x00838480
- 3.0:
- 人物基址:0x02C176D8;
- 背包存放基址:0x02E3C3E4;
- 游戏主窗口基址:0x01195F88;
- 使用物品的CALL:0x008384F0;
- 人物动作基址:0x02E3CD58;
- 动作使用的CALL:0x007139E0;
- 所有对象数组:0x2E64A28;
- 怪物基址:0x427FBA0
- 角色对象基址:0x427FBA0
- 一般游戏开发的时候,相关的数据都是放到一个结构或者是一个类中,那么这些数据的内存地址相距的比较近;
- 一般内存地址使用CE工具逆向出来后表示为Client.exe+278A75C,表示软件地址加上偏移量为其基址
- 生命值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
-
在添加窗口后,需要进行配置,才能在动态链接库注入后显示窗口
-
为窗口添加Class
-
修改MFC_DLL.cpp的代码
-
// 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 注入到游戏进程中
- 使用到的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)
- 关闭句柄
- HWND FindWindow(lpClassName, lpWindowNAme)
-
#### 代码
#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;
}
- 步骤
- 获取窗口对应的进程的PID
- 根据PID 获取进程
- 获取游戏进程的PID
- 根据PID 获取进程
- 在目标进程分配内存空间,方便写入DLL 全路径
- 将DLL 全路径写入到目标进程
- 远程注入DLL
- 等待目标进程执行完成
- 释放进程空间
- 关闭线程句柄
- 整数
- 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开头单词首字大写
-
新建解决方案GameData
-
创建头文件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;
-
创建源文件StructGame.cpp
- StructGame.cpp
#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; }
- 在游戏中,对应的物品都会有一个结构/类,包含了物品的一些信息
- 使用物品实际上调用了应该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;
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);
}
// 进行数据修改
}
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.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、代码
// 定义变参函数 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);
-
实现
// 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; }
-
控件调用
void CMainDialogWnd::OnBnClickedButton5() { // TODO: 在此添加控件通知处理程序代码 msgTest(NULL); }
-
目录结构
- 思路:
- 通过选中的对象逆向回溯出动作的数组
- 通过动作对象访问逆向回溯到攻击CALL 附近
- 封包断点bp WSASend
-
通过选中动作,利用CE 查找基址
-
使用OD 分析访问内存信息,得到基址
0082D8F2 8D8CB7 3C040000 LEA ECX,DWORD PTR DS:[EDI+ESI*4+43C]
动作公式:[02e3bd58]+43c+4*0
-
找动作的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
-
- 封装动作对象
- 封装动作对象列表
- 封装使用对象功能函数
-
封装基址
// 人物动作使用的CALL 的基址 #define BaseActionCall 0x00713970
-
封装结构
// 动作对象的结构 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;
-
实现结构方法
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; }
-
添加消息类型
#define MSG_ACTIONTEST 3 //测试动作
-
在主线程内调用结构体方法
case MSG_ACTIONTEST: { TCActionList* ptLIst = tList.getData(); ptLIst->TestActionMsg(); //ptLIst->UseActionByIndex(1); ptLIst->UseActionByName((char*)("攻击")); }; break;
-
发送消息到主线程
DWORD testActionMsg(LPVOID lpData) { SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_ACTIONTEST, (LPARAM)lpData); return 0; }
-
绑定控件,执行方法
void CMainDialogWnd::OnBnClickedButton6() { // TODO: 在此添加控件通知处理程序代码 testActionMsg(NULL); }
- 实现怪物选中
- 可能情况:
- 选怪变量被赋值
- 怪物是否被选中的属性
-
玩家:
[2E63A24] //存放的玩家对象的地址
+3428 玩家是否被选中
-
怪物:
[2E63A24]+1A64
选中怪物时,传入怪物的选中ID
没选中怪物时,值为0xFFFF