UE4外挂实现分析-PC端-附源码

UE4外挂实现分析-PC端

游戏分析

分析工具:

Cheat Engine 7.5

x64dbg

IDA Pro
参考文章:

UE4逆向笔记之GWORLD GName GameInstance - 小透明's Blog

【项目源码下载】https://download.csdn.net/download/Runnymmede/90079718

本次分析的游戏使用UE4.22引擎开发,外挂实现功能有透视和自瞄,两项功能都基于游戏内玩家对象和敌人对象的坐标位置实现。UE4游戏内对象的结构如下图所示

图片中的对象偏移与UE引擎版本相关,存在误差。

根据上图的关系,游戏中所有的对象都挂在UWorld下面,通过UWorld->GameInstance->ULocalPlayer->LocalPlayer->PlayerController->Actor可以获取到游戏玩家的Actor对象,进而获取玩家的坐标等信息

通过PWorld->ULevel->ActorCountPWorld->ULevel->ActorArray可以遍历游戏中所有的Actor对象,包括敌人的Actor对象,进而获取敌人坐标信息,在一局游戏中,PWorld指针与UWorld相同

CE分析UWorld

开启游戏使用CE打开游戏进程

寻找游戏内能直接获取的与玩家信息有关的详细数据,游戏中子弹数量能够直接查看到准确数值,并且方便控制,因此使用CE查找子弹数量的地址

首先搜索准确的32位整数50

开枪减少子弹数量,继续搜索48

只剩两个地址,修改这两个地址处的值,查看游戏内子弹数量是否发生变化,

可以确定子弹数量储存在0x1E3EDF40684地址处,对该地址进行指针分析

根据GameInstancePlayerController的偏移关系0x38 -> 0x0 -> 0x30过虑到如下指针链

其中存在条指针链

c 复制代码
"ShooterClient.exe"+02F6E6E8->0xD80->0x38->0x0->0x30->0x3B0->0x778->0x584
"ShooterClient.exe"+02F71060->0x160->0x38->0x0->0x30->0x3B0->0x778->0x584

因此可以分析出UWorldShooterClient.exe+02F6E6E8ShooterClient.exe+02F71060

c 复制代码
UWorld = [ShooterClient.exe+0x02F71060]
GameInstance = [UWorld+0x160]
ULocalPlayer = [GameInstance+0x38]
LocalPlayer=[ULocalPlayer]
PlayerController = [LocalPlayer+0x30]
PlayerActor = [PlayerController+03B0]ReadProcessMemory(hProcess, (LPVOID)((BYTE*)baseAddr + 0x2E6E0C0), (LPVOID)&GName, 8, NULL);

继续使用浮点数模糊搜索玩家坐标、视角信息等,由于已经确定玩家子弹数量地址,因此可以缩小搜索范围在0x1E3EDF40684附近

得到如下指针信息

c 复制代码
bullet = [[PlayerActor+0x778]+0x584]
posi_x = [[PlayerActor+0x3A0]+0x1A0]
posi_y = [[PlayerActor+0x3A0]+0x1A4]
posi_z = [[PlayerActor+0x3A0]+0x1A8]
persp_x = [[PlayerActor+0x3A0]+0x154]
persp_y = [[PlayerActor+0x3A0]+0x174]

基于上述信息,还能确定ULevel

c 复制代码
ULevel = [UWorld+0x30]

ActorCountActorArray的偏移可以使用CE的结构体分析功能

经过分析,确定ActorCountActorArray的偏移

c 复制代码
ActorCount = [ULevel+0xA0]
ActorArray = [ULevel+0x98]

遍历ActorArray可以获得游戏内所有的Actor对象,包含了敌人对象,但还需要识别是否为敌人,所以还需要查找对象的Name

CE分析GName

UE4.23以下版本使用的GName算法如下

c 复制代码
BYTE *GetName(int id)
{
    int idx0 = id / 0x4000;
    int idx1 = id % 0x4000;
    BYTE *NameArray = [GName + idx0 * 8];
    BYTE *Name = [NameArray + idx1 * 8] + 0xC;
    return Name;
}

使用CE搜索进程内存,查找关键字符串ByteProperty

如果上一个字符串为None,则表示搜索到了正确位置,此处为游戏对象的Name表

由于ByteProperty字符串id为1,可以根据GetName算法逆推GName

搜索地址0x1E3D5EA0024-0xC

搜索0x1E3D5E80008-1*8

搜索0x1E3D5E70080

可以确定GName为ShooterClient.exe+2D310B0ShooterClient.exe+2E6E0C0

编写代码验证上述分析的偏移

c 复制代码
int main()
{
    LPCWSTR procName = L"ShooterClient.exe";
    DWORD dwPID;
    HANDLE hProcess;
    LPVOID baseAddr;
    dwPID = getDwPidByName(procName);
    printf("PID: %d\n", dwPID);
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
    if (hProcess == NULL)
    {
        printf("open process failed\n");
        return 0;
    }
    baseAddr = getModuleBase(dwPID);
    printf("proc base: 0x%llx\n", baseAddr);

    LPVOID UWorld;
    LPVOID GName;
    LPVOID GameInstance;
    LPVOID ULocalPlayer;
    LPVOID LocalPlayer;
    LPVOID PlayerController;
    LPVOID PlayerActor;
    LPVOID PlayerPosition;
    LPVOID ULevel;
    DWORD ActorCount;
    LPVOID ActorArray;
    // 读取UWorld
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)baseAddr + 0x02F71060), (LPVOID)&UWorld, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)UWorld + 0x160), (LPVOID)&GameInstance, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)GameInstance + 0x38), (LPVOID)&ULocalPlayer, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)ULocalPlayer, (LPVOID)&LocalPlayer, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)LocalPlayer + 0x30), (LPVOID)&PlayerController, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PlayerController + 0x3B0), (LPVOID)&PlayerActor, 8, NULL);
    printf("\n");
    printf("UWorld: 0x%llx\n", UWorld);
    printf("GameInstance: 0x%llx\n", GameInstance);
    printf("ULocalPlayer: 0x%llx\n", ULocalPlayer);
    printf("LocalPlayer: 0x%llx\n", LocalPlayer);
    printf("PlayerController: 0x%llx\n", PlayerController);
    printf("PlayerActor: 0x%llx\n", PlayerActor);
    printf("\n");
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)UWorld + 0x30), (LPVOID)&ULevel, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)ULevel + 0xA0), (LPVOID)&ActorCount, 4, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)ULevel + 0x98), (LPVOID)&ActorArray, 8, NULL);
    // 读取玩家坐标
    FLOAT posi[3];
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PlayerActor + 0x3A0), (LPVOID)&PlayerPosition, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PlayerPosition+0x1A0), (LPVOID)posi, 0xC, NULL);
    printf("\n");
    printf("posi: [%f, %f, %f]\n", posi[0], posi[1], posi[2]);
    // 读取玩家视角
    FLOAT persp_x, persp_y;
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PlayerPosition + 0x154), (LPVOID)&persp_x, 0x4, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PlayerPosition + 0x174), (LPVOID)&persp_y, 0x4, NULL);
    printf("perspective: [%f, %f]\n", persp_x, persp_y);
    printf("\n");
    printf("ULevel: 0x%llx\n", ULevel);
    printf("ActorCount: %d\n", ActorCount);
    printf("ActorArray: 0x%llx\n", ActorArray);
    // 读取GName
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)baseAddr + 0x2E6E0C0), (LPVOID)&GName, 8, NULL);
    printf("GName: 0x%llx\n", GName);
    printf("\n");
    // 遍历ActorArry
    for (DWORD i = 0; i < ActorCount; i++)
    {
        LPVOID AActor;
        DWORD id;
        LPVOID PNameArray;
        LPVOID PName;
        CHAR name[0x100];
        ReadProcessMemory(hProcess, (LPVOID)((BYTE*)ActorArray + i * 8), (LPVOID)&AActor, 8, NULL);
        if (ReadProcessMemory(hProcess, (LPVOID)((BYTE*)AActor + 0x18), (LPVOID)&id, 4, NULL))
        {
            ReadProcessMemory(hProcess, (LPVOID)((BYTE*)GName + (id / 0x4000) * 8), (LPVOID)&PNameArray, 8, NULL);
            ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PNameArray + (id % 0x4000) * 8), (LPVOID)&PName, 8, NULL);
            if (ReadProcessMemory(hProcess, (LPVOID)((BYTE*)PName + 0xC), (LPVOID)name, 0x100, NULL))
            {
                printf("%d: %s\n", i, name);
            }
        }
    }
    return 0;
}

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=assets%2F2023-11-12-21-35-26-image.png\&pos_id=img-xid30bLc-17332748937


玩家的坐标、视角都已经找到了,并且Actor对象的name识别也成功了,猜测BotPawn_C为机器人玩家的Actor对象,猜测其坐标算法与玩家相同,CE结构体分析

发现按照Actor->0x3A0->0x1A0的偏移确实可以找到机器人坐标

机器人玩家坐标计算如下

c 复制代码
posi_x = [[AActor+0x3A0]+0x1A0]
posi_y = [[AActor+0x3A0]+0x1A4]
posi_z = [[AActor+0x3A0]+0x1A8]

修改以下代码可以获取所有机器人玩家坐标

c 复制代码
// printf("%d: %s\n", i, name);
if (!strcmp(name, "BotPawn_C"))
{
    LPVOID botPosition;
    FLOAT botPosi[3];
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)AActor + 0x3A0), (LPVOID)&botPosition, 8, NULL);
    ReadProcessMemory(hProcess, (LPVOID)((BYTE*)botPosition + 0x1A0), (LPVOID)botPosi, 0xC, NULL);
    printf("bot: [%f, %f, %f] \n", botPosi[0], botPosi[1], botPosi[2]);
}

至此,已实现获取玩家坐标、玩家视角、敌人坐标的目标,对坐标数据进行数学处理,使用GUI工具绘制到屏幕上,即可实现透视效果,同样可以通过计算玩家视角需要转动的角度,实现自瞄的功能。

外挂分析
VMP脱壳DUMP

入口push call,典型vmp,使用API断点回溯确定程序逻辑是否加密

根据外挂实现原理,读取进程需要使用ReadProcessMemoryAPI,而这之前还需要使用OpenProcessAPI打开进程,OpenProcess需要的参数为进程PID,但是该外挂程序不需要提供PID,因此该外挂运行早期会使用某些方式获取目标游戏的PID,需要利用Thelp32功能,对CreateThelp32Snapshot下断点

ScyllaHide过VMP反调试

3处nop断下后F9运行

Thelp32断下,第一次是VMP反调试调用的,忽略掉,F9运行

Thelp32第二次断下,分析调用栈回溯

发现此处为典型的msvc编译器主函数调用入口

因此该层为start,可以确定程序逻辑未加密,向上找到程序入口点OEP

对OEP下断点,取消Thelp32断点,重新运行程序,3次nop之后OEP断下

使用Scylla插件DUMP外挂内存

使用Fix Dump修复DUMP文件的导入数据

删除带X的FThunk

运行恢复后的DUMP文件hack_dump_SCY.exe,外挂功能正常

外挂脱壳完成

外挂逻辑分析

使用IDA Pro打开脱壳后的hack_dump_SYC.exe分析逻辑,动态调试之后对主函数注释如下,程序中的字符串大部分被加密,算法比较简单,但是使用动态调试也可以直接得到解密之后的字符串

主要功能就是打开游戏进程,获取游戏加载地址,创建窗口等操作,API断点回溯时断下的位置在GetPidByName函数中

作弊的主要逻辑在CheatProc过程函数中

showGUI函数调用imGUI库在屏幕上显示窗口

这里使用GetAsyncKeyStateAPI判断HOME键是否被按下,HOME按下之后切换GUI显示状态

CheatMain里第一个和最后一个函数是用来刷新屏幕上显示的文本标签的,可以直接忽略

继续进入到cheatMain函数,这里是主要的外观逻辑实现

首先使用ReadProcessMemoryAPI读取进程内存,获取UWorldGName等数据,偏移的计算在游戏分析部分得到的偏移基本相同,对所有的全局变量进行注释,方便后续分析

遍历游戏中所有的Actor对象,并且获取对象的name,与BotPawn_C进行比较,判断该AActor是否为机器人

是机器人时读取机器人坐标,根据玩家坐标、窗口分辨率计算是否在屏幕显示范围内,是的话则会在屏幕上显示玩家与机器人的距离

此部分还计算了机器人在屏幕上显示坐标与窗口中心的距离,循环结束后保持与屏幕中心距离最近的机器人坐标,用于自瞄功能

自瞄功能同样使用GetAsyncKeyState判断按键是否按下,这里判断的是鼠标右键,当鼠标右键按下时,修改玩家视角使其瞄向距离屏幕中心最近的机器人

至此,外挂程序功能分析完成。

【项目源码下载】https://download.csdn.net/download/Runnymmede/90079718

相关推荐
graceyun3 小时前
C语言进阶习题【1】指针和数组(4)——指针笔试题3
android·java·c语言
2401_897916068 小时前
Android 自定义 View _ 扭曲动效
android
天花板之恋8 小时前
Android AutoMotive --CarService
android·aaos·automotive
susu108301891112 小时前
Android Studio打包APK
android·ide·android studio
2401_8979078612 小时前
Android 存储进化:分区存储
android
Dwyane0319 小时前
Android实战经验篇-AndroidScrcpyClient投屏一
android
FlyingWDX19 小时前
Android 拖转改变视图高度
android
_可乐无糖19 小时前
Appium 检查安装的驱动
android·ui·ios·appium·自动化
一名技术极客1 天前
Python 进阶 - Excel 基本操作
android·python·excel
我是大佬的大佬1 天前
在Android Studio中如何实现综合实验MP3播放器(保姆级教程)
android·ide·android studio