起因是我一朋友总跟我吐槽永劫无间的外挂非常多,次次刷新我对这个游戏外挂的想象。 在一次吐槽之中,我说我来看看都有什么类型的吧!点开了Google,进行一个索的搜。不搜不知道哟嘿,连源代码都有就很离谱。

我为什么开始研究这个东西呢,多多少少和我的工作有点关系的。 手上有一个这样的项目,使用PyQt写的一个程序,我只修改Qml(UI)部分的代码会导致底层的C++因为内存问题而崩溃,这个崩溃还是随机发生的。 最无语的是如果启动不起来尝试个三四次就能启动了,就很怪,很怪欸!!!

要获取角色状态,99%吧,都是从内存里面读的。

原罪

花了一点点“代价”,获得了一份源代码。 项目名称叫做msimg32_jump,主要功能是自动振刀。

看了一天,这个挂的原理大概是:

  1. 从内存里读取状态

  2. 死循环判断每一个人目前的状态

  3. 当某瞬间满足了振刀条件,就会模拟键鼠输入

内存读取

相关文件:

  • Naraka.h

  • msimg32.cpp

msimg32.cpp开头好像有个寻找特征码的函数ULONG_PTR Find(ULONG_PTR Module, std::string Feature)

ULONG_PTR Find(ULONG_PTR Module, std::string Feature)
{
    // 去空
    for (int i = 0; i < Feature.length(); i++)
    {
        if (Feature[i] == ' ')
            Feature.replace(i, 1, "");
    }
    if (Feature.size() < 2)
        return 0;
​
    if (Feature.size() % 2 != 0)
        return 0;
    std::vector<std::pair<bool, BYTE>> Bytes;
    // 字节、掩码生成
    for (int i = 0; i < Feature.length(); i += 2)
    {
        if (Feature.substr(i, 2) == "??")
            Bytes.push_back(std::make_pair<bool, BYTE>(false, 0x0));
        else
            Bytes.push_back(std::make_pair<bool, BYTE>(true, static_cast<BYTE>(std::stol(Feature.substr(i, 2), 0, 16))));
    }
    // 校对
    PIMAGE_DOS_HEADER pImageDos = reinterpret_cast<PIMAGE_DOS_HEADER>(Module);
    PIMAGE_NT_HEADERS32 pImageNt = reinterpret_cast<PIMAGE_NT_HEADERS32>(Module + pImageDos->e_lfanew);
    ULONG Length = pImageNt->OptionalHeader.SizeOfImage - Bytes.size();
    for (int Offset = 0; Offset < Length; Offset++)
    {
        if (!IsBadHugeReadPtr(reinterpret_cast<PVOID>(Module + Offset), 1))
        {
            PBYTE pTempByte = reinterpret_cast<PBYTE>(Module + Offset);
            int ProofNum = 0;
            for (auto SingleByte : Bytes)
            {
                if (SingleByte.first && SingleByte.second != *pTempByte)
                    break;
                pTempByte++;
                ProofNum++;
            }
            if (ProofNum == Bytes.size() && *pTempByte)
                return Module + Offset;
        }
    }
    return NULL;
}

ChatGPT:

这段代码是用于在给定的模块内查找特定特征的函数。这是一种常见的在内存中搜索特定字节序列(也称为"签名"或"特征")的方法,这在逆向工程和游戏修改中非常常见。以下是对这段代码的详细解释:

  1. 函数 Find 接受两个参数:一个 ULONG_PTR 类型的 Module 和一个 std::string 类型的 FeatureModule 是要搜索的模块的基址,Feature 是要搜索的特征。

  2. 首先,函数通过遍历 Feature 字符串并删除所有空格来清理输入。

  3. 然后,函数检查 Feature 的长度。如果长度小于2或者长度不是偶数,函数返回0。这是因为特征通常以字节对的形式表示,每个字节由两个十六进制字符表示。

  4. 接下来,函数创建一个 Bytes 向量,用于存储特征的字节和掩码。如果特征中的某个字节是 "??",则将其视为通配符,并在 Bytes 向量中为其添加一个条目,其中 bool 值为 falseBYTE 值为 0x0。否则,将 bool 值设为 true,并将 BYTE 值设为特征中的字节。

  5. 函数接着获取模块的 DOS 头和 NT 头,以确定模块的大小。

  6. 然后,函数遍历模块的每个字节。对于每个字节,它检查是否可以读取该字节,然后比较该字节和 Bytes 向量中的相应字节。如果字节匹配(或者 Bytes 向量中的字节是通配符),则继续比较下一个字节。如果所有字节都匹配,函数返回匹配的字节序列的地址。

  7. 如果函数遍历了整个模块但没有找到匹配的字节序列,它返回 NULL

总的来说,这个函数的目的是在给定的模块中查找特定的字节序列。

挂哥写了几个好像内存地址一样的东西。

但是好像没有被调用过,笑死

偏移量

Naraka.h

#define Dx11Present 0x186EAF8
/*UnityPlayer.dll+463E20 - 48 8B 05 919C4001     - mov rax,[UnityPlayer.dll+186DAB8] { (22B203F94F0) }*/
#define GameWorld 0x188A5F8
/*UnityPlayer.dll+5B0400 - 48 8B 05 B1912D01     - mov rax,[UnityPlayer.dll+18895B8] { (22AC00EEB90) }*/
#define UWorld_Offset 0x7B64DF8
/*GameAssembly.dll+224D78F - 48 8B 05 22107E05     - mov rax,[GameAssembly.dll+7A2E7B8] { (258E558DC80) }*/

我的知识还停留在寻找特征码,偏移然后找到基址。有没有关系还不知道。 然后我问了一下ChatGPT大哥,他告诉我:

这些看起来像是内存地址,但不仅仅是内存地址。它们更具体地可以被称为偏移地址或者偏移量。这些地址是某些动态链接库(DLL)文件中特定功能或变量的位置。

在这个上下文中,#define 是 C/C++ 预处理器的一个指令,用于为各种数据类型定义别名,这里就是将这些内存地址定义为易于理解的名称。例如,Dx11PresentGameWorldUWorld_Offset 就是代表那些特定内存地址的别名。

后面的注释部分,比如 /*UnityPlayer.dll+463E20 - 48 8B 05 919C4001 - mov rax,[UnityPlayer.dll+186DAB8] { (22B203F94F0) }*/,显示的是在 UnityPlayer.dll 这个动态链接库文件的某个地址执行的指令。在这个例子中,该地址是 UnityPlayer.dll+463E20,执行的指令是 mov rax,[UnityPlayer.dll+186DAB8],这个指令表示将地址 UnityPlayer.dll+186DAB8 中的值移动到寄存器 rax 中。

这样的信息通常在逆向工程、内存修改或者调试中使用,这需要对汇编语言、内存管理和计算机结构有深入的理解。

下面就是挂哥找的一些偏移量

Naraka.h

#define Manager_Offset 0xB8
#define ActorBase_Offset 0x08
#define ActorList_Offset 0x58
#define ActorArray_Offset 0x18
#define ActorCount_Offset 0x20
​
#define ActorState 0x2B0
#define ActorState_Name 0x150
#define ActorState_PlayerType 0xC8
#define ActorState_TeamId 0xD8
#define ActorState_WeaponId 0xE8
​
#define ActorKit_Offset 0xC0
#define ActorKit_AnimPlable_Offset 0x218
#define ActionHitSimulateData_Offset 0xC8
#define HitReactionType_Offset 0xA0
#define AttackType_Offset 0x54
#define ZhenDao_Offset 0x58
#define ZhenDao2_Offset 0x88
​
#define ActorObjMsg_Offset 0xD0
#define ActorObjMsg_CurAnimPlayableAgent_Offset 0x68
#define ActorObjMsg_RuntimeLogicLayers_Offset 0x48
#define ActorObjMsg_RuntimeLogicLayersList_Offset 0x20
#define ActorObjMsg_CurPlayables_Offset 0x38
​
#define ActorRootQuaternion_Offset 0xA0

void UpNarakaData()函数中使用了偏移量

msimg32.cpp

m_GameMgr = Memory::ReadUlong64(Module::UnityPlayer + GameWorld);
    m_MatrixAddr = GetObjectFromList(Memory::ReadUlong64(m_GameMgr + 0x18), "Camera");
    if (m_MatrixAddr == NULL)
        return;
    m_MatrixAddr = Memory::ReadUlong64(m_MatrixAddr + 0x30);
    m_MatrixAddr = Memory::ReadUlong64(m_MatrixAddr + 0x18) + 0xDC;
​
    m_UWorld = Memory::ReadUlong64(Module::GameAssembly + UWorld_Offset);
    m_Manager = Memory::ReadUlong64(m_UWorld + Manager_Offset);
    m_ActorBase = Memory::ReadUlong64(m_Manager + ActorBase_Offset);
    g_LocalPlayer = Memory::ReadUlong64(m_ActorBase + ActorArray_Offset);
    if (g_LocalPlayer != NULL) {

m_GameMgr = Memory::ReadUlong64(Module::UnityPlayer + GameWorld);中的UnityPlayer是下面寻找程序入口里赋值的内存地址。

寻找程序入口

UINT StartThread(LPVOID pParam)的里面有两行

Module::UnityPlayer = (ULONG_PTR)GetModuleHandle(L"UnityPlayer.dll");
Module::GameAssembly = (ULONG_PTR)GetModuleHandle(L"GameAssembly.dll");

ChatGPT:

GetModuleHandle 是一个 Windows API 函数,它返回指定模块(在这里是 UnityPlayer.dllGameAssembly.dll)在内存中的起始地址。获取这些模块的起始地址可以在后续的代码中访问或者修改这些模块内部的数据和函数。

Module::UnityPlayerModule::GameAssembly 可能是全局变量,用来存储这两个模块的起始地址。这样就可以在程序的其他部分方便地访问这两个模块。

使用Process Explorer可以确认UnityPlayer.dll在内存中的位置

image-20230708201716232.png

判断状态

(这是最简单的那一个部分了属于是

最好笑还是中文编程,我不晓得为什么要写成中文。

我知道状态不好翻译成英语,当我看到挂哥把每种状态都转为状态码(?)的部分真的绷不住。

Naraka.h

enum EndureType{
    默认状态 = 5,
    受击状态 = 6,
    攻击状态 = 11,
    蓝霸体状态 = 20,
    紫霸体状态 = 21,
    雷刀状态 = 22,
    金霸体攻击 = 30,
    金霸体状态 = 31,
    金钟罩状态 = 50,
    撩原劲状态 = 50,
    风之牢笼状态 = 51,
    净天地状态 = 51,
    不祥之刃状态 = 51,
    深渊梦魇状态 = 51,
    寂静暗行状态 = 51,
    无尘传送状态 = 51,
    无尘已传送状态 = 53,
    赤练无明状态 = 52,
    岳山变身状态 = 55,
    岳山出场状态 = 80,
    岳山状态 = 41,
    岳山疾跑状态 = 42,
    金刚伏魔变身状态 = 55,
    金刚伏魔状态 = 80,
    金刚伏魔抓取状态 = 81,
    金刚伏魔攻击状态 = 90,
    冰心决状态 = 59,
    冰寒飞影状态 = 51,
    飞影剑状态 = 35,
    白刃取状态 = 21,
    武田瞬状态 = 51,
    灵机护体状态 = 15,
    悬壶济世状态 = 51,
};
​
enum EventSkills
{
    无操作 = 0,
    站振 = 1,
    跳振 = 2,
    闪振 = 3,
    蹲振 = 4,
    无敌帧 = 5,
};
​
enum WeaponId {
    无近武 = 0,
    长剑 = 1,
    太刀 = 2,
    阔刀 = 3,
    长枪 = 4,
    匕首 = 5,
    双节棍 = 6,
    双刀 = 7,
    棍 = 8,
};

相关文件:

  • msing32.cpp

核心判断逻辑在void IsZhenDaoThread()里面

while (true) {
        std::vector<ULONG_PTR>player = m_player;
        for (size_t i = 0; i < player.size(); i++) {
            ULONG_PTR m_Objects = player[i];
            ULONG_PTR m_ObjectEntity = GetObjectEntity(m_Objects);
            
            if (GetTeamId(m_Objects) == GetTeamId(g_LocalPlayer))//过滤本人队伍
                continue;
            m_ObjectPos = GetPlayerPos(m_ObjectEntity);
            float m_Distance = GetPlayerDistance(g_LocalPos, m_ObjectPos);
​
            float m_A = 获取人物朝向(GetRootComponent(m_ObjectEntity));
            float m_B = VectorToRotationYaw(FindLookAtVector(m_ObjectPos, g_LocalPos));
            float m_Direction = abs(计算角度差(m_A, m_B));
​
            float m_LocalA = 获取人物朝向(GetRootComponent(g_LocalEntity));
            float m_LocalB = VectorToRotationYaw(FindLookAtVector(g_LocalPos, m_ObjectPos));
            float m_LocalDirection = abs(计算角度差(m_LocalA, m_LocalB));
            
            m_ObjectWeap = GetObjectWeap(m_Objects);
            m_ObjectWeapTime = GetWeapXuliTime(m_Objects);
​
            if (g_LocalWeap == WeaponId::无近武 || m_ObjectWeap == WeaponId::无近武)
                continue;
            

很好笑的是两人都没武器的情况下会跳过判断,虽然是正确的看到这个判断的时候还是觉得好好笑。

if (g_LocalWeap == WeaponId::无近武 || m_ObjectWeap == WeaponId::无近武)
                continue;

这里有两段

std::vector<ULONG_PTR>player = m_player;
        for (size_t i = 0; i < player.size(); i++) {

真的,获取到所有人物状态的时候真的可以为所欲为。

DWORD GetActionName(...)是一个接受多个值后,判断这种情况下需要做出什么样动作的函数。

DWORD GetActionName(
    ULONG_PTR m_Object,
    FLOAT m_Distance,
    DWORD m_Weapid,
    FLOAT m_Direction,
    DWORD m_HitReactionType,
    DWORD m_AttackType) {
​
    wchar_t AttackName[128] = { NULL };
    ULONG_PTR m_ActorKit, m_GroundStatus = NULL;
    ULONG_PTR AnimPlable, BasePlayableTrackDriver, ObjectMessenger, EventTrackSet, ActionName = NULL;
​
    m_ActorKit = Memory::ReadUlong64(m_Object + ActorKit_Offset);
    if (m_ActorKit != NULL){
        AnimPlable = Memory::ReadUlong64(m_ActorKit + ActorKit_AnimPlable_Offset);
        if (AnimPlable != NULL) {
            BasePlayableTrackDriver = Memory::ReadUlong64(AnimPlable + 0x68);
            if (BasePlayableTrackDriver != NULL) {
                ObjectMessenger = Memory::ReadUlong64(BasePlayableTrackDriver + 0x28);
                if (ObjectMessenger != NULL) {
                    EventTrackSet = Memory::ReadUlong64(Memory::ReadUlong64(ObjectMessenger + 0x30) + 0x20);
                    if (EventTrackSet != NULL) {
                        ActionName = Memory::ReadUlong64(EventTrackSet + 0x10);
                        if (ActionName != NULL && m_HitReactionType == 蓝霸体状态 || m_HitReactionType == 22) {
                            Memory::ReadMemory(ActionName + 0x14, &AttackName, sizeof(AttackName));
                            if (m_Weapid == WeaponId::长剑) {
                                if (m_Distance <= 4.5f && m_Direction <= 45.0f) {
                                    if (!wcscmp(AttackName, L"male_sw_attack_light_03")) { return EventSkills::蹲振; } /*左键三段*/
                                    if (!wcscmp(AttackName, L"male_sw_attack_heavy_03")) { return EventSkills::蹲振; } /*右键三段*/
                                    if (!wcscmp(AttackName, L"female_sw_attack_light_03")) { return EventSkills::蹲振; } /*左键三段*/
                                    if (!wcscmp(AttackName, L"female_sw_attack_heavy_03")) { return EventSkills::蹲振; } /*右键三段*/
                                }
                                if (m_Distance <= 8.5f && m_Direction <= 60.0f) {
                                    if (!wcscmp(AttackName, L"male_sw_attack_hold_light_01") || !wcscmp(AttackName, L"female_sw_attack_hold_light_01")) {
                                        if (m_Distance <= 4.5f) {
                                            return EventSkills::蹲振;

我注意到了,这里使用了wcscmp对比字符串判断目前状态,是不是可以理解为,底层里状态也是使用字符串进行判断的?(好好笑)一般用数字代表状态消耗的资源更少。 male_sw_attack_light_03:左右键分轻重,但分男女是真没想到。

模拟键盘输入

相关文件:

  • Moukdbdrv.h

  • DrvClass.cpp

创建系统驱动文件

//bool DrvClass::LoadNtDrv()
WriteBinFile("C:\\Windows\\system32\\drivers\\MoukdbDrv.sys", DrvBin_sys, sizeof(DrvBin_sys));

向系统注册一个名为MoukdbDrv的服务

//创建驱动所对应的服务
    hServiceDDK = CreateServiceA(hServiceMgr,
        "MoukdbDrv", //驱动程序的在注册表中的名字  
        "MoukdbDrv", // 注册表驱动程序的 DisplayName 值  
        SERVICE_ALL_ACCESS, // 加载驱动程序的访问权限  
        SERVICE_KERNEL_DRIVER,// 表示加载的服务是驱动程序  
        SERVICE_DEMAND_START, // 注册表驱动程序的 Start 值  
        SERVICE_ERROR_IGNORE, // 注册表驱动程序的 ErrorControl 值  
        "C:\\Windows\\system32\\drivers\\MoukdbDrv.sys", // 注册表驱动程序的 ImagePath 值  
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);

.sys是怎么样的一个文件靠目前知识还不清楚。

尝试

逃避学习C++,我选择Rust。

分部实现

  1. 找出UnityPlayer.dllGameAssembly.dll在内存中的地址

    1. 获取NarakaBladepoint.exe的PID

    2. 列出PID进程加载的所有DLLs

    3. 找出UnityPlayer.dll或者GameAssembly.dll在内存中的地址

  2. 添加一个偏移量尝试读取

2023/07/10

完全不知道这个外挂是如何获取这些内存数据和地址的, 根据ChatGPT的说法,WinAPI里的GetModuleHandleW是返回当前自己DLL的内存地址。 不是从外部获取的?不太清楚。

ducat(和尚朋友)让我逛逛52pojie,我想着看看吧,然后发现一个帖子: 萌新逆向学习笔记——远程线程注入DLL 哟——,居然可以让“别人的exe”执行自己的DLL,相当于外部无法获取,但是把内鬼(DLL)送进去,通风报信。

这样子的话可能无法单纯使用Rust来写了,可恶。

2023/07/12

Bro wtf,根据上面老哥给的测试代码,就这么让我注入了。网易我真的...还是说游戏中检测我并不知道?

image-20230712111300073.png

虽然我知道外挂检测原理大概就是对外挂进行特征码扫描,匹配到数据库里的特征码就标记为外挂,但是WTF Bro,注入是有痕迹的,这点都不扫描吗?

2023/07/13

网易真的我哭死,我把挂哥写的源代码去掉验证部分试了试,直接注入进去了。绝对信任玩家,我哭死。

现在遇到一个问题是无法卸载DLL,DLL会一直在列表里,我有点担心。等弄懂了如何卸载再去抄获取状态那一部分的代码。看看能不能用。

文章作者: Lien
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 彼方
喜欢就支持一下吧