一般游戏开发的时候,相关的数据都是放到一个结构或者是一个类中,那么这些数据的内存地址相距的比较近;
一般内存地址使用CE工具逆向出来后表示为Client.exe+278A75C,表示软件地址加上偏移量为其基址
注入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
#include
#include
#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;
}
整数
字符串
CString str前缀
结构名 T开头全大写
类名 C开头单词首字大写
新建解决方案GameData
创建头文件BaseGame.h和 StructGame.h
#pragma once
// 游戏人物的基址
#define BaseRole 0x02B8A6D8
#pragma once
#include
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
#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;
}
配置链接库路径
配置 附加包含目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auIb3GNc-1590415578743)(https://i.loli.net/2019/07/12/5d282a710f95f93418.png)]
配置 添加库目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E3RCrCQr-1590415578745)(https://i.loli.net/2019/07/12/5d282aa5ca58875840.png)]
配置编译输出路径
修改输出目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaFEsBsS-1590415578747)(https://i.loli.net/2019/07/12/5d282b31d492535906.png)]
在游戏中,对应的物品都会有一个结构/类,包含了物品的一些信息
使用物品实际上调用了应该CALL
寻CALL 的过程
使用CE工具找到对象地址指针
去查看访问改指针的地址
使用OD 工具对这些地址进行动态调试
远程注入代码(使用金疮药)
push 1
push 1
push 0
mov ecx, 21DF06D0
call 00838470
// 物品结构
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);
}
// 进行数据修改
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wL4Vw681-1590415578751)(https://i.loli.net/2019/07/23/5d3707690948b56906.png)]
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
#include
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");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xyqK7p9r-1590415578757)(https://i.loli.net/2019/07/25/5d396c0068fd480536.png)]
// 定义变参函数
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);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EWZscljm-1590415578759)(https://i.loli.net/2019/07/25/5d396c8ded2eb52094.png)]
将程序注入到主线程
使用临界区
1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EtlphK7R-1590415578762)(https://i.loli.net/2019/07/25/5d3970363d1b650747.png)]
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("金创药(小)");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mS2QrNLF-1590415578767)(https://i.loli.net/2019/07/27/5d3c0b5f3718e26663.png)]
分析思路:从怪物明显的属性入手:名字、血量等
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);
}
目录结构
通过选中动作,利用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