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,表示按键的状态没有变化。
高位低位
长按10
没有按下00
刚刚按下11
刚刚抬起01

解释

里面的偏移量是从cheat engine中获取的。

锁弹药数的功能实现有两个思路,第一个是将弹药所在的内存位置不断覆盖新的数据,另一种就是直接修改内存中的代码,将修改弹药的函数patch成nop,达到根本不去调用减少弹药函数的目的。

脚本中的弹药和后座就是通过这种方式实现的。

血量是通过直接写数据实现的。

剩下的内容都是挺简单的,主要学习一下循环和开关思路即可。