UE5 逆向笔记

2110 字
11 分钟
UE5 逆向笔记

UE5 逆向笔记#

说明:本笔记由原始记录整理而来,尽量保留原有结论、偏移、定位关键词和代码片段。
偏移会随游戏更新失效,实际使用时优先重新定位并验证。

目录#

SCUM 当前偏移#

GName : 0x75BFA40
GObject : 0x75FBF80
GWorld : 0x7741DE0
Matrix : 0x7274AD0

补充记录:

  • LineTraceSingle:先在 VS Code 中查看 SDK 偏移,再去 IDA 定位函数地址。
  • find_obj:通过项目中提供的字符串,再去 IDA 定位。
  • 游戏更新后很多偏移都会失效,优先尝试重新定位或修改偏移。

快速定位关键词#

FName : IntProperty
GObject : Max UObject count is invalid. It must
GWorld : All Windows Closed
Matrix : CanvasObject

全局对象定位#

GName / FNamePool#

目标函数:

static FNamePool& GetNamePool()

主要思路:

  1. 找到 FNamePool
  2. 通过返回的 FNamePool 定位 GName
  3. FNamePool 内部存在 FNameEntryAllocator Entries;
  4. FNameEntryAllocator 底部存在名称块指针数组,是解析名称的关键。

相关截图:

GName 定位 1
GName 定位 1

GName 定位 2
GName 定位 2

FNameEntryAllocator
FNameEntryAllocator

GObject / GUObjectArray#

相关位置:

void UObjectBaseInit()
TUObjectArray ObjObjects;

主要思路:

  1. 先找到 InitUObject(),这个函数会初始化全局 GObject
  2. 通过 IDA 找到的地址还需要 +8
  3. 重点结构为 TUObjectArray / FUObjectItem
// GObject 关键数据结构
FUObjectItem** Objects;
FUObjectItem* PreAllocatedObjects;

相关截图:

GObject 定位 1
GObject 定位 1

GObject 定位 2
GObject 定位 2

GObject 结构
GObject 结构

GWorld#

目标函数:

void UGameEngine::Tick(float DeltaSeconds, bool bIdleMode)

关键结构:

TArray<TObjectPtr<AActor>> Actors;

主要链路:

UWorld -> ULevel(0x30) -> TArray<AActor*> Actors(0x98)

相关截图:

GWorld 定位 1
GWorld 定位 1

GWorld 定位 2
GWorld 定位 2

源代码对应位置
源代码对应位置

GWorld 结构
GWorld 结构

Matrix / ViewProjectionMatrix#

目标函数:

void UDisplayClusterViewportClient::Draw(FViewport* InViewport, FCanvas* SceneCanvas)
static UCanvas* GetCanvasByName(FName CanvasName)

定位思路:

  1. 找到关键函数 GetCanvasByName
  2. 找到 UCanvas 对象。
  3. UCanvas 中定位 ViewProjectionMatrix
  4. 程序会调用两次 GetCanvasByName,第二次调用才是目标。
  5. 在 CE 中查看第二个指针,通过晃动视角观察变动值。
  6. 当前记录中矩阵偏移为 0x280,内存中按单浮点查看。

UCanvas 相关成员:

public:
FCanvas* Canvas;
FSceneView* SceneView;
FMatrix ViewProjectionMatrix;
FQuat HmdOrientation;

相关截图:

Matrix 定位 1
Matrix 定位 1

GetCanvasByName
GetCanvasByName

UCanvas
UCanvas

Matrix CE 查看
Matrix CE 查看

UE5 核心数据结构#

TArray#

template<typename T>
struct TArray {
T data;
int number;
int maxnumber;
};

FName#

通过 AActor + 0x18 可以看到 Name 占用 0x8 字节。

struct FName {
int index;
int number;
};

UE5 源码中的相关结构:

private:
FNameEntryId ComparisonIndex; // 定位全局名称表,4 字节
#if !UE_FNAME_OUTLINE_NUMBER
uint32 Number; // 区分相同名称,4 字节
#endif
#if WITH_CASE_PRESERVING_NAME
FNameEntryId DisplayIndex; // 通常不参与编译
#endif

字段含义:

  • ComparisonIndex:用于定位全局名称表。
  • Number:用于区分名称表中相同名称的不同实例。
  • DisplayIndex:通常不参与编译。

UObject#

在虚幻引擎中,所有对象都会继承 UObject

UObjectBase 中存在 FName 字段,用来保存对象名称。

UObjectBase() :
ClassPrivate(NoInit),
NamePrivate(NoInit),
OuterPrivate(NoInit)
{
}

关键成员:

EObjectFlags ObjectFlags; // 4 bytes
int32 InternalIndex; // 4 bytes
ObjectPtr_Private::TNonAccessTrackedObjectPtr<UClass> ClassPrivate; // 8 bytes
FName NamePrivate; // 8 bytes

记录:

  • NamePrivate 在对象内的偏移通常为 0x18
  • 通过 SDK Dump 观察,游戏中的 UObject 大小约为 0x28
  • CE 中对象开头的指针类型通常是虚函数表指针。

相关截图:

UObject SDK Dump
UObject SDK Dump

AActor 与角色继承#

僵尸类继承关系:

ABP_Zombie2_C
-> AZomBie2
-> AConZcharacter
-> ACharacter
-> APawn
-> AActor
-> UObject

类职责记录:

  • AActor:实现移动行为,提供灵活性,可用于投掷物、移动平台、门、物理对象等。
  • APawn:最简单的可控制移动对象,可用于载具、飞行器等。
  • ACharacter:拥有复杂移动逻辑,例如走、跳、奔跑、攀爬,常用于玩家和 NPC 角色。

常见组件:

CharacterMovementComponent 处理所有移动相关逻辑
CapsuleComponent 碰撞检测、物理模拟
SkeletalMeshComponent 骨骼网格相关组件
SkinnedMeshComponent 包含骨骼具体坐标数组

AActor 中的重要成员:

struct USceneComponent* RootComponent; // 0x130(0x08)

人物坐标信息通常从 RootComponent 中继续查找。

相关截图:

AActor RootComponent
AActor RootComponent

名称解析:FName 与全局名称表#

全局名称表相关结构:

// FNameEntryAllocator 全局名称表内存结构
// mutable FRWLock Lock;
// uint32 CurrentBlock = 0;
// uint32 CurrentByteCursor = 0;
// uint8* Blocks[FNameMaxBlocks] = {};

源码中的解析函数:

FNameEntry& Resolve(FNameEntryHandle Handle) const
{
// Lock not needed
return *reinterpret_cast<FNameEntry*>(Blocks[Handle.Block] + Stride * Handle.Offset);
}

解析步骤:

  1. 自定义 FName
struct FName {
int index;
int number;
};
  1. 从源码确认 StrideHandle.BlockHandle.Offset
  2. 通过 GName 获取 Blocks
  3. 通过对象内的 NamePrivate 获取 FName 值。
  4. 根据 Handle.Block 定位名称块。
  5. 计算 Blocks[Handle.Block] + Stride * Handle.Offset
  6. 取出 FNameEntry 后读取 AnsiName

示例伪代码:

auto Stride = alignof(FNameEntry);
auto EntryAddress = Blocks[Handle.Block] + Stride * Handle.Offset;
FNameEntry* NameEntry = reinterpret_cast<FNameEntry*>(EntryAddress);
return std::string(NameEntry->AnsiName, NameEntry->Header.Len);

GWorld + 0x18 示例:

GWorld NamePrivate
GWorld NamePrivate

调试示例:

char tst[] = {
60, 2, 77, 97, 105, 110, 77, 101, 110, 117,
82, 4, 79, 110, 67, 111, 110
};
auto Stride = alignof(FNameEntry);
printf("Stride [+]: %x\n", Stride);
FNameEntryHandle Handle = { 0x000A8726 }; // GWorld NamePrivate 的值
auto Stridel = Stride * Handle.Offset;
printf("Stride [+]: %x\n", Stridel);
printf("Handle.Block [+]: %x\nHandle.Offset [+]: %x\n", Handle.Block, Handle.Offset);
auto a = 0x23c;
FNameEntryHeader header = { 0x23c };
memcpy(&header, &a, 2);
FNameEntry* Fname_entry = (FNameEntry*)tst;
printf("%s\n", Fname_entry->AnsiName);
std::cout.write(Fname_entry->AnsiName + 2, 6);

骨骼坐标#

相关截图:

骨骼分析
骨骼分析

分析记录:

  1. 第一个函数与 FName 结构有关。
  2. 通过 if 判断是否获取骨骼成功。
  3. 之后通过函数获取结构体中的骨骼数组。
  4. 最后使用 GetOrigin() 取得坐标。

重要函数:

GetBoneMatrix(BoneIndex);
WorldTM.GetOrigin();

实现要点:

  • ToMatrixWithScale 可以参考 UE4 源码。
  • XMLoadFloat4x4A 可以在 UE4 源码中搜索。
  • XMMatrixMultiply 同上。

骨骼坐标读取示例:

FVector3D Mesh::GetBoneArray(int Index)
{
// GetComponentSpaceTransforms()[ParentBoneIndex].ToMatrixWithScale()
int CurrentReadComponentTransforms = Memory::Read<int>(ptr + Offset::CurrentReadComponentTransforms);
Tarary<ULONG64> BoneArray{};
if (CurrentReadComponentTransforms) {
BoneArray = Memory::Read<Tarary<ULONG64>>(ptr + Offset::BonArray2);
} else {
BoneArray = Memory::Read<Tarary<ULONG64>>(ptr + Offset::BonArray1);
}
FTransform bonetf = Memory::Read<FTransform>(BoneArray.data + (Index * sizeof(FTransform)));
// GetComponentTransform()
FTransform ComponentTf = this->GetComponentTransform();
FMatrix BoneMatrix = FMatrix::ToMatrixWithScale(bonetf);
FMatrix ComponentMatrix = FMatrix::ToMatrixWithScale(ComponentTf);
FMatrix result = FMatrix::VectorMatrixMultiply(&BoneMatrix, &ComponentMatrix);
return FVector3D(FMatrix::GetOrigin(result));
}

骨骼索引:

int BoneIndexArray[] = {
98, 3, 2, 9, 10, 97, // 上半身
27, 22, 21, 20, 42, 43, 44, 45, // 下半身
1, 16, 17, 20 // 脊椎
};

绘制骨骼线示例:

void BrawBoneArray(AActor* obj, int start, int end)
{
FVector4D point2{};
for (int i{ start }; i < end; i++) {
FVector4D point1{};
FVector3D pos3 = obj->GetMesh().GetBoneArray(BoneIndexArray[i]);
if (Engine::WorldToScrren(pos3, point1, false)) {
if (i != start) {
if (point2.x == 0.0f || point2.y == 0.0f ||
point1.x == 0.0f || point1.y == 0.0f) {
continue;
}
ImGui::GetForegroundDrawList()->AddLine(
{ point2.x, point2.y },
{ point1.x, point1.y },
IM_COL32(255, 0, 0, 255)
);
}
}
point2 = point1;
}
}

可视判断:LineTraceSingle#

骨骼遮挡 / 可视判断函数:

LineTraceSingle

函数原型:

bool LineTraceSingle(
struct UObject* WorldContextObject,
struct FVector Start,
struct FVector End,
enum class ETraceTypeQuery TraceChannel,
bool bTraceComplex,
struct TArray<struct AActor*>& ActorsToIgnore,
enum class EDrawDebugTrace DrawDebugType,
struct FHitResult& OutHit,
bool bIgnoreSelf,
struct FLinearColor TraceColor,
struct FLinearColor TraceHitColor,
float DrawTime
);

SDK 记录:

Function Engine.KismetSystemLibrary.LineTraceSingle
(Final|Native|Static|Public|HasOutParms|HasDefaults|BlueprintCallable)
@ game+0x46ea700

自瞄角度计算#

基础角度计算:

FVector2D Aim_xy{};
FMinimalViewInfo info = Engine::GetplayerCamera(); // 获取 CameraCachePrivate
FVector3D diff = pos - info.Location; // 目标位置 - 摄像机位置
Aim_xy.x = atan2(diff.z, sqrt(diff.x * diff.x + diff.y * diff.y)) * 180 / 3.1415926;
Aim_xy.y = atan2(diff.y, diff.x) * 180 / 3.1415926;

调试记录:

  • 问题点:FVector3D pos = AimPlayer.GetMesh().GetBoneArray(86);
  • 现象:pos 锁定的是玩家当前坐标,而不是僵尸坐标。
  • 后续手动修改僵尸值后,成功锁头。
  • 再次调试发现问题出在 AutoAim::SetAimPlayer(actor);
  • 原因:没有把代码写入过滤逻辑,大量未过滤的 actor 被传入 AutoAim,最先传入的是玩家本身。
  • 使用以下输出确认:
printf("%s\n", Engine::GetName(AimPlayer.GetObjFName()).c_str());

输出结果:

BP_Prisoner_C

结论:需要先完成目标过滤,避免玩家自身被设置为目标。

Hook 与远程线程#

基本流程:

  1. 在目标进程中申请一段内存。
  2. 清空申请的内存。
  3. 构造 shellcode。
  4. 执行前保存寄存器,执行后还原寄存器。
  5. 使用写入函数把参数和 shellcode 写入目标内存。
  6. 通过远程线程执行。
  7. 等待线程结束并关闭句柄。
  8. 使用 VirtualFreeEx 释放申请的远程内存。

示例:

ULONG64 adr = Memory::alloc_mem(0x1000);
Memory::mem_zeor(adr, 0x1000);
// 构造 shellcode 后,以 adr 为基地址写入参数和代码。
// 写入时需要注意系统函数调用约定。
HANDLE handle = create_remote_thread(adr);
WaitForSingleObject(handle, INFINITE);
CloseHandle(handle);
BOOL freed = VirtualFreeEx(Memory::GetPid(), (LPVOID)adr, 0, MEM_RELEASE);
if (!freed) {
DWORD err = GetLastError();
printf("VirtualFreeEx failed: %u\n", err);
}

待整理 / 待补充#

  • ImGui 章节目前只有标题,后续可补充菜单、绘制、字体、热键、窗口样式等内容。
  • LineTraceSingle 可以补充实际调用封装、参数枚举与可视判断返回逻辑。
  • GName 可以补充完整的 FNameEntryHeader、宽字符 / 窄字符判断与读取逻辑。
  • GObject 可以补充对象遍历、对象数量读取、无效对象过滤。
  • GWorld 可以补充 GameInstanceLocalPlayerPlayerControllerPawnCameraManager 链路。
  • 骨骼章节可以补充常用骨骼 ID 表,并按头部、躯干、手臂、腿部分组。
  • 偏移建议按游戏版本或更新时间建立表格,避免新旧记录混在一起。

联系方式: https://t.me/+pyLg3pcwqOs4MGFl

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

UE5 逆向笔记
https://c7lz11.cn/posts/ue5/
作者
Changed
发布于
2026-03-11
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Changed
Hello, I'm Changed.
热爱技术,喜欢分享,喜欢音乐。
公告
图床使用的是cloudflare可能加载有些慢,可以使用魔法。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
7
分类
1
标签
12
总字数
4,585
运行时长
0
最后活动
0 天前

文章目录