【技术分享】CK竞技之王游戏辅助制作
2024-11-20 / 1 评论 / 3,725 阅读 / 764 点赞

【技术分享】CK竞技之王游戏辅助制作

发光的神
2024-11-20 / 1 评论 / 3,725 阅读 / 正在检测是否收录...

简介

以下教程是自我总结出的,我会很详细的进行编写,配详细的步骤和视频教程,本文章用到的工具会有 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位程序上,微软不公开的但是可以用,这两个函数不属于动态链接库,感兴趣的可以自己去研究。

lldqbi7z.png

完整代码(读写子弹数量)

// 读写内存简单例子:
#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 坐标地址,有了这些数据才能为后期做准备。

llgnk84f.png

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

llgo0w0y.png

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

llg0pzd7.png

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_y

3.计算两点之间的直线距离,可以使用勾股定理计算,本教程中我使用了欧氏距离计算。

Euclidean distance

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写入计算出的敌人位置y

7.获取游戏准星 x,y 移动坐标,最终配合 x 角度与 y 角度,用来移动准星实现自瞄效果。(以上方式是最原始的自瞄,用三角函数进行实现,只是单个人自瞄哦,多人只要遍历敌人数据x,y,z,加入到数组中通过距离判断优先级再自瞄,这样就完成了暂时不做比较懒。)

无限急速雷

lli0p078.png

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

lli0snml.png

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

lli0uyrb.png

我分析过了,扔雷的速度就在这个反汇编代码,引用的这个地址附近,通过访问内存就能看到 + 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 窗口

lor28ey9.png

方框透视详解

先理一下思路,要用到的功能有,通过窗口句柄取进程ID,打开进程句柄,读游戏敌人 x y z 内存坐标数据,本文章代码用到的是外部窗口绘制,所以找个可以创建图形窗口能画图的库就行,设置窗口无边框,设置窗口透明,设置鼠标穿透,设置窗口置顶和实时位置覆盖,这样就能绘制了。

三维投影变换

1.主要用到三维图形的投影算法,把游戏里的数据进行计算后把敌人位置绘制到屏幕上显示,所以要在游戏里找到相机矩阵。
在游戏里能影响到相机的方向的只有视角,用 CE 搜浮点数未知初始值,人物不动鼠标转动视角,再次搜变动了数值,以此类推一个一个进去看用内存视图观察特征,当然嫌麻烦可以找矩阵验证工具,可以自己网上找这里就不提供了。
常见相机矩阵有 4 x 4,其他的还有 4 x 3 等等,如果矩阵有变化那么三维图像投影变换算法也需要进行更改。

矩阵特征:
规律总结1:跳动时,有三个数据在变动
规律总结2:晃动鼠标时,有一个数据保持 0 不变
规律总结3:存在3对两两相同

lorxu4i4.png

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

lorxoz2r.png

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 游戏窗口高度

投影公式

  1. 相机 Z 计算

m4xgu7gb.png

  1. 比例因子

m4xgtx7v.png

  1. 屏幕上的 X 坐标
    m4xgun8t.png
  2. 屏幕上的 Y 坐标

m4xgvsra.png

这里还有Python3的版本:Python3 透视自瞄外挂制作 ,仅供学习参考。

2024-12-20 09:58:10 星期五 更新过

764

评论 (1)

取消
  1. 头像
    小迷弟
    Windows 10 · Google Chrome

    大神 出一期LOL

    回复
0:00