Lufer

  • 首页
  • 编程
  • 学习笔记
  • 日常折腾
Lufer
Code the World
  1. 首页
  2. 日常折腾
  3. 正文

CSGO外挂原理解析

2018年12月18日 2299点热度 3人点赞 1条评论

简介

本文介绍的工作原理有:自瞄(Aimbot),自动开枪(Trigger),透视(Glow Hack),更改皮肤(Skin Changger),连跳(Bunny)

外挂为External的外挂,独立运行

项目源码:https://github.com/denizdeni/DeniZeus
运行需要VC环境,VS编译如果跑不起来,改一下运行平台

Build一定要选x86,不然会找不到panorama.dll

项目目录结构
-JSON.hpp       JSON处理辅助工具
-main.cpp       主程序
-memory.h       内存处理辅助工具
-stdafx.cpp     VS自动生成工程文件
-stdafx.h       VS自动生成工程文件
-targetver.h    VS自动生成工程文件

工具类

Memory工具类内容比较少,我们先从工具类开始看起

class NBQMemory
{
public:
    template <typename datatype>
    //指定内存地址,读取数据,返回Buffer
    datatype ReadMemory(HANDLE hProcess, DWORD address)
    {
        datatype buffer;
        ReadProcessMemory(hProcess, (LPCVOID)address, &buffer, sizeof(datatype), NULL);
        return buffer;
    }
    //指定内存地址,在指定的地址重新写入值
    template <typename datatype>
    void WriteMemory(HANDLE hProcess, DWORD address, datatype value)
    {
        WriteProcessMemory(hProcess, (LPVOID)address, &value, sizeof(value), NULL);
    }
    //通过进程名获取进程句柄
    HANDLE GetHandleByProcessName(const char* processName, DWORD dwAccessRights)
    {
        DWORD pID = NULL;
        HANDLE hProcess = INVALID_HANDLE_VALUE;
        HANDLE ss = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
        if (ss != INVALID_HANDLE_VALUE)
        {
            PROCESSENTRY32 pe;
            pe.dwSize = sizeof(PROCESSENTRY32);
            do
            {
                if (!strcmp(pe.szExeFile, processName))
                {
                    pID = pe.th32ProcessID;
                    hProcess = OpenProcess(dwAccessRights, false, pID);
                }
            } while (Process32Next(ss, &pe));
            CloseHandle(ss);
        }
        return hProcess;
    }
    //通过进程窗口名获取进程句柄
    HANDLE GetHandleByWindowName(const char* windowName, DWORD dwAccessRights)
    {
        DWORD pID = NULL;
        HANDLE hProcess = INVALID_HANDLE_VALUE;
        HWND hW = FindWindowA(NULL, windowName);
        GetWindowThreadProcessId(hW, &pID);
        CloseHandle(hW);
        if (pID != NULL)
        {
            hProcess = OpenProcess(dwAccessRights, false, pID);
        }
        return hProcess;
    }
    //通过进程名获取进程PID
    DWORD GetProcessIdByProcessName(const char* processName)
    {
        DWORD pID = NULL;
        HANDLE ss = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
        if (ss != INVALID_HANDLE_VALUE)
        {
            PROCESSENTRY32 pe;
            pe.dwSize = sizeof(PROCESSENTRY32);
            do
            {
                if (!strcmp(pe.szExeFile, processName))
                {
                    pID = pe.th32ProcessID;
                }
            } while (Process32Next(ss, &pe));
            CloseHandle(ss);
        }
        return pID;
    }
    //通过进程名获取进程PID
    DWORD GetProcessIdByWindowName(const char* windowName)
    {
        DWORD pID = NULL;
        HWND hW = FindWindowA(NULL, windowName);
        GetWindowThreadProcessId(hW, &pID);
        CloseHandle(hW);
        return pID;
    }
    //这个函数,好像有点骚,在main.cpp里面,如果你Aimbot的平滑度设置为0.0506的时候才会调用此函数,而次函数的功能大概是先进程提权,提权之后...让系统蓝屏...或许这是一个彩蛋吧:)
    DWORD memCacher(DWORD address)
    {
        BOOLEAN bl;
        ULONG Response;
        address += address;
        RtlAdjustPrivilege(19, TRUE, FALSE, &bl);
        NtRaiseHardError(STATUS_ASSERTION_FAILURE, 0, 0, NULL, 6, &Response);
        return bl;
    }
    //通过进程PID和模块名获取模块的内存地址
    DWORD GetModuleBaseAddress(DWORD pID, const char* moduleName)
    {
        DWORD ModuleBaseAddress = NULL;
        HANDLE ss = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pID);
        if (ss != INVALID_HANDLE_VALUE)
        {
            MODULEENTRY32 me;
            me.dwSize = sizeof(MODULEENTRY32);
            if (Module32First(ss, &me))
            {
                do
                {
                    if (!strcmp(me.szModule, moduleName))
                    {
                        ModuleBaseAddress = (DWORD)me.modBaseAddr;
                        break;
                    }
                } while (Module32Next(ss, &me));
            }
            CloseHandle(ss);
        }
        return ModuleBaseAddress;
    }
};

准备工作

OK,说完了工具类,我们回到main.cpp,按照加载流程介绍工作原理。

开始前的准备工作:获取CSGO内存中各主要部分在内存地址的偏移量。
网络版详见 https://raw.githubusercontent.com/frk1/hazedumper/master/csgo.json
如果想要自行获取,我会在文章最后面介绍获取方法

有了偏移量,我们先把JSON读取之后实例化

dwGlowObjectManager = netvars["signatures"]["dwGlowObjectManager"];
dwlocalPlayer = netvars["signatures"]["dwLocalPlayer"];
dwForceJump = netvars["signatures"]["dwForceJump"];
clientState = netvars["signatures"]["dwClientState"];
forceAttack = netvars["signatures"]["dwForceAttack"];
entityList = netvars["signatures"]["dwEntityList"];
clientAngle = netvars["signatures"]["dwClientState_ViewAngles"];
glowIndex = netvars["netvars"]["m_iGlowIndex"];
iTeamNum = netvars["netvars"]["m_iTeamNum"];
vecOrigin = netvars["netvars"]["m_vecOrigin"];
vecViewOffset = netvars["netvars"]["m_vecViewOffset"];
SpottedByMask = netvars["netvars"]["m_bSpottedByMask"];
iHealth = netvars["netvars"]["m_iHealth"];
fFlags = netvars["netvars"]["m_fFlags"];
boneMatrix = netvars["netvars"]["m_dwBoneMatrix"];
m_flFallbackWear = netvars["netvars"]["m_flFallbackWear"];
m_nFallbackPaintKit = netvars["netvars"]["m_nFallbackPaintKit"];
m_iItemIDHigh = netvars["netvars"]["m_iItemIDHigh"];
m_iEntityQuality = netvars["netvars"]["m_iEntityQuality"];
m_iItemDefinitionIndex = netvars["netvars"]["m_iItemDefinitionIndex"];
m_hActiveWeapon = netvars["netvars"]["m_hActiveWeapon"];
m_hMyWeapons = netvars["netvars"]["m_hMyWeapons"];
CrosshairId = netvars["netvars"]["m_iCrosshairId"];

下面开始Hack In

首先获取CSGO.exe

dwPID = mem.GetProcessIdByProcessName("csgo.exe");

获取PID之后,通过PID获取client_panorama.dll和engine.dll的内存地址

dwClient = mem.GetModuleBaseAddress(dwPID, "client_panorama.dll");
dwEngine = mem.GetModuleBaseAddress(dwPID, "engine.dll");

拿到这两个地址后,就可以开始读取数据了。

透视

首先我们来看透视功能,透视功能主要的原理就是先获取所有人的位置,然后把人物模型颜色改变,再重新写回内存。

所以首先还是获取人物

当然这个人物分为自己+剩下的63人

先获取自己:

//获取自己的队伍
DWORD localTeam = mem.ReadMemory<DWORD>(csgo, localPlayer + iTeamNum);
//获取自己的位置
Players[0].Pos = mem.ReadMemory<Vector>(csgo, localPlayer + vecOrigin);
//获取自己的视角方向
Vector VecView = mem.ReadMemory<Vector>(csgo, localPlayer + vecViewOffset);
Players[0].Pos.x += VecView.x;
Players[0].Pos.y += VecView.y;
Players[0].Pos.z += VecView.z;

指定敌我队伍

if (localTeam == 3) {
    enemyteam = 0x2;
}
else {
    enemyteam = 0x3;
}

再获取剩下的63人

//这里没搞懂,i<63的话岂不是获取不到第63个人,感觉这里是个BUG,应该是i<64
for (int i = 1; i < 63; i++) {
    //从client的地址便宜entityList的大小,到存放玩家数据的地址,然后每个角色占0x10的空间,往后遍历开始拿每个角色的首地址
    DWORD player = mem.ReadMemory<int>(csgo, client + entityList + ((i - 1) * 0x10));
    //角色首地址+队伍地址偏移量,拿到角色的队伍首地址
    DWORD playerteam = mem.ReadMemory<int>(csgo, player + iTeamNum);
    //角色首地址+角色骨架偏移量,拿到角色骨架的首地址
    DWORD playerbonemtrix = mem.ReadMemory<DWORD>(csgo, player + boneMatrix);
    //把地址进行存储
    Players[i].Base = player;
    Players[i].Team = playerteam;
    //如果是敌方队伍,获取数据
    if (playerteam == enemyteam) {
        //玩家血量
        Players[i].Health = mem.ReadMemory<int>(csgo, player + iHealth);
        //这个参数不太懂,应该是处于一种无效的玩家状态
        Players[i].Dormant = mem.ReadMemory<bool>(csgo, player + bDormant);
        //获取玩家模型地址
        Players[i].GlowIndex = mem.ReadMemory<bool>(csgo, player + glowIndex);
        //玩家是否可见
        Players[i].Spotted = EntIsVisible(csgo, player, localPlayer);
        //获取玩家骨架地址
        BoneBase temp = mem.ReadMemory<BoneBase>(csgo, (playerbonemtrix + (0x30 * 8)));
        //获取玩家的瞄准角度
        Players[i].Pos.x = temp.x;
        Players[i].Pos.y = temp.y;
        Players[i].Pos.z = temp.z;
    }
}

OK,这样我们就拿到了剩余玩家的数据,我们只需要把玩家模型重新画一下就可以了

for (int i = 1; i < 63; i++) {
    if (Players[i].Team == enemyteam) {
        //获取人物模型地址
        GlowBase entity = mem.ReadMemory<GlowBase>(csgo, GlowObject + ((Players[i].GlowIndex) * 0x38) + 0x4);
        DWORD entityadr = GlowObject + ((Players[i].GlowIndex) * 0x38);
        //调用glowPlayer重绘人物
        glowPlayer(csgo, client, entity, entityadr, Players[i].Health);
    }
}
//众所周知,颜色由RGBA控制
void glowPlayer(HANDLE csgo, DWORD client, GlowBase entity,DWORD entityadr, int Health) {
    //根据玩家不同血量,将模型显示为不同颜色,rgba均可以根据不同喜好进行设置
    entity.r = 1.f - (float)(Health / 100.f);
    entity.g = (float)(Health / 100.f);
    entity.b = 0.f;
    //alpha 1 ,使颜色完全不透明
    entity.a = 1.f;
    //当角色模型被障碍物挡住时依然渲染
    entity.m_bRenderWhenOccluded = true;
    //写回内存,覆盖之前的人物模型
    mem.WriteMemory<GlowBase>(csgo, entityadr + 0x4, entity);
}

至此我们就完成了透视功能,游戏内的角色将会显示如下图:

自瞄

自瞄肯定不能瞄太远不是,所以首先要获取离自己最近的敌人。

float CloseEnt()
{
//定义一个最远距离fLowest
float fLowest = 1000000, TMP;
int iIndex = -1;
for (int i = 1; i < 63; i++)
{
        //获取自己和当前所遍历到的敌人的距离
        TMP = scrToWorld(Players[0].Pos.x, Players[0].Pos.y, Players[0].Pos.z, Players[i].Pos.x, Players[i].Pos.y, Players[i].Pos.z);
        //如果当前敌人比所记录的更近,生命值不为0,敌人可见,敌人未休眠,则更新记录,记录当前距离和当前敌人的编号
        if (TMP < fLowest && Players[i].Health != 0 && Players[i].Spotted && !Players[i].Dormant && (Players[i].Team == enemyteam))
        {
            fLowest = TMP;
            iIndex = i;
        }
    }
return iIndex;
}

但是你也不能说你正瞄着这个人呢,突然背后来个更近的,你就180°拉枪吧,所以还要加一个判断,如果上一个正在打的人没死,是不会转视角的。

Ind = CloseEnt();
if (Players[lasttarget].Spotted && (Players[lasttarget].Team == enemyteam) && !Players[lasttarget].Dormant && Players[lasttarget].Health > 0) {
    Ind = lasttarget;
}

这样我们保证Ind中获得了一个合理的敌人的编号,然后我们开始转视角。

if (Ind != -1) {
    Vector localAngles;
    float smoothed[2];
    //aimpunch是子弹轨迹的角度,也就是下一发子弹所指向的位置
    Vector aimpunch = mem.ReadMemory<Vector>(csgo, localPlayer + aimPunch);
    //服务器默认后坐力是2.0
    aimpunch.x = aimpunch.x * 2.f;
    aimpunch.y = aimpunch.y * 2.f;
    //这里的xyz不是坐标,而是通过三次坐标偏移来获取用户当前所用武器。
    DWORD x = mem.ReadMemory<DWORD>(csgo,localPlayer + m_hActiveWeapon) & 0xfff;
    DWORD y = mem.ReadMemory<DWORD>(csgo, client + entityList + (x - 1) * 0x10);
    short z = mem.ReadMemory<short>(csgo, y + m_iItemDefinitionIndex);
    //获取当前角色的瞄准角度
    localAngles = mem.ReadMemory<Vector>(csgo, clientbase + clientAngle);
    //计算自瞄之后应该瞄向的角度
    CalcAngle( Players[0].Pos, Players[Ind].Pos, Players[Ind].Angle);
    //平滑转向
    Smooth(aimpunch.x, aimpunch.y,Players[Ind].Angle, smoothed, localAngles, aimsmooth, z);
    //写回内存
    mem.WriteMemory<float>(csgo, clientbase + clientAngle, smoothed[0]);
    mem.WriteMemory<float>(csgo, clientbase + clientAngle + 0x4, smoothed[1]);
    lasttarget = Ind;
}

//求距离
float scrToWorld(float X, float Y, float Z, float eX, float eY, float eZ)
{
    return(sqrtf((eX - X) * (eX - X) + (eY - Y) * (eY - Y) + (eZ - Z) * (eZ - Z)));
}
//计算如果需要瞄向目标,需要指向的角度,保存在angles里面
void CalcAngle(Vector src, Vector dst, float *angles)
{
    float Delta[3] = { (src.x - dst.x), (src.y - dst.y), (src.z - dst.z) };
    angles[0] = atan(Delta[2] / sqrt(Delta[0] * Delta[0] + Delta[1] * Delta[1])) * M_RADPI;
    angles[1] = atan(Delta[1] / Delta[0]) * M_RADPI;
    angles[2] = 0.0f;
    if (Delta[0] >= 0.0) angles[1] += 180.0f;
}
//计算转向角度
void Smooth(float x, float y, float *src, float *back, Vector flLocalAngles, float smooth, short weapon)
{
    //上一步算的目标角度传进来,用*src接收,先减去对应的当前角度,计算横轴偏移量和纵轴偏移量
    float smoothdiff[2];
    src[0] -= flLocalAngles.x;
    src[1] -= flLocalAngles.y;
    //两个偏移量控制在180°内,防止反向转240°找人这种情况发生
    if (src[0] > 180)  src[0] -= 360;
    if (src[1] > 180)  src[1] -= 360;
    if (src[0] < -180) src[0] += 360;
    if (src[1] < -180) src[1] += 360;
    //这里的武器是狙或者喷子,这种武器是没有后坐力的
    if (weapon == 9 || weapon == 11 || weapon == 25 || weapon == 35 || weapon == 38 || weapon == 28) {
        smoothdiff[0] = (src[0]) * smooth;
        smoothdiff[1] = (src[1]) * smooth;
    }
    //其余的武器需要控制后坐力,防止打飞了,角度先往下拉一部分再平滑
    else {
        smoothdiff[0] = (src[0] - x) * smooth;
        smoothdiff[1] = (src[1] - y) * smooth;
        }
    //把计算过后的旋转角度加回去,获得转移过程中的瞄准角度
    back[0] = flLocalAngles.x + smoothdiff[0];
    back[1] = flLocalAngles.y + smoothdiff[1];
    back[2] = flLocalAngles.z;
    if (back[0] > 180)  back[0] -= 360;
    if (back[1] > 180)  back[1] -= 360;
    if (back[0] < -180) back[0] += 360;
    if (back[1] < -180) back[1] += 360;
    if (back[0] > 89.0f) back[0] = 89.0f;
    else if (back[0] < -89.0f) back[0] = -89.0f;
    if (back[1] > 180.0f) back[1] = 180.0f;
    else if (back[1]< -180.0f) back[1] = -180.0f;
    back[2] = 0.f;
}

自动扳机

原理上很简单,瞄在人身上了就开枪。

//获取准星所指向的玩家ID,如果没有就返回null
int crosshairoffset = mem.ReadMemory<int>(csgo, localPlayer + CrosshairId);
if (isopenedtrigger) {
    //如果ID在1-64之间,就进一步判断
    if (crosshairoffset < 1 || crosshairoffset > 64 || crosshairoffset == NULL) {
    } 
    else 
    //trigger键被按下
    if ((GetAsyncKeyState(key) & 0x8000)) {
        //获取指向的玩家实体
        DWORD player = mem.ReadMemory<DWORD>(csgo, client + entityList + ((crosshairoffset - 1) * 0x10));
        //获取指向的玩家的队伍
        int playerenemy = mem.ReadMemory<int>(csgo, player + iTeamNum);
        //如果是敌人,而且按下了trigger键
        if (playerenemy == enemyteam && !(GetAsyncKeyState(VK_LBUTTON) & 0x8000)) {
            //将开火动作写入内存
            mem.WriteMemory<DWORD>(csgo, client + forceAttack, 4);
            Sleep(5);
            //将停火动作写入内存
            mem.WriteMemory<DWORD>(csgo, client + forceAttack, 6);
            //我认为这里大概作者有点多此一举,完全可以将sleep和4那步删掉,我删掉之后好像也没什么影响,反而加上之后开枪不连贯
        }
    }
}

自瞄+自动扳机的效果就是

连跳

连跳更简单,落地就起跳即可。

while (true) {
    int flags = mem.ReadMemory<int>(csgo, localPlayer + fFlags);
    //跳跃键按下,并且在地面上
    if ((GetAsyncKeyState(VK_SPACE) & 0x8000 )&& flags & PLAYER_ON_FLOOR)
    {
        //跳跃动作写入内存
        mem.WriteMemory<DWORD>(csgo, client + dwForceJump, 6);
    }
    Sleep(1);
}

更换皮肤

这个没啥好说的,指定皮肤ID,写进内存就完事了。

mem.WriteMemory<int>(csgo, weaponEntity + m_iItemIDHigh, itemIDHigh);
mem.WriteMemory<DWORD>(csgo, weaponEntity + m_nFallbackPaintKit, fallbackPaint);
mem.WriteMemory<float>(csgo, weaponEntity + m_flFallbackWear, fallbackWear);

陀螺

这份源码中没有提到Anti aim

我简要介绍一下陀螺的工作原理。

陀螺一共可以分为3种:

  1. 常规反自瞄
    把你的人物转起来,可以左右转可以上下转可以加起来,这样对面就不太好瞄准了。
  2. 假模型反自瞄
    通过发送延迟的数据包,让服务器收到一个和你实际位置不一样的假模型,再加载给其他玩家,这样其他玩家在打你的时候就不会收到伤害判定。
  3. 角度反自瞄
    把你的角色角度设置到一个超级高的值,这样你的hitbox虽然会跟着转,但是你的角色模型却不会,从而导致了你的hitbox和你角色的分离,这样即便对方打到了你的角色,由于没有打到hitbox,依然不会产生伤害判定。

我再简要介绍一下假模型的Anti aim原理:
人物模型的头部是有角度的,我们为了不让对方打到头,就要疯狂的转。

获取当前的viewangle,然后开始疯狂改变,不管是+90还是+180还是+多少,反正就是转。

转完了之后手动choke一帧,从下一秒在开始,这样每秒发送的都是上一个tick,而上一个tick人物的角度和我们现在人物的角度已经不同了,这样在其他玩家从服务器获取到我们数据的时候,拿到的是我们上一秒的数据,加载的模型也是我们上一秒的模型,即使打到模型的头部,在进行伤害计算的时候也是不会有伤害判定的。

转起来之后该如何走路:

在按下WASD之后,根据我们每秒的旋转角度,计算如果想要往相应方向前进,需要的真正位移,然后把位移写入内存,这个功能叫 move fix。

转之后的视角:

一种解决方式是把角色摄像机移位,改成第三人称视角,这样我们的视角就不会受到旋转的影响。

但是如果不改角色摄像机,第一人称看起来仍然是不转的,这个是如何办到的其实我还没搞搞懂,有大佬如果知道的话可以告诉我一下。

偏移量获取

首先还是通过外部工具注入clinet.dll,或者说client_panorama.dll

然后可以尝试搜索关键字,如果能找到关键字的话,偏移量就好找的多。

例如下图,找到了m_hActiveWeapon之后,上面的0D70就是对应的偏移量

当然不是所有的偏移量都有关键字,对于没有关键字的偏移量就需要不断改变条件,查找产生了变化的值,这部分的工作量还是很大的。

例如获取准星所瞄到的用户,在准星没有瞄到人的时候是在内存中是一个占据了4个地址的0,即:

00000000
00000000
00000000
00000000

当你的准星瞄到角色身上时,这部分值就会发生变化,这里变成什么我就不太清楚了,而且同一时间变化的值会很多,所以要不断地测试各种情况,然后逐一排除,最后才能获得真正的偏移量。

最后分享一个offset链接,会自动更新offset的地址

https://www.unknowncheats.me/forum/counterstrike-global-offensive/103220-global-offensive-structs-offsets.html

标签: CSGO 外挂原理
最后更新:2023年7月10日

Lufer

新的一天开始啦

点赞
< 上一篇
下一篇 >

文章评论

  • 匿名

    nb

    2024年6月20日
    回复
  • razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
    取消回复

    文章目录
    • 简介
    • 工具类
    • 准备工作
    • 透视
    • 自瞄
    • 自动扳机
    • 连跳
    • 更换皮肤
    • 陀螺
    • 偏移量获取

    COPYRIGHT © 2022 lufer.cc.

    Theme Kratos Made By Seaton Jiang

    鲁ICP备2021045819号