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

相关推荐
一起搞IT吧1 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@1 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组3 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19963 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸3 小时前
Flutter 生命周期完全指南
android·flutter·ios
阿幸软件杂货间3 小时前
阿幸课堂随机点名
android·开发语言·javascript
没有了遇见3 小时前
Android 渐变色整理之功能实现<二>文字,背景,边框,进度条等
android
没有了遇见4 小时前
Android RecycleView 条目进入和滑出屏幕的渐变阴影效果
android
站在巨人肩膀上的码农5 小时前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗
呼啦啦--隔壁老王5 小时前
屏幕旋转流程
android