V1
这是一个AssualtCube的简单修改器,实现了锁血,锁弹药,无后座等功能。这里只呈现最后的脚本代码,不包含过程中使用Cheat Engine获取偏移量的过程。
编写思路
- 编写一个proc,用来获得目标进程的ProcessId,模块地址,解析多级指针功能
- 调用proc的函数,通过CE获取偏移量,进行内存数据修改。
输入进程名称,返回进程号。
DWORD GetProcId(const wchar_t* procName) {
DWORD procId = 0; // processid 需要是dowrd
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(procEntry);
if (Process32First(hSnap, &procEntry)) {
do {
if (!_wcsicmp(procEntry.szExeFile, procName)) {
procId = procEntry.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &procEntry));
}
}
CloseHandle(hSnap);
return 0;
}
下边是api解析
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
拍摄指定进程的快照,以及这些进程使用的堆、模块和线程
第一个参数 TH32CS_SNAPPROCESS 表示获取进程快照
第二个参数置为 0 表示获取所有进程
PROCESSENTRY32 用来保存进程信息
使用之前必须初始化dwSize 否则后边遍历的时候可能会出错,无法获取
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
Process32Next
这一对API形式上差不多,具体就是遍历刚才获取的HANDLE中的进程,装载到 LPPROCESSENTRY32 中。
相互配合,首先使用first获取第一个,之后不断调用next进行遍历
_wcsicmp
wide char string ignorecases cmp
分解之后就是比较wchar,并且大小写不敏感。
接下来是获取模块的地址,这里模块可以理解为是dll或者exe,输入是进程id和模块名称,返回模块的地址。
uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName) {
uintptr_t modBaseAddr = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId);
if (hSnap != INVALID_HANDLE_VALUE) {
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(modEntry);
if (Module32First(hSnap, &modEntry)) {
do {
if (!_wcsicmp(modEntry.szModule, modName))
{
modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
break;
}
}while(Module32Next(hSnap, &modEntry));
}
}
CloseHandle(hSnap);
return modBaseAddr;
}
内容解析就不解析了,基本跟上边的原理差不多,一个是对进程进行遍历,一个是对模块进行遍历,最后获取的是模块的地址。这里有一个指针转化注意一下。
modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
最后最后就是解析多级指针,输入进程句柄,模块基址地址,多级指针偏移量,返回解析之后最后一级的地址。
uintptr_t FindDMAAddy(HANDLE hProc, uintptr_t ptr, std::vector<unsigned int> offsets)
{
uintptr_t addr = ptr;
for (unsigned int i = 0; i < offsets.size(); ++i)
{
ReadProcessMemory(hProc, (BYTE*)addr, &addr, sizeof(addr), 0);
addr += offsets[i];
}
return addr;
}
BOOL ReadProcessMemory(
HANDLE hProcess, // 目标进程句柄
LPCVOID lpBaseAddress,// 目标进程的内存地址
LPVOID lpBuffer, // 用于存储读取数据的缓冲区
SIZE_T nSize, // 读取的字节数
SIZE_T *lpNumberOfBytesRead // 可选,返回实际读取的字节数
);
调用函数,实现数据修改功能
#include <Windows.h>
#include <iostream>
#include "proc.h"
#include <vector>
int main() {
DWORD procId = GetProcId(L"ac_client.exe");
uintptr_t moduleBase = GetModuleBaseAddress(procId, L"ac_client.exe");
HANDLE hProcess = 0;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, procId);
uintptr_t dynamicPtrBaseAddr = moduleBase + 0x10f4f4;
std::cout << "dynamicPtrBaseAddr = " << "0x" << std::hex << dynamicPtrBaseAddr << std::endl;
std::vector<unsigned int> ammoOffsets = { 0x374, 0x14, 0x0 };
uintptr_t ammoAddr = FindDMAAddy(hProcess, dynamicPtrBaseAddr, ammoOffsets);
std::cout << "ammoAddr = " << "0x" << std::hex << ammoAddr << std::endl;
int ammoValue = 0;
ReadProcessMemory(hProcess, (BYTE*)ammoAddr, &ammoValue, sizeof(ammoValue), 0);
std::cout << "Current Ammo = " << std::dec << ammoValue << std::endl;
int newAmmo = 1337;
WriteProcessMemory(hProcess, (BYTE*)ammoAddr, &newAmmo, sizeof(ammoValue), 0);
ReadProcessMemory(hProcess, (BYTE*)ammoAddr, &newAmmo, sizeof(newAmmo), 0);
std::cout << "New Ammo = " << std::dec << newAmmo << std::endl;
getchar();
return 0;
}
API解析
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);
参数分别
获取句柄的权限
该进程创建的进程是否继承这个权限
进程id
之前的hack更加类似一个POC,能够一次性修改数据,而不能手动控制是否进行多次修改。现在进行更新,加上开关功能,并且加上了一个消除后坐力的功能。
V2
查找后坐力在这里不是重点,简单介绍一下思路。
这里发现的recoil的思路大概就是,找到current weapon的指针,指向的是一系列武器的结构体,在里面每个结构体的靠近结尾处,有记录后坐力。这里视频教的很模糊,并没有明确教如何在好多类似数据中找到后坐力数据。之后使用cheat engine,找到什么访问了这个数据。查看cheat engine中的内存布局,在访问位置打断点→执行到函数返回。记录一下执行到这里的地址,在IDA中反编译一下,能看出来是计算散布,镜头抖动,后座力的函数。所以思路就是将对于这个函数的栈准备和call都用nop patch掉,之后就好了
#include "stdafx.h"
#include <Windows.h>
#include "mem.h"
#include "proc.h"
#include <iostream>
int main()
{
HANDLE hProcess = 0;
uintptr_t moduleBase = 0, LocalPlayerPtr = 0, healthAddr = 0;
bool bHealth = false, bAmmo = false, bRecoil = false;
const int newValue = 1337;
DWORD procID = GetProcId(L"ac_client.exe");
if (procID)
{
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, procID);
moduleBase = GetModuleBaseAddress(procID, L"ac_client.exe");
LocalPlayerPtr = moduleBase + 0x10f4f4;
healthAddr = FindDMAAddy(hProcess, LocalPlayerPtr, { 0xF8 });
}
else
{
std::cout << "Process Not Found. Exiting. " << std::endl;
return 0;
}
DWORD dwExit = 0;
while (GetExitCodeProcess(hProcess, &dwExit) && dwExit == STILL_ACTIVE)
{
if (GetAsyncKeyState(VK_NUMPAD1) & 1)
{
bHealth = !bHealth;
}
if (GetAsyncKeyState(VK_NUMPAD2) & 1)
{
bAmmo = !bAmmo;
if (bAmmo)
{
mem::PatchEx((BYTE*)(moduleBase + 0x637e9), (BYTE*)"\xFF\x06", 2, hProcess);
}
else
{
mem::PatchEx((BYTE*)(moduleBase + 0x637e9), (BYTE*)"\xFF\x0E", 2, hProcess);
}
}
if (GetAsyncKeyState(VK_NUMPAD3) & 1)
{
bRecoil = !bRecoil;
if (bRecoil)
{
mem::NopEx((BYTE*)(moduleBase + 0x63786), 10, hProcess);
}
else
{
mem::PatchEx((BYTE*)(moduleBase + 0x63786), (BYTE*)"\x50\x8D\x4C\x24\x1C\x51\x8B\xCE\xFF\xD2", 10, hProcess);
}
}
if (GetAsyncKeyState(VK_INSERT) & 1)
{
return 0;
}
if (bHealth)
{
mem::PatchEx((BYTE*)healthAddr, (BYTE*)&newValue, sizeof(newValue), hProcess);
}
Sleep(10);
}
return 0;
}
GetExitCodeProcess
BOOL GetExitCodeProcess(
[in] HANDLE hProcess,
[out] LPDWORD lpExitCode
);
望文生义一下就知道了,这个函数会获取进程的状态,在脚本中用来判断游戏进程是否还在运行。
GetAsyncKeyState
SHORT GetAsyncKeyState(
[in] int vKey
);
获取键盘上按键的状态,
返回值类型是 SHORT
,它是一个 16 位值。
- 高位 (MSB):表示按键是否被按住。如果按键被按下,则高位为 1;如果按键未按下,则高位为 0。
- 低位 (LSB):表示按键是否有变化(即是否刚刚被按下或松开)。如果低位为 1,表示按键有状态变化;如果低位为 0,表示按键的状态没有变化。
高位 | 低位 | |
---|---|---|
长按 | 1 | 0 |
没有按下 | 0 | 0 |
刚刚按下 | 1 | 1 |
刚刚抬起 | 0 | 1 |
解释
里面的偏移量是从cheat engine中获取的。
锁弹药数的功能实现有两个思路,第一个是将弹药所在的内存位置不断覆盖新的数据,另一种就是直接修改内存中的代码,将修改弹药的函数patch成nop,达到根本不去调用减少弹药函数的目的。
脚本中的弹药和后座就是通过这种方式实现的。
血量是通过直接写数据实现的。
剩下的内容都是挺简单的,主要学习一下循环和开关思路即可。