书接上回
上一贴:[b]Nobody(大多数)游戏修改学习笔记
(感觉继续写在上一帖的话太长了,索性就重新开一贴)
我们先处理上一帖留下的问题,衣物整洁度我目前无法实现编辑的功能,只能hook实现无限数值了,代码会在后面按照顺序讲解
我个人喜欢以调试的视角去解析代码,所以我这里会按照执行顺序一步一步解读
首先是主函数
int main()
{
//读取目标游戏进程ID
DWORD game_PID = GetProcessIDByName(L"Nobody.exe");
if (game_PID == 0)
{
printf("未找到目标游戏进程\n");
return 1;
}
HANDLE game_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, game_PID);
////游戏模块基址
LPVOID base_address = GetModuleBaseAddress(game_PID, L"GameAssembly.dll");
ChoiceMenu(game_handle, game_PID, base_address);
}
就是获取游戏的模块基址,怎么获取的呢
DWORD GetProcessIDByName(const wchar_t* process_gamename)
{
//创建系统快照
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(snapshot == INVALID_HANDLE_VALUE)
{
return 0;
}
//初始化进程结构体
PROCESSENTRY32 process_entry = {0};
process_entry.dwSize = sizeof(PROCESSENTRY32);
//获取第一个进程,这里意味着告诉系统“从这个快照的开头开始遍历”
if(!Process32First(snapshot, &process_entry))
{
CloseHandle(snapshot);
return 0;
}
//遍历所有进程
do
{
if(wcscmp(process_entry.szExeFile, process_gamename) == 0)
{
CloseHandle(snapshot);
return process_entry.th32ProcessID;
}
} while (Process32Next(snapshot, &process_entry));
CloseHandle(snapshot);
return 0;
}
这是根据给定的进程名查找并返回该进程的 ID(PID),这算是一种模板了,还有其它也可以实现,感兴趣的可以自行去了解
游戏进程我们拿到了,先前也说过了,游戏的运算逻辑绝大部分是在DLL里实现的,因此我们需要获取dll的模块基址,并非Nobody.exe的基址
LPVOID GetModuleBaseAddress(DWORD processID, const wchar_t* moduleName)
{
LPVOID lpBaseAddress = nullptr;
//打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if(hProcess != NULL)
{
HMODULE hmod[MAX_PATH];
DWORD cbNeeded;
//枚举进程内的模块
if (EnumProcessModules(hProcess, hmod, sizeof(hmod), &cbNeeded))
{
//计算模块数量
DWORD moduleCount = cbNeeded / sizeof(HMODULE);
for (DWORD i = 0; i < moduleCount; i++)
{
wchar_t szModuleName[MAX_PATH];
//遍历并匹配名称
if (GetModuleBaseName(hProcess, hmod[i], szModuleName, sizeof(szModuleName) / sizeof(wchar_t)))
{
if (wcscmp(szModuleName, moduleName) == 0)
{
lpBaseAddress = hmod[i];
break;
}
}
}
}
CloseHandle(hProcess);
}
return lpBaseAddress;
}
这是获取模块基址的方法,也算是一种模板了,与上面的函数搭配起来就是查询基址的双子星,嘿嘿,当然也有其它方法,可以自行了解
然后进入我们的选择菜单
void ChoiceMenu(HANDLE game_handle, DWORD game_PID, LPVOID base_address)
{
DWORD choice;
printf("*********************************\n");
printf("*\t欢迎游玩大多数游戏\t*\n");
printf("*********************************\n");
printf("* 模块基址: 0x%" PRIxPTR "\t*\n", base_address);
printf("* 请选择要修改的数值类型:\t*\n");
printf("* 0.退出修改\t\t\t*\n");
printf("* 1.金钱\t\t\t*\n");
printf("* 2.饱腹度\t\t\t*\n");
printf("* 3.情绪值\t\t\t*\n");
printf("* 4.心态\t\t\t*\n");
printf("* 5.衣物整洁度\t\t\t*\n");
printf("* 6.技能点\t\t\t*\n");
//可后续添加新的属性修改
printf("*********************************\n");
do
{
printf("请选择要修改的数值:\n");
scanf_s("%u", &choice);
switch (choice)
{
case 1:
{
FLOAT num;
printf("请输入金钱数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Money, num);
printf("金钱数值已修改为: %.2f\n", num);
break;
}
case 2:
{
FLOAT num;
printf("请输入饱腹度数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Food, num);
printf("饱腹度数值已修改为: %f\n", num);
break;
}
case 3:
{
FLOAT num;
printf("请输入情绪值数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, emotion, num);
printf("情绪值数值已修改为: %f\n", num);
break;
}
case 4:
{
FLOAT num;
printf("请输入心态数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Mindset, num);
printf("心态数值已修改为: %f\n", num);
break;
}
case 5:
{
CHAR tmp;
static LPVOID hook_addr;
printf("请选择 a.无限整洁度 or b.取消无限整洁度\n");
scanf_s(" %c", &tmp, 1);
if (tmp == 'a')
{
hook_addr = HookGetClothClean(game_handle, base_address);
if (hook_addr != NULL)
{
printf("已成功安装衣物整洁度无限的Hook\n");
}
else
{
printf("安装衣物整洁度无限的Hook失败\n");
}
}
else if (tmp == 'b')
{
if (UnHook(game_handle, hook_addr, Clothingclean))
{
printf("已成功取消衣物整洁度无限的Hook\n");
}
else
{
printf("取消衣物整洁度无限的Hook失败\n");
}
}
else
{
printf("无效的选择\n");
}
break;
}
case 6:
{
UINT num;
CHAR tmp;
static LPVOID hook_addr;
printf("请选择 a.编辑技能点 or b.无限技能点 or c.取消无限技能点\n");
scanf_s(" %c", &tmp,1);
if (tmp == 'a')
{
printf("请输入技能点数值: ");
scanf_s("%u", &num);
SetGameValue(game_handle, base_address, SkillPoint, num);
printf("技能点数值已修改为: %u\n", num);
break;
}
else if (tmp == 'b')
{
hook_addr = HookGetSkillPiont(game_handle, base_address);
if (hook_addr != NULL)
{
printf("已成功安装技能点无限的Hook\n");
}
else
{
printf("安装技能点无限的Hook失败\n");
}
break;
}
else if (tmp == 'c')
{
if (UnHook(game_handle, hook_addr, SkillPoint))
{
printf("已成功取消技能点无限的Hook\n");
}
else
{
printf("取消技能点无限的Hook失败\n");
}
break;
}
else
{
printf("无效的选择\n");
break;
}
}
//可后续添加新的属性修改
}
} while (choice);
}
这里感觉没什么说的,主要是选择菜单的搭建以及修改操作的逻辑判断与执行
这里先介绍一下常量和一些定义
typedef enum Type{
Money, Food, emotion, Mindset, Clothingclean, SkillPoint, TypeCount
}ValueType;
typedef struct {
DWORD* offset;
size_t offsetsize;
size_t valuesize;
const char* name;
} ValueInfo;
//各属性偏移
static DWORD money_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x18 };
static DWORD food_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x20 };
static DWORD emotion_offset[] = { 0xB8, 0, 0xB0, 0x30, 0x10 };
static DWORD Mindset_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x40 };
static DWORD Clothingclean_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x4C };
static DWORD skillpoint_offset[] = { 0xB8, 0, 0xB0, 0x28, 0xB0};
//技能点数值地址偏移和Hook代码
static UINT64 get_skillpoint_offset = 0x377CD0;
static DWORD Original_get_skillpointCode[6] = { 0x40, 0x53, 0x48, 0x83, 0xEC, 0x40 };
static DWORD Hook_get_skillpointCode[6] = { 0xB8, 0, 0, 0, 0, 0xC3 };
static ValueInfo value_infos[TypeCount] = {
{ money_offset, sizeof(money_offset) / sizeof(DWORD), sizeof(int), "Money" },
{ food_offset, sizeof(food_offset) / sizeof(DWORD), sizeof(int), "Food" },
{ emotion_offset, sizeof(emotion_offset) / sizeof(DWORD), sizeof(int), "Emotion" },
{ Mindset_offset, sizeof(Mindset_offset) / sizeof(DWORD), sizeof(int), "Mindset" },
{ Clothingclean_offset, sizeof(Clothingclean_offset) / sizeof(DWORD), sizeof(int), "Clothingclean" },
{ skillpoint_offset, sizeof(skillpoint_offset) / sizeof(DWORD), sizeof(int), "SkillPoint" }
};
这里的通用属性,我的想法是写个结构体来存储,方便后续的通用函数的传参,不用特定的去为不特别属性重新定义
这里感觉通过名称很容易辨识出来,就不多说
然后就是SetGameValue函数,顾名思义--设置游戏数值
template<typename T>
void SetGameValue(HANDLE handle, LPVOID baseaddr, UINT Type, T EditValue)
{
LPVOID value_address = (LPVOID)((uintptr_t)baseaddr + 0x29b9d78); //示例偏移地址
switch (Type)
{
case Money:
ReadProcessMemory(handle, value_address, &value_address , sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Money);
printf("金钱数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case Food:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Food);
printf("饱腹度数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case emotion:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, emotion);
printf("情绪值数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case Mindset:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Mindset);
printf("心态数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case SkillPoint:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, SkillPoint);
printf("技能点数值地址: 0x%" PRIxPTR "\n", value_address);
EditSkillPoint(handle, value_address, (UINT)EditValue);
break;
}
}
这里通过枚举,来定向选择想要修改的数值,方法就是通过模块基址,依据提前写好的偏移,一步一步正确的地址,然后通过API去修改该地址的数据,从而达到修改游戏数据的操作
函数的参数我用了模板,因为有不同类型的参数会传进来,后续的属性也不一定是同类型,所以选择用模板更通用
注意:偏移并不是单纯的相加,而是一个指针链(GPT这么说的),通俗点讲就是俄罗斯套娃,一个地址套一个,所以应该是该地址指向的值去加上偏移,然后指向数据的直接地址,还不懂的话,那就看代码
uintptr_t GetValueAddress(HANDLE hprocess, LPVOID baseaddr, ValueType type)
{
if (type > TypeCount) return 0;
ValueInfo* desc = &value_infos[type];
return DerefPointerChain(hprocess, baseaddr, desc->offset, desc->offsetsize);
}
uintptr_t DerefPointerChain(HANDLE hprocess, LPVOID baseaddr, DWORD offset[], size_t offsetCount)
{
uintptr_t currentAddress = (uintptr_t)baseaddr;
for (size_t i = 0; i < offsetCount; i++)
{
currentAddress += offset[i];
if (i == offsetCount - 1)
{
return currentAddress;
}
if (!ReadProcessMemory(hprocess, (LPCVOID)currentAddress, ¤tAddress, sizeof(currentAddress), NULL))
{
return 0;
}
}
return 0;
}
GetValueAddress函数算是个过渡函数,整理下一个函数需要的参数,主要是DerefPointerChain函数需要的参数有点多
从代码来看其实也没有多难,无非就是循环一下拼指针链而已
接下来就是编辑技能点了(吐个槽:我就纳闷了,一个单机游戏为啥数值还要加密,真就不明白了)
这个技能点呢我刚开始改的时候是挺迷茫的,无从下手,但是看到风大大的修改器却可以做到,我就纳闷了,我又回到CE,认真研究起来看到玩家属性里有个_talenPoints
看到这个类型是MemoryCheck.MemoryInt,感觉这里面藏着加密的算法,然后就去看看,在搜索栏里输入,选择MemoryCheck.MemoryInt,可以看到
在询问了GPT后,他告诉我GetDecrypted里或许有我想找的算法,
这两个代码让我警觉起来,原因是在之前的尝试中_talenPoints的值也是高位与低位相异或,所以我询问GPT,他也说这两行代码比较可疑,于是这个rcx的值怎么确定呢
我尝试下断点,结果没有停下来,我突然想起_talenPoints的地址就是rcx吗,不然这个怎么高低位去异或呢,想到着我就去验证,在CE中去改变值,然后通过异或来得到想要的值,发现真的可以,哈哈,总算被我逮到了
接下来就是代码的演示了
void EditSkillPoint(HANDLE hprocess, LPVOID SkillPointAddr, UINT EditNum)
{
UINT64 OriginalValue = 0;
DWORD OriHighValue;
DWORD OriLowValue;
ReadProcessMemory(hprocess, (LPCVOID)SkillPointAddr, &OriginalValue, sizeof(OriginalValue), NULL);
if(OriginalValue == 0)
{
printf("未找到技能点数值地址\n");
return;
}
OriHighValue = (DWORD)(OriginalValue >> 32);
OriLowValue = (DWORD)(OriginalValue & 0xFFFFFFFF);
//构造自己想要的数值
OriLowValue = OriHighValue ^ EditNum;
OriginalValue = ((UINT64)OriHighValue << 32) | OriLowValue;
//修改内存保护以允许写入
DWORD oldProtect;
VirtualProtectEx(hprocess, (LPVOID)SkillPointAddr, sizeof(OriginalValue), PAGE_EXECUTE_READWRITE, &oldProtect);
//写入修改的值
WriteProcessMemory(hprocess, (LPVOID)SkillPointAddr, &OriginalValue, sizeof(OriginalValue), NULL);
//恢复原来的内存保护
VirtualProtectEx(hprocess, (LPVOID)SkillPointAddr, sizeof(OriginalValue), oldProtect, &oldProtect);
}
他的偏移可以在那界面看到,是0xB0
后面就是构造自己想要的值,然后通过异或来编辑成自己修改的数值
然后是两个hook
一个技能点一个衣物整洁度,我感觉实现的代码都大差不差,主要是hook那个地址,这里我就简单说一下
技能点的hook的是
这个直接修改获取的结果,实现无限的功能
整洁度的首先把第一层的改写地址找到,应该是get_Clean函数,然后进入到汇编界面,一直拉到函数最后,发现有个函数
在跟踪到这个函数,这个函数就是管理衣物整洁的函数,就可以hook了
注意:这个hook的是当前装备的衣服,不包含放在背包的衣服
这里贴上代码
//hook衣物整洁度
LPVOID HookGetClothClean(HANDLE hprocess, LPVOID base_address)
{
static UINT64 getclothclean_offset = 0x382250;
LPVOID hook_addr = (LPVOID)((uintptr_t)base_address + getclothclean_offset);
BYTE hook_code[] = {
0xFF, 0x31, // push [rcx]
0xC7, 0x01, 0x00, 0x00, 0xC8, 0x42, // mov dword ptr [rcx], 0x42C80000 (100.0f的IEEE 754表示) + ret
0xF3, 0x0F, 0x10, 0x01, // movss xmm0, [rcx]
0x8F, 0x01, // pop [rcx]
0xC3 // ret
};
BYTE original_code[sizeof(hook_code)] = {0};
if (!ReadProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
return 0;
}
if (!WriteProcessMemory(hprocess, hook_addr, hook_code, sizeof(hook_code), NULL))
{
printf("写入Hook代码失败\n");
return NULL;
}
return hook_addr;
}
//hook技能点
LPVOID HookGetSkillPiont(HANDLE hprocess, LPVOID base_address)
{
static UINT64 get_skillpoint_offset = 0x377CD0;
LPVOID hook_addr = (LPVOID)((uintptr_t)base_address + get_skillpoint_offset);
BYTE original_code[6] = {0}, hook_code[6] = {0xB8, 0x37, 0x25, 0x00, 0x00, 0xC3};
if(!ReadProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
return 0;
}
if (!WriteProcessMemory(hprocess, hook_addr, hook_code, sizeof(hook_code), NULL))
{
printf("写入Hook代码失败\n");
return NULL;
}
return hook_addr;
}
//取消hook
BOOL UnHook(HANDLE hprocess, LPVOID hook_addr, UINT Type)
{
if (Type == SkillPoint)
{
BYTE original_code[6] = { 0x40, 0x53, 0x48, 0x83, 0xEC, 0x40 };
if (!WriteProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
printf("恢复原始代码失败\n");
return FALSE;
}
return TRUE;
}
else if(Type == Clothingclean)
{
BYTE original_code[] =
{
0x48,0x89,0x5C,0x24,0x08,
0x48,0x89,0x74,0x24,0x10,
0x57,0x48,0x83,0xEC,0x40
};
if (!WriteProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
printf("恢复原始代码失败\n");
return FALSE;
}
return TRUE;
}
else
{
printf("无效的数值类型\n");
return FALSE;
}
}
下面的就是取消钩子,没什么说的了,硬编码就是在改之前提前存储的
PS:这里的地址我也是按照偏移来弄得,但是我实验了好几次,发现偏移不会变,因此就直接写死偏移了,想要更稳妥一点看可以尝试AOB扫描。
至此,现阶段的修改器就介绍完毕,后续有新添加的属性我也会继续写在这篇帖子之后
附上完整代码
#include<stdio.h>
#include <iostream>
#include<windows.h>
#include <tlhelp32.h>
#include <string.h>
#include <shlwapi.h>
#include <psapi.h>
#include <inttypes.h>
typedef enum Type{
Money, Food, emotion, Mindset, Clothingclean, SkillPoint, TypeCount
}ValueType;
typedef struct {
DWORD* offset;
size_t offsetsize;
size_t valuesize;
const char* name;
} ValueInfo;
//各属性偏移
static DWORD money_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x18 };
static DWORD food_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x20 };
static DWORD emotion_offset[] = { 0xB8, 0, 0xB0, 0x30, 0x10 };
static DWORD Mindset_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x40 };
static DWORD Clothingclean_offset[] = { 0xB8, 0, 0xB0, 0x28, 0x4C };
static DWORD skillpoint_offset[] = { 0xB8, 0, 0xB0, 0x28, 0xB0};
//技能点数值地址偏移和Hook代码
static UINT64 get_skillpoint_offset = 0x377CD0;
static DWORD Original_get_skillpointCode[6] = { 0x40, 0x53, 0x48, 0x83, 0xEC, 0x40 };
static DWORD Hook_get_skillpointCode[6] = { 0xB8, 0, 0, 0, 0, 0xC3 };
static ValueInfo value_infos[TypeCount] = {
{ money_offset, sizeof(money_offset) / sizeof(DWORD), sizeof(int), "Money" },
{ food_offset, sizeof(food_offset) / sizeof(DWORD), sizeof(int), "Food" },
{ emotion_offset, sizeof(emotion_offset) / sizeof(DWORD), sizeof(int), "Emotion" },
{ Mindset_offset, sizeof(Mindset_offset) / sizeof(DWORD), sizeof(int), "Mindset" },
{ Clothingclean_offset, sizeof(Clothingclean_offset) / sizeof(DWORD), sizeof(int), "Clothingclean" },
{ skillpoint_offset, sizeof(skillpoint_offset) / sizeof(DWORD), sizeof(int), "SkillPoint" }
};
uintptr_t DerefPointerChain(HANDLE hprocess, LPVOID baseaddr, DWORD offset[], size_t offsetCount)
{
uintptr_t currentAddress = (uintptr_t)baseaddr;
for (size_t i = 0; i < offsetCount; i++)
{
currentAddress += offset[i];
if (i == offsetCount - 1)
{
return currentAddress;
}
if (!ReadProcessMemory(hprocess, (LPCVOID)currentAddress, ¤tAddress, sizeof(currentAddress), NULL))
{
return 0;
}
}
return 0;
}
uintptr_t GetValueAddress(HANDLE hprocess, LPVOID baseaddr, ValueType type)
{
if (type > TypeCount) return 0;
ValueInfo* desc = &value_infos[type];
return DerefPointerChain(hprocess, baseaddr, desc->offset, desc->offsetsize);
}
DWORD GetProcessIDByName(const wchar_t* process_gamename)
{
//创建系统快照
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(snapshot == INVALID_HANDLE_VALUE)
{
return 0;
}
//初始化进程结构体
PROCESSENTRY32 process_entry = {0};
process_entry.dwSize = sizeof(PROCESSENTRY32);
//获取第一个进程,这里意味着告诉系统“从这个快照的开头开始遍历”
if(!Process32First(snapshot, &process_entry))
{
CloseHandle(snapshot);
return 0;
}
//遍历所有进程
do
{
if(wcscmp(process_entry.szExeFile, process_gamename) == 0)
{
CloseHandle(snapshot);
return process_entry.th32ProcessID;
}
} while (Process32Next(snapshot, &process_entry));
CloseHandle(snapshot);
return 0;
}
void EditSkillPoint(HANDLE hprocess, LPVOID SkillPointAddr, UINT EditNum)
{
UINT64 OriginalValue = 0;
DWORD OriHighValue;
DWORD OriLowValue;
ReadProcessMemory(hprocess, (LPCVOID)SkillPointAddr, &OriginalValue, sizeof(OriginalValue), NULL);
if(OriginalValue == 0)
{
printf("未找到技能点数值地址\n");
return;
}
OriHighValue = (DWORD)(OriginalValue >> 32);
OriLowValue = (DWORD)(OriginalValue & 0xFFFFFFFF);
//构造自己想要的数值
OriLowValue = OriHighValue ^ EditNum;
OriginalValue = ((UINT64)OriHighValue << 32) | OriLowValue;
//修改内存保护以允许写入
DWORD oldProtect;
VirtualProtectEx(hprocess, (LPVOID)SkillPointAddr, sizeof(OriginalValue), PAGE_EXECUTE_READWRITE, &oldProtect);
//写入shellcode
WriteProcessMemory(hprocess, (LPVOID)SkillPointAddr, &OriginalValue, sizeof(OriginalValue), NULL);
//恢复原来的内存保护
VirtualProtectEx(hprocess, (LPVOID)SkillPointAddr, sizeof(OriginalValue), oldProtect, &oldProtect);
}
LPVOID HookGetClothClean(HANDLE hprocess, LPVOID base_address)
{
static UINT64 getclothclean_offset = 0x382250;
LPVOID hook_addr = (LPVOID)((uintptr_t)base_address + getclothclean_offset);
BYTE hook_code[] = {
0xFF, 0x31, // push [rcx]
0xC7, 0x01, 0x00, 0x00, 0xC8, 0x42, // mov dword ptr [rcx], 0x42C80000 (100.0f的IEEE 754表示) + ret
0xF3, 0x0F, 0x10, 0x01, // movss xmm0, [rcx]
0x8F, 0x01, // pop [rcx]
0xC3 // ret
};
BYTE original_code[sizeof(hook_code)] = {0};
if (!ReadProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
return 0;
}
if (!WriteProcessMemory(hprocess, hook_addr, hook_code, sizeof(hook_code), NULL))
{
printf("写入Hook代码失败\n");
return NULL;
}
return hook_addr;
}
LPVOID HookGetSkillPiont(HANDLE hprocess, LPVOID base_address)
{
static UINT64 get_skillpoint_offset = 0x377CD0;
LPVOID hook_addr = (LPVOID)((uintptr_t)base_address + get_skillpoint_offset);
BYTE original_code[6] = {0}, hook_code[6] = {0xB8, 0x37, 0x25, 0x00, 0x00, 0xC3};
if(!ReadProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
return 0;
}
if (!WriteProcessMemory(hprocess, hook_addr, hook_code, sizeof(hook_code), NULL))
{
printf("写入Hook代码失败\n");
return NULL;
}
return hook_addr;
}
BOOL UnHook(HANDLE hprocess, LPVOID hook_addr, UINT Type)
{
if (Type == SkillPoint)
{
BYTE original_code[6] = { 0x40, 0x53, 0x48, 0x83, 0xEC, 0x40 };
if (!WriteProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
printf("恢复原始代码失败\n");
return FALSE;
}
return TRUE;
}
else if(Type == Clothingclean)
{
BYTE original_code[] =
{
0x48,0x89,0x5C,0x24,0x08,
0x48,0x89,0x74,0x24,0x10,
0x57,0x48,0x83,0xEC,0x40
};
if (!WriteProcessMemory(hprocess, hook_addr, original_code, sizeof(original_code), NULL))
{
printf("恢复原始代码失败\n");
return FALSE;
}
return TRUE;
}
else
{
printf("无效的数值类型\n");
return FALSE;
}
}
LPVOID GetModuleBaseAddress(DWORD processID, const wchar_t* moduleName)
{
LPVOID lpBaseAddress = nullptr;
//打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if(hProcess != NULL)
{
HMODULE hmod[MAX_PATH];
DWORD cbNeeded;
//枚举进程内的模块
if (EnumProcessModules(hProcess, hmod, sizeof(hmod), &cbNeeded))
{
//计算模块数量
DWORD moduleCount = cbNeeded / sizeof(HMODULE);
for (DWORD i = 0; i < moduleCount; i++)
{
wchar_t szModuleName[MAX_PATH];
//遍历并匹配名称
if (GetModuleBaseName(hProcess, hmod[i], szModuleName, sizeof(szModuleName) / sizeof(wchar_t)))
{
if (wcscmp(szModuleName, moduleName) == 0)
{
lpBaseAddress = hmod[i];
break;
}
}
}
}
CloseHandle(hProcess);
}
return lpBaseAddress;
}
template<typename T>
void SetGameValue(HANDLE handle, LPVOID baseaddr, UINT Type, T EditValue)
{
LPVOID value_address = (LPVOID)((uintptr_t)baseaddr + 0x29b9d78); //示例偏移地址
switch (Type)
{
case Money:
ReadProcessMemory(handle, value_address, &value_address , sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Money);
printf("金钱数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case Food:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Food);
printf("饱腹度数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case emotion:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, emotion);
printf("情绪值数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case Mindset:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, Mindset);
printf("心态数值地址: 0x%" PRIxPTR "\n", value_address);
WriteProcessMemory(handle, value_address, &EditValue, sizeof(EditValue), NULL);
break;
case SkillPoint:
ReadProcessMemory(handle, value_address, &value_address, sizeof(value_address), NULL);
value_address = (LPVOID)GetValueAddress(handle, value_address, SkillPoint);
printf("技能点数值地址: 0x%" PRIxPTR "\n", value_address);
EditSkillPoint(handle, value_address, (UINT)EditValue);
break;
}
}
void ChoiceMenu(HANDLE game_handle, DWORD game_PID, LPVOID base_address)
{
DWORD choice;
printf("*********************************\n");
printf("*\t欢迎游玩大多数游戏\t*\n");
printf("*********************************\n");
printf("* 模块基址: 0x%" PRIxPTR "\t*\n", base_address);
printf("* 请选择要修改的数值类型:\t*\n");
printf("* 0.退出修改\t\t\t*\n");
printf("* 1.金钱\t\t\t*\n");
printf("* 2.饱腹度\t\t\t*\n");
printf("* 3.情绪值\t\t\t*\n");
printf("* 4.心态\t\t\t*\n");
printf("* 5.衣物整洁度\t\t\t*\n");
printf("* 6.技能点\t\t\t*\n");
printf("*********************************\n");
do
{
printf("请选择要修改的数值:\n");
scanf_s("%u", &choice);
switch (choice)
{
case 1:
{
FLOAT num;
printf("请输入金钱数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Money, num);
printf("金钱数值已修改为: %.2f\n", num);
break;
}
case 2:
{
FLOAT num;
printf("请输入饱腹度数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Food, num);
printf("饱腹度数值已修改为: %f\n", num);
break;
}
case 3:
{
FLOAT num;
printf("请输入情绪值数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, emotion, num);
printf("情绪值数值已修改为: %f\n", num);
break;
}
case 4:
{
FLOAT num;
printf("请输入心态数值: ");
scanf_s("%f", &num);
SetGameValue(game_handle, base_address, Mindset, num);
printf("心态数值已修改为: %f\n", num);
break;
}
case 5:
{
CHAR tmp;
static LPVOID hook_addr;
printf("请选择 a.无限整洁度 or b.取消无限整洁度\n");
scanf_s(" %c", &tmp, 1);
if (tmp == 'a')
{
hook_addr = HookGetClothClean(game_handle, base_address);
if (hook_addr != NULL)
{
printf("已成功安装衣物整洁度无限的Hook\n");
}
else
{
printf("安装衣物整洁度无限的Hook失败\n");
}
}
else if (tmp == 'b')
{
if (UnHook(game_handle, hook_addr, Clothingclean))
{
printf("已成功取消衣物整洁度无限的Hook\n");
}
else
{
printf("取消衣物整洁度无限的Hook失败\n");
}
}
else
{
printf("无效的选择\n");
}
break;
}
case 6:
{
UINT num;
CHAR tmp;
static LPVOID hook_addr;
printf("请选择 a.编辑技能点 or b.无限技能点 or c.取消无限技能点\n");
scanf_s(" %c", &tmp,1);
if (tmp == 'a')
{
printf("请输入技能点数值: ");
scanf_s("%u", &num);
SetGameValue(game_handle, base_address, SkillPoint, num);
printf("技能点数值已修改为: %u\n", num);
break;
}
else if (tmp == 'b')
{
hook_addr = HookGetSkillPiont(game_handle, base_address);
if (hook_addr != NULL)
{
printf("已成功安装技能点无限的Hook\n");
}
else
{
printf("安装技能点无限的Hook失败\n");
}
break;
}
else if (tmp == 'c')
{
if (UnHook(game_handle, hook_addr, SkillPoint))
{
printf("已成功取消技能点无限的Hook\n");
}
else
{
printf("取消技能点无限的Hook失败\n");
}
break;
}
else
{
printf("无效的选择\n");
break;
}
}
}
} while (choice);
}
int main()
{
//读取目标游戏进程ID
DWORD game_PID = GetProcessIDByName(L"Nobody.exe");
if (game_PID == 0)
{
printf("未找到目标游戏进程\n");
return 1;
}
HANDLE game_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, game_PID);
//游戏模块基址
LPVOID base_address = GetModuleBaseAddress(game_PID, L"GameAssembly.dll");
ChoiceMenu(game_handle, game_PID, base_address);
}