简介
以下教程是自我总结出的,我会很详细的进行编写,配详细的步骤和视频教程,本文章用到的工具会有 Cheat engine、x64dbg,本文章只用C语言编写代码,用单机游戏 CK竞技之王 进行辅助的制作,该游戏是CS进行魔改仿穿越火线,在此声明并不是穿越火线游戏,做这个教程目的只有一个让你们明白它的原理和搜法,在游戏中不管是网络游戏还是单机游戏,他们的共同点都是一样的,能够被本地修改达到开挂的目的(在此声明只是作为学习交流)。
基本了解
游戏它是程序,因为运行程序就会创建进程,通过进程就可以访问程序中的内存地址,通过地址可以访问到存储的数据,举个简单的例子:比如游戏开发者创建了个变量 float speed = 2.5; 来定义人物速度,那么就会在内存中开辟一个空间,然后会有一个内存地址像 0x12345 这样的(只是打比方),我们可以通过这个地址来访问到里面的数据(知道C语言指针的就很好理解),也就是 2.5,通过微软给的系统 API 接口 kernel32.dll 里的 ReadProcessMemory 函数就能办到,然后就是写内存数值了,可以通过 WriteProcessMemory 函数,要用到的API函数我都会写在下面的。
取窗口句柄
HWND FindWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName
);取窗口句柄因为 ReadProcessMemory,第一个参数 hProcess 要的是进程句柄(注意了 FindWindow 取的是窗口句柄而不是进程句柄哦)。
取进程线程
DWORD GetWindowThreadProcessId(
HWND hWnd,
LPDWORD lpdwProcessId
);通过 GetWindowThreadProcessId,来获取进程 PID 当然你直接获取进程 PID 就省去了找窗口。
取进程句柄
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);通过 OpenProcess,打开进程来取进程句柄,这样就拿到进程句柄了。
读内存数据
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);通过 ReadProcessMemory 来读进程内存数据。
写内存数据
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);通过 WriteProcessMemory 来写进程内存数据,以上是简单介绍让大家有个基本了解(注意以上只能对 32 程序进程操作)。
32位、64位内存读写
内核级进程内存读
NTSTATUS ZwWow64ReadVirtualMemory64(
HANDLE Process,
PVOID64 BaseAddress,
PVOID Buffer,
ULONG64 BufferSize,
PULONG64 NumberOfBytesRead
);内核级进程内存写
NTSTATUS ZwWow64WriteVirtualMemory64(
HANDLE Process,
PVOID64 BaseAddress,
PVOID Buffer,
ULONG64 BufferSize,
PULONG64 NumberOfBytesWritten
);内核级别的进程内存读写,可以用在32和64位程序上,微软不公开的但是可以用,这两个函数不属于动态链接库,感兴趣的可以自己去研究。

完整代码(读写子弹数量)
// 读写内存简单例子:
#include <Windows.h> // 包含头文件 Windows.h
#include <stdio.h>
int main()
{
DWORD processid; // 保存进程PID
HANDLE hProcess; // 保存进程句柄
HWND hwnd = FindWindow("Valve001", NULL); // 取窗口句柄
GetWindowThreadProcessId(hwnd, &processid); // 取进程PID
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processid); // 取进程句柄
if (hProcess == NULL)
{
printf("打开进程失败\n");
return 1;
}
int values; // 返回读取数据
SIZE_T bytesRead; // 存读取字节数
LPVOID Addresses = (LPVOID)0x7000FA4; // 临时地址读取
ReadProcessMemory(hProcess, Addresses, &values, sizeof(int), &bytesRead); // 读取内存地址数据
printf("子弹数量:%d\n", values);
values = 100;
SIZE_T bytesWritten;
WriteProcessMemory(hProcess, Addresses, &values, sizeof(int), &bytesWritten); // 写内存地址数据 100
CloseHandle(hProcess); // 关闭句柄
return 0;
}以上函数都在这个目录下:C:\Windows\System32\,这里面是存放的是 32 位动态链接库,上面的函数是 kernel32.dll 里面的,常用的 dll 我写在下面了,感兴趣的可以自己去看看。
user32.dll // 提供了用户界面方面的功能,包括窗口、菜单、鼠标、键盘等。
kernel32.dll // 包含了许多核心的操作系统功能,如进程和线程管理、内存管理、文件操作等。
advapi32.dll // 提供了高级的系统和安全功能,如访问控制、认证等。
gdi32.dll // 提供了图形设备接口相关的功能,包括绘图和字体操作。
shell32.dll // 提供了与 Windows shell 相关的功能,如文件和文件夹操作、图标处理等。
comdlg32.dll // 提供了用于打开和保存对话框等通用的常见对话框。人物自瞄(C语言)
#include <stdio.h>
#include <math.h>
#include <windows.h>
#define PI 3.1415926
float GetDistance(float x1, float y1, float x2, float y2)
{
float distance = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); // 计算二维距离
return distance / 20.0f;
}
int main()
{
DWORD processid;
HANDLE hProcess;
HWND hwnd = FindWindow("Valve001", NULL);
GetWindowThreadProcessId(hwnd, &processid);
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processid);
if (hProcess == NULL)
{
printf("打开进程失败\n");
return 1;
}
LPVOID myAddr[] = { // 临时地址
(LPVOID)0x0B466A44, // 自己x
(LPVOID)0x0B466A4C, // 自己y
(LPVOID)0x0B466A48, // 自己z
};
LPVOID emAddr[] = { // 临时地址
(LPVOID)0x0B466D68, // 敌人x
(LPVOID)0x0B466D70, // 敌人y
(LPVOID)0x0B466D6C, // 敌人z
};
LPVOID aimAddr[] = { // 临时地址
(LPVOID)0x02de10c8, // 准星 x
(LPVOID)0x02de10c4, // 准星 y
};
SIZE_T bytesRead;
float Aimvalues[2];
float myvalues[3];
float envalues[3];
float differ_x, differ_y, distan;
float AiMBot_x, AiMBot_y, differ;
while (TRUE)
{
for (int i = 0; i < 3; i++)
{
ReadProcessMemory(hProcess, aimAddr[i], &Aimvalues[i], sizeof(float), &bytesRead);
ReadProcessMemory(hProcess, myAddr[i], &myvalues[i], sizeof(float), &bytesRead);
ReadProcessMemory(hProcess, emAddr[i], &envalues[i], sizeof(float), &bytesRead);
}
differ_x = envalues[0] - myvalues[0]; // 计算敌我坐标差
differ_y = envalues[2] - myvalues[2];
distan = sqrt(pow(differ_x, 2) + pow(differ_y, 2));
differ = GetDistance(myvalues[0], myvalues[2], envalues[0], envalues[2]); // 计算二维距离
AiMBot_y = atan((myvalues[1] - envalues[1]) / distan) / PI * 180 + 3; // 计算y轴方向上的角度
if (differ_x > 0 && differ_y > 0) // 判断第一象限
{
AiMBot_x = atan(differ_y / differ_x) * 180 / PI; // 计算x轴方向上的角度
}
if (differ_x < 0 && differ_y > 0) // 判断第二象限
{
AiMBot_x = atan(differ_y / differ_x) * 180 / PI + 180;
}
if (differ_x < 0 && differ_y < 0) // 判断第三象限
{
AiMBot_x = atan(differ_y / differ_x) * 180 / PI + 180;
}
if (differ_x > 0 && differ_y < 0) // 判断第四象限
{
AiMBot_x = atan(differ_y / differ_x) * 180 / PI;
}
WriteProcessMemory(hProcess, aimAddr[0], &AiMBot_x, sizeof(float), &bytesRead); // 将计算出的敌人位置x写入准星x
WriteProcessMemory(hProcess, aimAddr[1], &AiMBot_y, sizeof(float), &bytesRead); // 将计算出的敌人位置y写入准星y
}
CloseHandle(hProcess);
return 0;
}自瞄代码详解
1.首先通过 Cheat Engine 获取人物在游戏中的 x,y,z 坐标地址,有了这些数据才能为后期做准备。

Tips:使用CE搜未知初始值浮点数,人物坐标是浮点数,人物往左移动 也就是 x 轴 那么就是减少了,往右走就是增加了,数轴的基本常识左减右增,来来回回过滤用二分排除法很快就能找到x坐标了。

Tips:一旦找到 x 坐标那么 y,z 就都找到了,x坐标的内存地址是 0x2458fa44 只要 0x2458fa44 + 4 就是 z 坐标 + 8 就是 y 坐标,这样就全部拿到了,可以看到我额外标出HP,因为这里是跟人物相关的数据一般都在附近,就像一个对象或者结构体里面的数据都会存放在一起的。

2.计算 x,y 轴方向的差值(水平与垂直距离),假设自己的位置是 (0, 0),敌人的位置是 (3, 5),那么 x 轴方向的差值是 3,y 轴方向的差值是 5。
√(3^2 + 5^2) = √34 ≈ 5.83 // 勾股定理水平距离:3(敌人的 x 坐标)- 0(自己 x 坐标) = 3
垂直距离:5(敌人的 y 坐标)- 0(自己 y 坐标) = 5
float differ_x = enemy_x - self_x
float differ_y = enemy_y - self_y3.计算两点之间的直线距离,可以使用勾股定理计算,本教程中我使用了欧氏距离计算。

d = √((x2 - x1)^2 + (y2 - y1)^2) // 二维欧氏距离// 计算二维欧氏距离
float GetDistance(float x1, float y1, float x2, float y2)
{
float distance = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
return distance / 20.0f;
}4.计算自己与敌人之间的角度(相对于 x 轴)用反正切函数来计算角度。
θ = arctan(5 / 3)AiMBot_y = atan((myvalues[1] - envalues[1]) / distan) / PI * 180 + 3; // 计算y轴方向上的角度5.使用 atan 函数计算 y 轴方向上的角度,作为左后自瞄的 y 角度。
if (differ_x > 0 && differ_y > 0) // 判断第一象限
{
AiMBot_x = atan(differ_y / differ_x) * 180 / PI; // 计算 x 轴方向上的角度
}
// 代码省略AiMBot_x = atan(differ_y / differ_x) * 180 / PI; // 计算x轴方向上的角度6.通过差值正负判断象限,用 atan 函数计算 x 轴方向上的角度,作为左后自瞄的 x 角度。
WriteProcessMemory(hProcess, aimAddr[0], &AiMBot_x, sizeof(float), &bytesRead); // 准星x写入计算出的敌人位置x
WriteProcessMemory(hProcess, aimAddr[1], &AiMBot_y, sizeof(float), &bytesRead); // 准星y写入计算出的敌人位置y7.获取游戏准星 x,y 移动坐标,最终配合 x 角度与 y 角度,用来移动准星实现自瞄效果。(以上方式是最原始的自瞄,用三角函数进行实现,只是单个人自瞄哦,多人只要遍历敌人数据x,y,z,加入到数组中通过距离判断优先级再自瞄,这样就完成了暂时不做比较懒。)
无限急速雷

思路很简单,先找雷数量右键看谁访问了这个地址,就可以追踪到访问它的代码段,这里面就会有很多对手雷的功能操作。(比如投掷距离,投掷速度,引爆时间。)

看到这个地方,有 #fire_in_the_hole 这里就是调用扔雷的声音,再往下就是扔雷的操作。

我分析过了,扔雷的速度就在这个反汇编代码,引用的这个地址附近,通过访问内存就能看到 + 4 的位置是 1.75 修改它扔雷速度就无冷却。
方框透视(C语言)
绘制方框透视的方式也就两种,内部绘制 和 外部绘制,内部就是嵌入到了游戏窗口内,本文章用的是外部绘制,也就是说创建了一个窗口,设置透明并覆盖在游戏窗口上方,读游戏中的数据计算出来再进行绘制。
我这里用的是 Windows API 进行绘制,会存在一点小问题,只是提供个思路给你们,在C语言也可以用 D3D9、SDL、Imgui 等等。
#include <Windows.h>
#include <tlhelp32.h>
// 根据进程ID和模块名获取模块句柄
HMODULE GetModuleHandleFromProcess(DWORD processId, const char *moduleName)
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processId);
MODULEENTRY32 moduleEntry;
moduleEntry.dwSize = sizeof(moduleEntry);
if (Module32First(hSnapshot, &moduleEntry))
{
do
{
if (stricmp(moduleEntry.szModule, moduleName) == 0)
{
CloseHandle(hSnapshot);
return moduleEntry.hModule;
}
} while (Module32Next(hSnapshot, &moduleEntry));
}
CloseHandle(hSnapshot);
return NULL;
}
// 窗口消息处理函数
LRESULT CALLBACK MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_SIZING:
return TRUE;
case WM_NCHITTEST:
return HTTRANSPARENT;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
// 绘制矩形
void DrawRectangle(HDC hdc, int x, int y, int width, int height)
{
RECT rect = {x, y, x + width, y + height};
HBRUSH hBrush = (HBRUSH)GetStockObject(NULL_BRUSH);
HPEN hPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0));
HGDIOBJ hOldPen = SelectObject(hdc, hPen);
HGDIOBJ hOldBrush = SelectObject(hdc, hBrush);
Rectangle(hdc, x, y, x + width, y + height);
SelectObject(hdc, hOldBrush);
SelectObject(hdc, hOldPen);
DeleteObject(hPen);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
// 获取进程ID和句柄
DWORD processid;
HANDLE hProcess;
HWND Gamehwnd = FindWindow(NULL, "Compete King");
GetWindowThreadProcessId(Gamehwnd, &processid);
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processid);
// 获取模块句柄
const char *moduleName = "amxmodx_mm.dll";
HMODULE hModule = GetModuleHandleFromProcess(processid, moduleName);
// 注册窗口类
WNDCLASSEX wc = {
sizeof(WNDCLASSEX), CS_CLASSDC,
MsgProc, 0L, 0L, GetModuleHandle(NULL),
NULL, NULL, NULL, NULL, TEXT("D3D9"), NULL};
RegisterClassEx(&wc);
// 创建窗口
RECT rects;
GetWindowRect(Gamehwnd, &rects);
int left = rects.left;
int top = rects.top;
int right = rects.right;
int bottom = rects.bottom;
int sWidth = right - left;
int sHeight = bottom - top;
HWND hWnd = CreateWindowEx(
WS_EX_LAYERED | WS_EX_TRANSPARENT, wc.lpszClassName, TEXT("D3D9"),
WS_POPUP,
left, top,
sWidth - 6, sHeight - 3,
NULL, NULL,
wc.hInstance, NULL);
SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
ShowWindow(hWnd, SW_SHOWDEFAULT);
UpdateWindow(hWnd);
MSG msg;
float MatData;
float Matarray[4][4];
HDC hdc = GetDC(hWnd);
ZeroMemory(&msg, sizeof(msg));
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// 获取游戏窗口大小
if (!GetWindowRect(Gamehwnd, &rects))
{
PostQuitMessage(0);
break;
}
int left = rects.left;
int top = rects.top;
int right = rects.right;
int bottom = rects.bottom;
int sWidth = right - left;
int sHeight = bottom - top;
SetWindowPos(hWnd, HWND_TOPMOST, left + 3, top, sWidth, sHeight, SWP_NOSIZE);
int initNumber = -4;
for (int i = 0; i < 4; i++) // 遍历存储 4 x 4 矩阵
{
for (int j = 0; j < 4; j++)
{
initNumber += 4;
LPVOID Addresses = (LPVOID)0x2C20100 + initNumber;
ReadProcessMemory(hProcess, Addresses, &MatData, sizeof(float), NULL);
Matarray[i][j] = MatData;
}
}
int GameWinWidth = 1030 / 2;
int GameWinHeight = 797 / 2;
float vorx, vory, vorz, vory2, FanHeight, FanWidth, row;
long long offset = 0xFFFFFFFFFFFFFCDC;
RECT clientRect;
GetClientRect(hWnd, &clientRect);
FillRect(hdc, &clientRect, (HBRUSH)GetStockObject(BLACK_BRUSH));
for (int enemy_num = 0; enemy_num < 2; enemy_num++) // 遍历敌人数组
{
offset += 0x324;
LPVOID baseAddress = (LPVOID)((DWORD_PTR)hModule + 0x001CFE08);
int baseaddr;
ReadProcessMemory(hProcess, baseAddress, &baseaddr, sizeof(int), NULL);
float enemyHP, enemyX, enemyY, enemyZ;
ReadProcessMemory(hProcess, (LPVOID)baseaddr + 0x1E0 + offset, &enemyHP, sizeof(float), NULL); // enemyHP
ReadProcessMemory(hProcess, (LPVOID)baseaddr + 0x88 + offset, &enemyX, sizeof(float), NULL); // enemyX
ReadProcessMemory(hProcess, (LPVOID)baseaddr + 0x8C + offset, &enemyY, sizeof(float), NULL); // enemyY
ReadProcessMemory(hProcess, (LPVOID)baseaddr + 0x90 + offset, &enemyZ, sizeof(float), NULL); // enemyZ
/* 三维图形的投影算法 */
vorz = Matarray[0][2] * enemyX + Matarray[1][2] * enemyY + Matarray[2][2] * enemyZ + Matarray[3][2];
row = 1 / vorz;
vorx = GameWinWidth + (Matarray[0][0] * enemyX + Matarray[1][0] * enemyY + Matarray[2][0] * enemyZ + Matarray[3][0]) * row * GameWinWidth;
vory = GameWinHeight - (Matarray[0][1] * enemyX + Matarray[1][1] * enemyY + Matarray[2][1] * (enemyZ - 50) + Matarray[3][1]) * row * GameWinHeight;
vory2 = GameWinHeight - (Matarray[0][1] * enemyX + Matarray[1][1] * enemyY + Matarray[2][1] * (enemyZ + 30) + Matarray[3][1]) * row * GameWinHeight;
if (vorz < 0)
{
continue;
}
FanHeight = vory - vory2;
FanWidth = FanHeight * 0.6;
DrawRectangle(hdc, vorx - FanWidth / 2, vory2, (int)FanWidth, (int)FanHeight); // 绘制方框矩形
}
ReleaseDC(hWnd, hdc);
}
}
UnregisterClass(wc.lpszClassName, wc.hInstance);
return (int)msg.wParam;
}gcc main.c -s -lgdi32 -ld3d9 -mwindows // 编译成 exe 并隐藏 DOS 窗口
方框透视详解
先理一下思路,要用到的功能有,通过窗口句柄取进程ID,打开进程句柄,读游戏敌人 x y z 内存坐标数据,本文章代码用到的是外部窗口绘制,所以找个可以创建图形窗口能画图的库就行,设置窗口无边框,设置窗口透明,设置鼠标穿透,设置窗口置顶和实时位置覆盖,这样就能绘制了。
三维投影变换
1.主要用到三维图形的投影算法,把游戏里的数据进行计算后把敌人位置绘制到屏幕上显示,所以要在游戏里找到相机矩阵。
在游戏里能影响到相机的方向的只有视角,用 CE 搜浮点数未知初始值,人物不动鼠标转动视角,再次搜变动了数值,以此类推一个一个进去看用内存视图观察特征,当然嫌麻烦可以找矩阵验证工具,可以自己网上找这里就不提供了。
常见相机矩阵有 4 x 4,其他的还有 4 x 3 等等,如果矩阵有变化那么三维图像投影变换算法也需要进行更改。
矩阵特征:
规律总结1:跳动时,有三个数据在变动
规律总结2:晃动鼠标时,有一个数据保持 0 不变
规律总结3:存在3对两两相同

2.当我在游戏里人物跳起来的时候,可以发现只有三个值不停的发送变化,其他的都没变而且存在3对两两相同数:
-0.11 -0.11
-0.99 -0.99
0.04 0.04

3.当我在游戏里旋转游戏视角的时候,可以发现有一个 0 不变。
相机z = 矩阵[0][2] x 敌人x坐标 + 矩阵[1][2] x 敌人y坐标 + 矩阵[2][2] x 敌人z坐标 + 矩阵[3][2]
比例 = 1 / 相机z
相机x = 游戏窗口宽度 + (矩阵[0][0] x 敌人x坐标 + 矩阵[1][0] x 敌人y坐标 + 矩阵[2][0] x 敌人z坐标 + 矩阵[3][0]) x 比例 x 游戏窗口宽度
相机y = 游戏窗口高度 - (矩阵[0][1] x 敌人x坐标 + 矩阵[1][1] x 敌人y坐标 + 矩阵[2][1] x (敌人z - 50) + 矩阵[3][1]) x 比例 x 游戏窗口高度
相机y2 = 游戏窗口高度 - (矩阵[0][1] x 敌人x坐标 + 矩阵[1][1] x 敌人y坐标 + 矩阵[2][1] x (敌人z + 30) + 矩阵[3][1]) x 比例 x 游戏窗口高度投影公式
- 相机 Z 计算:

- 比例因子:

- 屏幕上的 X 坐标:

- 屏幕上的 Y 坐标:

这里还有Python3的版本:Python3 透视自瞄外挂制作 ,仅供学习参考。
2024-12-20 09:58:10 星期五 更新过
大神 出一期LOL