在创建游戏程序的快捷方式,然后在快捷方式中添加-insecure关闭VAC验证服务方便进行初步的调试。如果是用7Launcher平台的得添加Run_CSGO.exe的设置并启动。

image-20211107154920433image-20211107170154792

进到游戏后记得在设置里面打开开发者控制台,方便控制机器人

image-20211107154724783

控制台常用指令
net_graph “0/1”  关闭/显示当前fps以及ping、loss、choke、tick等服务器与网络参数
mp_roundtime_defuse 50 休闲/竞技模式每局时间60分钟
sv_cheats 0/1 关闭/开启作弊功能
bot_stop 1 bot原地不动
bot_kick 踢出所有电脑
bot_add 随机添加一个bot
https://www.csgo.com.cn/news/gamenews/20170825/205831.shtml

0x01 自瞄 & 透视准备

1.0 寻找自身矩阵

我们知道矩阵的值一般是以浮点数为主,所以我们可以在CE里面搜索未知初始值的单浮点数,通过开关镜鼠标移动键盘移动不断搜索。

image-20211107171601541

找到自身4x4矩阵,可能会找到很多个

image-20211107203248709

1.1 寻找角度信息

角度信息就比较好找了,CSGO当准星指向最上方的时候,Y轴角度值为-89,当准星指向最下方的时候,上下角度的角度值为89。通过这个特点我们就能很容易定位到。

image-20211107211400488

唯一要注意的用默认调试器选项可能会照成游戏进程闪退,推荐使用VEH调试器。

image-20211107211634118

1.2 寻找位置信息

这个位置信息就是任务在整个游戏环境中的XYZ坐标,我们可以通过敌人坐标值与我们坐标值进行某种计算得到与敌人的相对距离。

最方便的是从Z坐标开始寻找,因为XY我们不能确认是增大还是减小,Z坐标可以通过所在地势高低很好判断。不过很容易会找成坐标数组这个得具体分析。

image-20211107215946272

1.3 寻找敌人和队友位置信息

这个就是通过自身间接寻找敌人的坐标,就是先走到和敌人同一水平高度,然后搜索数值范围,不断开启bot运动多次搜索进行定位。

image-20211108185444998

我们利用CE的结构分析观察上层指针

image-20211108185612149

我们通过大胆猜测和测试,可以发现每间隔0x10就是另一个人物的结构体指针,在个结构体0xA8是该人物的Z坐标,0x100是血量等。

image-20211108185911638

图中一共有四个结构体指针,和游戏中人物数量匹配,敌人和队友都在这一个数组中。

image-20211108190024088

1.4 寻找人物骨骼

为了实现骨骼透视和锁头,还需要得到敌人的骨骼坐标,在游戏建模的时候为了实现人物模型的生动性,一个人物是由多个模块组成的比如头、手、腿、脚等,这样设计可以实现各个骨骼的各自移动。因此,每块骨骼都应该具有一个独立的坐标点,我们得到了其中某些骨骼的坐标点,才可以实现出骨骼透视和锁头。

骨骼坐标也是由XYZ的,搜索骨骼坐标有个非常关键的技巧,当我们看向敌人的时候,敌人的骨骼坐标就会发生变动,而当我们远离且不看向敌人时,这个值就不变。所以我们就可以利用这个规律进行骨骼坐标搜索。

image-20211108213622222

我们找到骨骼坐标头后可以发现,当我们看向敌人的时候这个数组中的XYZ值一直在浮动,远离不看就变动停止。而且这个骨骼头位置是通过人物结构体二级指针指向的。

image-20211108213838528

1.5 寻找人物结构体的阵营信息

通过结构分析工具进行对比查看

image-20211108214513897

我们把0xF4偏移的值改成一样的造成了游戏标识判断错误,改成1敌人模型消失,所以可以大胆猜测这里就是阵营判断。

image-20211108214816890

0x02 内置辅助

对于透视分为内置辅助和外置辅助,内置的意思就是我们编写一个Dll文件注入到目标进程Hook掉D3D绘制的相关函数,以实现我们的透视方框;而外置辅助就是在游戏窗口上生成一个透明的窗口,然后通过跨进程读取游戏内存数据,在自己的透明窗口上进行透视方框绘制。

现在我们先讨论内置辅助的前置准备-HOOKD3D绘制相关函数,电脑在安装DirectX后会有相关的例子,本节以官方的ShadowMap.exe程序进行测试,我们使用MinHook这个HOOK框架进行操作。

image-20211109195650911

image-20211110111605425

HRESULT WINAPI MyReset(IDirect3DDevice9* direct3dDevice9, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
    OutputDebugString(L"MyReset");

    HRESULT result = fpReset(direct3dDevice9, pPresentationParameters);
    return result;
}

HRESULT WINAPI MyEndScene(IDirect3DDevice9* direct3dDevice9)
{


    HRESULT result = fpEndScene(direct3dDevice9);
    return result;
}

unsigned int WINAPI InitD3D9(PVOID data)
{
    OutputDebugString(L"InitD3D9");

    g_Direct3d9 = Direct3DCreate9(D3D_SDK_VERSION);
    OutputDebugString(L"Direct3DCreate9");

    memset(&g_Present, 0, sizeof(g_Present));
    g_Present.Windowed = TRUE;
    g_Present.SwapEffect = D3DSWAPEFFECT_DISCARD;
    g_Present.BackBufferFormat = D3DFMT_UNKNOWN;

    HRESULT result = g_Direct3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, FindWindowW(L"Direct3DWindowClass", NULL),
        D3DCREATE_SOFTWARE_VERTEXPROCESSING, &g_Present, &g_Direct3ddevice9);
    OutputDebugString(L"CreateDevice");

    //HOOK
    int* g_direct3d9Table = (int*)*(int*)g_Direct3d9;
    int* g_direct3dDevice9Table = (int*)*(int*)g_Direct3ddevice9;

    if (MH_Initialize() != MH_OK)
       return 1;
    OutputDebugString(L"MH_Initialize");

    if (MH_CreateHook((LPVOID)g_direct3dDevice9Table[16], &MyReset,
        reinterpret_cast<LPVOID*>(&fpReset)) != MH_OK)
        return 1;
    if (MH_CreateHook((LPVOID)g_direct3dDevice9Table[42], &MyEndScene,
        reinterpret_cast<LPVOID*>(&fpEndScene)) != MH_OK)
        return 1;
    OutputDebugString(L"MH_CreateHook");
    
    if (MH_EnableHook((LPVOID)g_direct3dDevice9Table[16]) != MH_OK)
        return 1;
    if (MH_EnableHook((LPVOID)g_direct3dDevice9Table[42]) != MH_OK)
        return 1;
    OutputDebugString(L"MH_EnableHook");
    
    return 0;
}

2.1 ImGui绘制菜单

ImGui是一个开源的图形库上手比较简单,本节拿来做菜单的绘制测试。我们参考imgui官方给的例子把需要的cpp和h文件加入到我们测试工程。

image-20211110194921684

模仿着官方实例代码我们可以编写出我们自己的菜单,主要就是依赖于此前对D3D9的HOOK在对应绘制函数上进行操作。

LRESULT CALLBACK MyProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam))
        return true;

    return CallWindowProcW(g_fpProc, hWnd, uMsg, wParam, lParam);
}

void InitImGui(IDirect3DDevice9* direct3dDevice9)
{
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsLight();
    ImGuiIO& io = ImGui::GetIO(); 
    io.IniFilename = NULL;
    io.LogFilename = NULL;

    ImGui_ImplWin32_Init(FindWindowW(L"Direct3DWindowClass", NULL));
    ImGui_ImplDX9_Init(direct3dDevice9);
}

HRESULT WINAPI MyReset(IDirect3DDevice9* direct3dDevice9, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
    OutputDebugString(L"MyReset");

    ImGui_ImplDX9_InvalidateDeviceObjects();
    HRESULT result = g_fpReset(direct3dDevice9, pPresentationParameters);
    ImGui_ImplDX9_CreateDeviceObjects();

    return result;
}

HRESULT WINAPI MyEndScene(IDirect3DDevice9* direct3dDevice9)
{
    static BOOL b_First = TRUE;
    if (b_First)
    {
        b_First = FALSE;
        InitImGui(direct3dDevice9);
        g_fpProc = (WNDPROC)SetWindowLongA(FindWindowW(L"Direct3DWindowClass", NULL), GWL_WNDPROC, (LONG)MyProc);
    }

    ImGui_ImplDX9_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();

    ImGui::Begin("MyWindow");
    ImGui::Text("Test Windows");
    ImGui::End();

    ImGui::EndFrame();
    ImGui::Render();
    ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

    HRESULT result = g_fpEndScene(direct3dDevice9);

    return result;
}

image-20211110195143944

0x03 外置辅助

外置辅助就是游戏进程窗口上方建立一个透明窗口,本例子将配合Blackbone这个库进行跨进程读写操作,直接上源码:

void StartCheat()
{
    InitCheat(L"csgo.exe");

    extern pDoingCheat g_CheatFunc;
    g_CheatFunc = DoingCheat;

    // 以下和内置辅助类似,创建透明D3D窗口,在窗口过程函数中WM_PAINT调用DoingCheat函数
    HWND g_hTransparentHwnd =  CreateTransparentWindow(g_hGameWnd);
    InitDirect3d9(g_hTransparentHwnd);
    
    MessageLoop(g_hGameWnd, g_hTransparentHwnd);
}

void InitCheat(LPCWSTR lpProcessName)
{
    g_hGameWnd = FindWindow(NULL, L"Counter-Strike: Global Offensive");

    auto pids = Process::EnumByName(lpProcessName);
    if (!pids.empty())
    {
        procCSGO.Attach(pids[0]);

        auto clientModule = procCSGO.modules().GetModule(L"client.dll");
        auto serverModule = procCSGO.modules().GetModule(L"server.dll");
        auto engineModule = procCSGO.modules().GetModule(L"engine.dll");

        g_MatrixAddress = clientModule->baseAddress + dwMatrix;
        g_PlayersAddress = clientModule->baseAddress + dwCharacter;
        g_AngleAddress = engineModule->baseAddress + dwAngleBase;
        g_SelfAddress = serverModule->baseAddress + dwXYZBase;

        printf("自己矩阵基地址 : %8x \n", g_MatrixAddress);
        printf("自己角度基地址 : %8x \n", g_AngleAddress);
        printf("自己位置基地址 : %8x \n", g_SelfAddress);
        printf("玩家信息基地址 : %8x \n", g_PlayersAddress);
        printf("\n");
    }
}
void DoingCheat() 
{
    Player* player = GetPlayerList();// 遍历读取玩家列表信息

    DrawPlayerBox(player);// 绘制人物矩形

    if (GetMouseRightDown())// 自瞄
        StartAimbot(player);

    FreePlayList(player);
}

玩家结构体

struct Player
{
    BOOL effective;        //是否有效
    int aimbot_len;        //自瞄长度
    BOOL self;            //是否自己
    float location[3];    //身体位置
    float head_bone[3];    //头骨位置
    BOOL mirror;        //是否开镜
    int camp;            //阵营
    int blood;            //血量
    struct Player* next = NULL;
};

遍历获取玩家信息

Player* GetPlayerList()
{
    Player* headPlayer = NULL, * tmpPlayer = NULL;
    auto& memoryCSGO = procCSGO.memory();

    for (int i = 0; i < g_PlayerNumberMax; i++)
    {
        DWORD dwPlayerBaseAddress = 0;
        DWORD dwPlayerBlood = 0;
        DWORD dwBoneBaseAddress = 0;
        memoryCSGO.Read((g_PlayersAddress + i * 0x10), sizeof(dwPlayerBaseAddress), &dwPlayerBaseAddress);
        if (dwPlayerBaseAddress == 0)
            break;

        // Blood
        memoryCSGO.Read((dwPlayerBaseAddress + 0x100), sizeof(dwPlayerBlood), &dwPlayerBlood);
        if (dwPlayerBaseAddress <= 0)
            continue;

        Player* temp = (Player*)VirtualAlloc(NULL, sizeof(Player), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        temp->blood = dwPlayerBlood;
        temp->aimbot_len = 9999;
        temp->effective = TRUE;

        // Bone
        if (!memoryCSGO.Read((dwPlayerBaseAddress + 0x26A8), sizeof(dwBoneBaseAddress) ,&dwBoneBaseAddress))
        {
            memoryCSGO.Read((dwBoneBaseAddress + 99 * sizeof(float)), sizeof(float), &(temp->head_bone[0]));
            memoryCSGO.Read((dwBoneBaseAddress + 103 * sizeof(float)), sizeof(float), &(temp->head_bone[1]));
            memoryCSGO.Read((dwBoneBaseAddress + 107 * sizeof(float)), sizeof(float), &(temp->head_bone[2]));
        }

        memoryCSGO.Read(dwPlayerBaseAddress + 0xA0, sizeof(temp->location), temp->location);
        memoryCSGO.Read(dwPlayerBaseAddress + 0xF4, sizeof(int), &temp->camp);
        memoryCSGO.Read(dwPlayerBaseAddress + 0x3914, sizeof(BOOL), &temp->mirror);

        if (headPlayer == NULL)
        {
            headPlayer = temp;
            tmpPlayer = headPlayer;
        }
        else
        {
            tmpPlayer->next = temp;
            tmpPlayer = temp;
        }
    }
    return headPlayer;
}

绘制透视方框

void DrawPlayerBox(Player* playerHead)
{
    int x, y, width, height;
    GetWindowSize(g_hGameWnd, x, y, width, height);
    width /= 2;
    height /= 2;

    float matrix[4][4];
    auto& memoryCSGO = procCSGO.memory();
    memoryCSGO.Read(g_MatrixAddress, (sizeof(float) * 4 * 4), matrix);

    // SelfCoordinate
    float selfLocation[3];
    DWORD dwLocationBaseAddress = 0;
    memoryCSGO.Read(g_SelfAddress, sizeof(dwLocationBaseAddress), &dwLocationBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwLocationBaseAddress + 0x1DC, (sizeof(float) * 3), selfLocation);

    // SelfCamp
    Player* tempPlayerHead = playerHead;
    for(int i = 0;i < g_PlayerNumberMax && tempPlayerHead; ++i)
    {
        if (tempPlayerHead->effective)
        {
            int temp_x = abs(selfLocation[0] - tempPlayerHead->location[0]);
            int temp_y = abs(selfLocation[1] - tempPlayerHead->location[1]);
            if (temp_x < 5.0f && temp_y < 5.0f)
            {
                tempPlayerHead->self = TRUE;
                g_MyCamp = tempPlayerHead->camp;
                break;
            }
            tempPlayerHead = tempPlayerHead->next;
        }
    }

    for (int i = 0; i < g_PlayerNumberMax && playerHead; ++i)
    {
        int x, y, w, h;
        if (playerHead->effective && playerHead->self == FALSE && ChangeMatrixInfo(matrix, playerHead->location, width, height, x, y, w, h))
        {
            if (playerHead->blood > 0)
            {
                if(playerHead->camp!=3 && playerHead->camp!=2)
                    printf("%d\n", playerHead->camp);
                D3DCOLOR color = D3DCOLOR_XRGB(255, 255, 0);
                if (g_MyCamp != playerHead->camp)
                {
                    color = D3DCOLOR_XRGB(255, 0, 0);
                    playerHead->aimbot_len = GetAimbotLen(width, height, x, y);
                }
                DrawRect(color, x, y, w, h);
                DrawPlayerBlood(playerHead->blood, x - 5, y, h);
                DrawUnderLine(color, width, height, x + (w / 2), y + h);
            }
        }
        playerHead = playerHead->next;
    }
}

自瞄

void StartAimbot(Player* player, float maxFov = 30.0f)
{
    float selfLocation[3];
    auto& memoryCSGO = procCSGO.memory();
    DWORD dwLocationBaseAddress = 0;
    memoryCSGO.Read(g_SelfAddress, sizeof(dwLocationBaseAddress), &dwLocationBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwLocationBaseAddress + 0x1DC, (sizeof(float) * 3), selfLocation);

    // 获取最近人物骨骼
    Player* aimPlayer = GetRecentHeadBone(player);
    if (aimPlayer == NULL)
        return;

    // 获取当前角度
    float flCurrentAngle[2];
    DWORD dwAngleBaseAddress = 0;
    memoryCSGO.Read(g_AngleAddress, sizeof(dwAngleBaseAddress), &dwAngleBaseAddress);
    if (dwLocationBaseAddress)
        memoryCSGO.Read(dwAngleBaseAddress + 0x4D90, (sizeof(float) * 2), flCurrentAngle);

    float flAimAngle[2];
    GetAimBotAngle(selfLocation, aimPlayer->head_bone, flAimAngle, -15.0f);

    if (abs(flAimAngle[0] - flCurrentAngle[0]) > maxFov
        || abs(flAimAngle[1] - flCurrentAngle[1]) > maxFov)
        return;
    // 自瞄
    memoryCSGO.Write(dwAngleBaseAddress + 0x4D90, (sizeof(float) * 2), flAimAngle);
}

成果

image-20211112164514553

image-20211112164546054

这里有两个我认为值得一提的点,一个是创建透明窗口的名字我使用了时间作为种子随机生成窗口名;另外一个我把所有的偏移放到了一个头文件里方便以后进行修改,当然可以用BlackBone的特征码匹配自动寻找基址。

image-20211112165849750

image-20211112165645814

0x04 总结

花了一周左右的时间比较完整的实现了CSGO辅助的学习,最后配合MinHook,D3D和BlackBone了解了内外置自瞄+透视的辅助功能实现原理,收获还是比较多的,这里放几个我个人认为比较好的学习资料以供后来者学习参考。

Osiris --- 开源的CSGO辅助

How-to-create-a-csgo-cheating-program --- 本文主要参考的教程

Xenos --- 好用的开源DLL注入器