Dumper-7 偏移推断算法
目录
- 项目概述
- 整体架构
- 核心工具函数
- GObjects 全局对象数组查找
- UObject 成员偏移查找
- FName 系统检测
- UStruct 成员偏移查找
- UFunction 成员偏移查找
- UClass 成员偏移查找
- Property 成员偏移查找
- 特殊属性类型偏移查找
- FField 系统偏移查找
- SDK 相关偏移查找
- 算法设计原则总结
1. 项目概述
1.1 什么是 Dumper-7
Dumper-7 是一个用于 Unreal Engine 4/5 游戏的 SDK 生成器。它通过 DLL 注入的方式,在运行时自动分析游戏进程内存,推断出 UE4 引擎核心结构体的内存布局,最终生成可用的 C++ SDK。
1.2 核心挑战
由于 UE4 游戏在编译时会剥离符号信息,且不同版本、不同编译配置的内存布局可能不同,Dumper-7 必须:
- 在没有符号信息的情况下定位关键数据结构
- 自动适配不同 UE 版本的内存布局差异
- 处理各种编译器优化和平台差异
1.3 支持的 UE 版本
- UE4.11 - UE4.27
- UE5.0 - UE5.4+
2. 整体架构
2.1 工作流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ Dumper-7 工作流程 │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ DLL 注入 │
└──────┬───────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ Generator::InitEngineCore() │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 1. ObjectArray::Init() - 查找 GObjects 全局对象数组 │ │
│ │ 2. FName::Init_Windows() - 初始化 FName 名称系统 │ │
│ │ 3. Off::Init() - 自动查找所有结构体偏移 │ │
│ │ 4. PropertySizes::Init() - 初始化属性大小 │ │
│ │ 5. ProcessEvent::InitPE() - 查找 ProcessEvent 函数 │ │
│ │ 6. World::InitGWorld() - 查找 GWorld 全局变量 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ Generator::InitInternal() │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 1. PackageManager::Init() - 收集所有包和依赖关系 │ │
│ │ 2. StructManager::Init() - 收集所有结构体 │ │
│ │ 3. EnumManager::Init() - 收集所有枚举 │ │
│ │ 4. MemberManager::Init() - 处理成员名称冲突 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ Generator::Generate() │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ - CppGenerator 生成 C++ SDK 头文件 │ │
│ │ - MappingGenerator 生成 USMAP 映射文件 │ │
│ │ - IDAMappingGenerator 生成 IDA 脚本 │ │
│ │ - DumpspaceGenerator 生成 Dumpspace 格式 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
2.2 模块划分
Dumper/
├── Engine/
│ ├── Public/
│ │ ├── OffsetFinder/
│ │ │ ├── OffsetFinder.h # 偏移查找算法声明
│ │ │ └── Offsets.h # 偏移量存储
│ │ └── Unreal/
│ │ ├── ObjectArray.h # GObjects 数组封装
│ │ ├── UnrealObjects.h # UE 对象类型封装
│ │ ├── UnrealTypes.h # FName 等基础类型
│ │ └── Enums.h # 枚举定义
│ └── Private/
│ ├── OffsetFinder/
│ │ ├── OffsetFinder.cpp # 偏移查找算法实现
│ │ └── Offsets.cpp # 偏移初始化
│ └── Unreal/
│ ├── ObjectArray.cpp # GObjects 初始化
│ └── UnrealObjects.cpp # 对象方法实现
├── Generator/
│ ├── Public/Generators/
│ │ ├── Generator.h # 生成器基类
│ │ └── CppGenerator.h # C++ 代码生成器
│ └── Private/Generators/
│ └── Generator.cpp # 生成器实现
└── Platform/
└── Public/
└── Platform.h # 平台抽象层
3. 核心工具函数
3.1 FindOffset - 通用值匹配查找
这是最基础的偏移查找函数,用于在多个对象的相同偏移处查找匹配的值。
template<int Alignment = 4, typename T>
int32_t FindOffset(
const std::vector<std::pair<void*, T>>& ObjectValuePair,
int MinOffset = 0x28,
int MaxOffset = 0x1A0
);
算法原理
输入: [(对象A地址, 期望值A), (对象B地址, 期望值B), ...]
输出: 所有对象在该偏移处都匹配期望值的偏移量
算法:
1. 从 MinOffset 开始,按 Alignment 对齐遍历
2. 对于每个候选偏移:
a. 检查所有对象在该偏移处的值是否匹配期望值
b. 如果某对象在更高偏移才匹配,更新搜索起点并重新验证
3. 返回所有对象都匹配的最高偏移
实现细节
template<int Alignment = 4, typename T>
int32_t FindOffset(const std::vector<std::pair<void*, T>>& ObjectValuePair,
int MinOffset, int MaxOffset)
{
int32_t HighestFoundOffset = MinOffset;
bool bFoundOffset = false;
for (int i = 0; i < ObjectValuePair.size(); i++)
{
if (ObjectValuePair[i].first == nullptr)
continue; // 跳过空指针
for (int j = HighestFoundOffset; j < MaxOffset; j += Alignment)
{
// 读取该偏移处的值
const T TypedValueAtOffset = *reinterpret_cast<T*>(
static_cast<uint8_t*>(ObjectValuePair[i].first) + j);
if (TypedValueAtOffset == ObjectValuePair[i].second && j >= HighestFoundOffset)
{
bFoundOffset = true;
// 关键:如果在更高偏移找到匹配,重新验证所有对象
if (j > HighestFoundOffset)
{
HighestFoundOffset = j;
i = 0; // 重新从第一个对象开始验证
}
break;
}
}
}
return bFoundOffset ? HighestFoundOffset : OffsetNotFound;
}
为什么使用"最高偏移"策略
考虑以下场景:
- 对象A 在偏移 0x10 和 0x20 处都有值 5
- 对象B 只在偏移 0x20 处有值 5
如果简单地返回第一个匹配,会错误地返回 0x10。使用"最高偏移"策略可以确保找到所有对象都匹配的正确偏移。
3.2 GetValidPointerOffset - 有效指针查找
用于查找两个对象在相同偏移处都存储有效指针的位置。
template<bool bCheckForVft = true>
int32_t GetValidPointerOffset(
const void* PtrObjA,
const void* PtrObjB,
int32_t StartingOffset,
int32_t MaxOffset,
bool bNeedsToBeInProcessMemory = false
);
算法原理
输入: 两个对象的地址,搜索范围
输出: 两个对象在该偏移处都存储有效指针的偏移量
验证条件:
1. 指针本身可读 (不是野指针)
2. 如果 bCheckForVft=true,指针指向的内存也可读 (验证虚函数表)
3. 如果 bNeedsToBeInProcessMemory=true,指针必须在进程内存范围内
实现细节
template<bool bCheckForVft = true>
int32_t GetValidPointerOffset(const void* PtrObjA, const void* PtrObjB,
int32_t StartingOffset, int32_t MaxOffset,
bool bNeedsToBeInProcessMemory)
{
const uint8_t* ObjA = static_cast<const uint8_t*>(PtrObjA);
const uint8_t* ObjB = static_cast<const uint8_t*>(PtrObjB);
if (Platform::IsBadReadPtr(ObjA) || Platform::IsBadReadPtr(ObjB))
return OffsetNotFound;
for (int j = StartingOffset; j <= MaxOffset; j += sizeof(void*))
{
// 读取偏移处的指针
void* PtrA = *reinterpret_cast<void* const*>(ObjA + j);
void* PtrB = *reinterpret_cast<void* const*>(ObjB + j);
// 验证指针有效性
bool bIsAValid = !Platform::IsBadReadPtr(PtrA);
bool bIsBValid = !Platform::IsBadReadPtr(PtrB);
// 可选:验证虚函数表 (指针指向的第一个成员也是有效指针)
if constexpr (bCheckForVft)
{
bIsAValid = bIsAValid && !Platform::IsBadReadPtr(*reinterpret_cast<void**>(PtrA));
bIsBValid = bIsBValid && !Platform::IsBadReadPtr(*reinterpret_cast<void**>(PtrB));
}
// 可选:验证指针在进程内存范围内
if (bNeedsToBeInProcessMemory)
{
if (!Platform::IsAddressInProcessRange(PtrA) ||
!Platform::IsAddressInProcessRange(PtrB))
continue;
}
if (bIsAValid && bIsBValid)
return j;
}
return OffsetNotFound;
}
虚函数表验证的意义
UE4 中的 UObject 都有虚函数表,其内存布局为:
UObject 实例:
┌─────────────────┐
│ VTable 指针 ────┼──► ┌─────────────────┐
│ ... │ │ 虚函数指针1 │
└─────────────────┘ │ 虚函数指针2 │
│ ... │
└─────────────────┘
通过检查 **ptr 是否可读,可以验证 *ptr 是否指向一个有效的 UObject。
4. GObjects 全局对象数组查找
GObjects 是 UE4 中存储所有 UObject 实例的全局数组,是整个 Dumper 的基础。
4.1 GObjects 的两种布局
FFixedUObjectArray (UE4.11 - UE4.20)
struct FFixedUObjectArray
{
FUObjectItem* Objects; // 对象数组指针
int32 MaxObjects; // 最大对象数
int32 NumObjects; // 当前对象数
};
struct FUObjectItem
{
UObject* Object; // 对象指针
int32 Flags; // 标志
int32 ClusterRootIndex; // 集群根索引
};
内存布局:
偏移 0x00: Objects (void*)
偏移 0x08: MaxObjects (int32) [64位]
偏移 0x0C: NumObjects (int32)
FChunkedFixedUObjectArray (UE4.21+)
struct FChunkedFixedUObjectArray
{
FUObjectItem** Objects; // 分块指针数组
// ... 其他成员
int32 MaxElements; // 最大元素数
int32 NumElements; // 当前元素数
int32 MaxChunks; // 最大分块数
int32 NumChunks; // 当前分块数
};
内存布局(默认):
偏移 0x00: Objects (void**)
偏移 0x10: MaxElements (int32)
偏移 0x14: NumElements (int32)
偏移 0x18: MaxChunks (int32)
偏移 0x1C: NumChunks (int32)
4.2 GObjects 查找算法
验证函数 - FChunkedFixedUObjectArray
bool IsAddressValidGObjects(uintptr_t Address, const FChunkedFixedUObjectArrayLayout& Layout)
{
// 读取布局中的各个字段
void* Objects = *reinterpret_cast<void**>(Address + Layout.ObjectsOffset);
int32 MaxElements = *reinterpret_cast<int32*>(Address + Layout.MaxElementsOffset);
int32 NumElements = *reinterpret_cast<int32*>(Address + Layout.NumElementsOffset);
int32 MaxChunks = *reinterpret_cast<int32*>(Address + Layout.MaxChunksOffset);
int32 NumChunks = *reinterpret_cast<int32*>(Address + Layout.NumChunksOffset);
// 验证条件 1: NumChunks 在合理范围 [1, 20]
if (NumChunks > 0x14 || NumChunks < 0x1)
return false;
// 验证条件 2: MaxChunks 在合理范围 [6, 1535]
if (MaxChunks > 0x5FF || MaxChunks < 0x6)
return false;
// 验证条件 3: 元素数量合理
if (NumElements <= 0x800 || MaxElements <= 0x10000)
return false;
// 验证条件 4: 数量关系正确
if (NumElements > MaxElements || NumChunks > MaxChunks)
return false;
// 验证条件 5: MaxElements 是 16 的倍数
if ((MaxElements % 0x10) != 0)
return false;
// 验证条件 6: 每块元素数合理
int32 ElementsPerChunk = MaxElements / MaxChunks;
if ((ElementsPerChunk % 0x10) != 0)
return false;
if (ElementsPerChunk < 0x8000 || ElementsPerChunk > 0x80000)
return false;
// 验证条件 7: NumChunks 与 NumElements 匹配
bool bNumChunksFitsNumElements = ((NumElements / ElementsPerChunk) + 1) == NumChunks;
if (!bNumChunksFitsNumElements)
return false;
// 验证条件 8: 所有 Chunk 指针有效
void** ObjectsPtr = reinterpret_cast<void**>(Objects);
for (int i = 0; i < NumChunks; i++)
{
if (!ObjectsPtr[i] || Platform::IsBadReadPtr(ObjectsPtr[i]))
return false;
}
return true;
}
搜索策略
void ObjectArray::Init(bool bScanAllMemory, const char* ModuleName)
{
// 定义验证回调
auto IsAddressValidGObjects = [](const void* CurrentAddress) -> bool
{
// 尝试 FFixedUObjectArray 布局
if (MatchesAnyLayout(FFixedUObjectArrayLayouts, Address))
return true;
// 尝试 FChunkedFixedUObjectArray 布局
if (MatchesAnyLayout(FChunkedFixedUObjectArrayLayouts, Address))
return true;
return false;
};
void* GObjectsAddress = nullptr;
if (!bScanAllMemory)
{
// 首先只扫描 .data 段(速度快)
GObjectsAddress = Platform::IterateSectionWithCallback(
Platform::GetSectionInfo(".data"),
IsAddressValidGObjects,
0x4, // 4字节对齐
0x50 // 搜索深度
);
}
if (!GObjectsAddress)
{
// 如果 .data 段没找到,扫描所有内存段
GObjectsAddress = Platform::IterateAllSectionsWithCallback(
IsAddressValidGObjects, 0x4, 0x50, ModuleName);
}
}
4.3 FUObjectItem 布局检测
void ObjectArray::InitializeFUObjectItem(uint8_t* FirstItemPtr)
{
// 查找 Object 指针的偏移(第一个有效指针)
for (int i = 0x0; i < 0x20; i += 4)
{
if (!Platform::IsBadReadPtr(*reinterpret_cast<uint8_t**>(FirstItemPtr + i)))
{
FUObjectItemInitialOffset = i;
break;
}
}
// 查找 FUObjectItem 的大小(通过找第二个对象的位置)
for (int i = FUObjectItemInitialOffset + sizeof(void*); i <= 0x38; i += 4)
{
void* SecondObject = *(void**)(FirstItemPtr + i);
void* ThirdObject = *(void**)(FirstItemPtr + (i * 2) - FUObjectItemInitialOffset);
// 如果第二个和第三个位置都是有效指针,说明找到了正确的大小
if (!Platform::IsBadReadPtr(SecondObject) && !Platform::IsBadReadPtr(*reinterpret_cast<void**>(SecondObject)) &&
!Platform::IsBadReadPtr(ThirdObject) && !Platform::IsBadReadPtr(*reinterpret_cast<void**>(ThirdObject)))
{
SizeOfFUObjectItem = i - FUObjectItemInitialOffset;
break;
}
}
}
4.4 支持的特殊布局
某些游戏修改了 GObjects 的布局,Dumper-7 预定义了多种布局:
constexpr std::array FChunkedFixedUObjectArrayLayouts =
{
// 默认布局 (UE4.21+)
FChunkedFixedUObjectArrayLayout {
.ObjectsOffset = 0x00,
.MaxElementsOffset = 0x10,
.NumElementsOffset = 0x14,
.MaxChunksOffset = 0x18,
.NumChunksOffset = 0x1C,
},
// Back4Blood 布局
FChunkedFixedUObjectArrayLayout {
.ObjectsOffset = 0x10, // Objects 在最后
.MaxElementsOffset = 0x00,
.NumElementsOffset = 0x04,
.MaxChunksOffset = 0x08,
.NumChunksOffset = 0x0C,
},
// Multiversus 布局
FChunkedFixedUObjectArrayLayout {
.ObjectsOffset = 0x18,
.MaxElementsOffset = 0x10,
.NumElementsOffset = 0x00, // NumElements 在最前
.MaxChunksOffset = 0x14,
.NumChunksOffset = 0x20,
},
// ... 更多布局
};
5. UObject 成员偏移查找
UObject 是 UE4 所有对象的基类,其内存布局是整个反射系统的基础。
5.1 UObject 标准内存布局
UObject (64位):
┌─────────────────────────────────────┐
│ 偏移 0x00: VTable 指针 (8 bytes) │
│ 偏移 0x08: Flags (4 bytes) │
│ 偏移 0x0C: Index (4 bytes) │
│ 偏移 0x10: Class 指针 (8 bytes) │
│ 偏移 0x18: Name (8 bytes) │ ← FName 结构
│ 偏移 0x20: Outer 指针 (8 bytes) │
└─────────────────────────────────────┘
5.2 FindUObjectFlagsOffset - Flags 偏移查找
启发式原理
UObject::Flags 是一个枚举值,大多数对象的 Flags 值为 0x43:
RF_Public(0x01)RF_Standalone(0x02)RF_MarkAsNative(0x04)RF_Transactional(0x40)
算法实现
int32_t FindUObjectFlagsOffset()
{
constexpr auto EnumFlagValueToSearch = 0x43;
constexpr auto MinNumFlagValuesRequiredAtOffset = 0xA0; // 至少 160 个对象匹配
// 对前 0x20 个对象进行搜索
for (int i = 0; i < 0x20; i++)
{
int Offset = 0x0;
while (Offset != OffsetNotFound)
{
// 在当前对象中查找值为 0x43 的偏移
Offset = FindOffset(std::vector{
std::pair{ ObjectArray::GetByIndex(i).GetAddress(), EnumFlagValueToSearch }
}, Offset, 0x40);
if (Offset == OffsetNotFound)
break;
// 统计前 0x100 个对象中该偏移处值为 0x43 的数量
int32 NumObjectsWithFlagAtOffset = 0;
int Counter = 0;
for (UEObject Obj : ObjectArray())
{
if (Counter++ == 0x100)
break;
const int32 TypedValueAtOffset = *reinterpret_cast<int32*>(
reinterpret_cast<uintptr_t>(Obj.GetAddress()) + Offset);
if (TypedValueAtOffset == EnumFlagValueToSearch)
NumObjectsWithFlagAtOffset++;
}
// 如果超过 160 个对象匹配,认为找到了正确偏移
if (NumObjectsWithFlagAtOffset > MinNumFlagValuesRequiredAtOffset)
return Offset;
}
}
return OffsetNotFound;
}
统计学原理
| 条件 | 原因 |
|---|---|
| 搜索值 = 0x43 | 这是 UE4 中最常见的对象标志组合 |
| 阈值 = 160/256 | 约 62.5% 的对象应该有这个标志 |
| 随机数据概率 | 随机 int32 等于 0x43 的概率约为 1/2³² |
5.3 FindUObjectIndexOffset - Index 偏移查找
启发式原理
UObject::InternalIndex 等于对象在 GObjects 数组中的索引。
算法实现
int32_t FindUObjectIndexOffset()
{
std::vector<std::pair<void*, int32_t>> Infos;
// GObjects[0x55] 的 Index 应该是 0x55
Infos.emplace_back(ObjectArray::GetByIndex(0x055).GetAddress(), 0x055);
// GObjects[0x123] 的 Index 应该是 0x123
Infos.emplace_back(ObjectArray::GetByIndex(0x123).GetAddress(), 0x123);
// 从 VTable 之后开始搜索
return FindOffset<4>(Infos, sizeof(void*));
}
为什么选择 0x55 和 0x123
- 这些是"特殊"的数值,不太可能与其他字段冲突
- 0x55 = 85,0x123 = 291,都是合理的对象索引
- 避免使用 0、1 等可能出现在多个字段的值
5.4 FindUObjectClassOffset - Class 偏移查找 ⭐
启发式原理
利用 UClass 的自引用特性:Class CoreUObject.Class 的 Class 指针最终指向自己。
类型系统的自引用
任意 UObject UClass (如 Actor) UClass (Class)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ VTable │ │ VTable │ │ VTable │
│ Flags │ │ Flags │ │ Flags │
│ Index │ │ Index │ │ Index │
│ Class ──────────┼─────►│ Class ──────────┼───────►│ Class ──────────┼──┐
│ Name │ │ Name │ │ Name │ │
│ Outer │ │ Outer │ │ Outer │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘ │
▲ │
└──────────────┘
自引用循环!
算法实现
int32_t FindUObjectClassOffset()
{
// 验证函数:检查 Class 指针链是否形成循环
auto IsValidCyclicUClassPtrOffset = [](const uint8_t* ObjA, const uint8_t* ObjB,
int32_t ClassPtrOffset) -> bool
{
const uint8_t* NextClassA = ObjA;
const uint8_t* NextClassB = ObjB;
// 最多遍历 16 层
for (int MaxLoopCount = 0; MaxLoopCount < 0x10; MaxLoopCount++)
{
const uint8_t* CurrentClassA = NextClassA;
const uint8_t* CurrentClassB = NextClassB;
// 沿着 Class 指针链向上遍历
NextClassA = *reinterpret_cast<const uint8_t* const*>(NextClassA + ClassPtrOffset);
NextClassB = *reinterpret_cast<const uint8_t* const*>(NextClassB + ClassPtrOffset);
// 如果指针无效,说明这不是 Class 偏移
if (!NextClassA || !NextClassB ||
Platform::IsBadReadPtr(NextClassA) || Platform::IsBadReadPtr(NextClassB))
return false;
// 如果指针指向自己,说明找到了 UClass 的 Class 成员
if (CurrentClassA == NextClassA && CurrentClassB == NextClassB)
return true;
}
return false;
};
// 选择两个对象进行测试
const uint8_t* ObjA = static_cast<const uint8_t*>(ObjectArray::GetByIndex(0x055).GetAddress());
const uint8_t* ObjB = static_cast<const uint8_t*>(ObjectArray::GetByIndex(0x123).GetAddress());
int32_t Offset = 0;
while (Offset != OffsetNotFound)
{
// 查找有效指针偏移
Offset = GetValidPointerOffset<true>(ObjA, ObjB, Offset + sizeof(void*), 0x50);
// 验证是否是循环引用
if (IsValidCyclicUClassPtrOffset(ObjA, ObjB, Offset))
return Offset;
}
return OffsetNotFound;
}
为什么这个方法可靠
- 唯一性:只有 Class 指针会形成这种循环引用
- 普遍性:所有 UE4 版本都有这个特性
- 验证性:通过两个不同对象交叉验证,避免误判
5.5 FindUObjectNameOffset - Name 偏移查找 ⭐⭐
启发式原理
FName::ComparisonIndex 是一个索引值,有特定的统计特征:
- 不会太小(前几个索引是保留名称)
- 不会太大(受名称表大小限制)
- 平均值在合理范围内
算法实现
template<typename IteratorType>
int32_t FindNameOffsetForSomeClass(
std::function<bool(int32_t)> IsPotentialValidOffset,
IteratorType DataSetStartIterator,
IteratorType DataSetEndIterator)
{
// 候选偏移的统计信息
struct ValueInfo
{
int32 Offset; // 候选偏移
int32 NumNamesWithLowCmpIdx = 0; // CompIdx 在 [0,16] 范围的数量
uint64 TotalValue = 0; // 所有值的总和
bool bIsValidCmpIdxRange = true; // 是否所有值都在有效范围内
};
// 约束常量
constexpr auto MaxAllowedComparisonIndexValue = 0x4000000; // 单个值上限 (~67M)
constexpr auto MaxAllowedAverageComparisonIndexValue = 0x2000000; // 平均值上限 (~33M)
constexpr auto MinAllowedAverageComparisonIndexValue = 0x280; // 平均值下限 (640)
constexpr auto LowComparisonIndexUpperCap = 0x10; // "低索引"定义
constexpr auto MaxAllowedNamesWithLowCmpIdx = 0x40; // 低索引数量上限 (64)
std::vector<ValueInfo> PossibleOffsets;
// 初始化候选偏移列表
for (int i = sizeof(void*); i <= 0x40; i += 0x4)
{
if (!IsPotentialValidOffset(i))
continue;
PossibleOffsets.push_back(ValueInfo{ i });
}
int NumObjectsConsidered = 0;
// 遍历所有对象,收集统计数据
for (auto It = DataSetStartIterator; It != DataSetEndIterator; ++It)
{
const void* CurrentObject = (*It).GetAddress();
// 跳过可能越界的对象
const bool bIsGoingPastPageBounds =
(reinterpret_cast<uintptr_t>(CurrentObject) & 0xFFF) > (0x1000 - 0x44);
if (bIsGoingPastPageBounds)
continue;
NumObjectsConsidered++;
for (ValueInfo& Info : PossibleOffsets)
{
const uint32 ValueAtOffset = *reinterpret_cast<const uint32*>(
reinterpret_cast<uintptr_t>(CurrentObject) + Info.Offset);
Info.TotalValue += ValueAtOffset;
Info.bIsValidCmpIdxRange = Info.bIsValidCmpIdxRange &&
(ValueAtOffset < MaxAllowedComparisonIndexValue);
Info.NumNamesWithLowCmpIdx += (ValueAtOffset <= LowComparisonIndexUpperCap);
}
}
// 筛选满足所有条件的偏移
for (const ValueInfo& Info : PossibleOffsets)
{
const auto AverageValue = Info.TotalValue / NumObjectsConsidered;
if (Info.bIsValidCmpIdxRange && // 所有值都在有效范围
Info.NumNamesWithLowCmpIdx <= MaxAllowedNamesWithLowCmpIdx && // 低索引不能太多
AverageValue >= MinAllowedAverageComparisonIndexValue && // 平均值不能太低
AverageValue <= MaxAllowedAverageComparisonIndexValue) // 平均值不能太高
{
return Info.Offset;
}
}
return -1;
}
排除已知偏移
int32_t FindUObjectNameOffset()
{
auto IsPotentiallyValidOffset = [](int32 Offset) -> bool
{
// 排除已知的其他成员偏移
return Offset != Off::UObject::Class &&
Offset != (Off::UObject::Class + sizeof(void*) / 2) && // 指针中间
Offset != Off::UObject::Outer &&
Offset != (Off::UObject::Outer + sizeof(void*) / 2) &&
Offset != Off::UObject::Flags &&
Offset != Off::UObject::Index &&
Offset != Off::UObject::Vft &&
Offset != (Off::UObject::Vft + sizeof(void*) / 2);
};
return FindNameOffsetForSomeClass(IsPotentiallyValidOffset,
ObjectArray().begin(),
ObjectArray().end());
}
统计学原理详解
| 约束条件 | 值 | 原因 |
|---|---|---|
| 单个值上限 | 0x4000000 | FName 索引不会超过 6700 万 |
| 平均值下限 | 0x280 | 正常游戏至少有几百个名称 |
| 平均值上限 | 0x2000000 | 平均索引不会太大 |
| 低索引上限 | 0x10 | 索引 0-16 是保留名称 |
| 低索引数量上限 | 0x40 | 不会有太多对象使用保留名称 |
5.6 FindUObjectOuterOffset - Outer 偏移查找
启发式原理
Outer 是一个指向另一个 UObject 的有效指针。
算法实现
int32_t FindUObjectOuterOffset()
{
int32_t LowestFoundOffset = 0xFFFF;
// 多次随机采样,避免选中 UPackage(没有 Outer)
for (int i = 0; i < 0x10; i++)
{
int32_t Offset = 0;
// 随机选择两个对象
const void* ObjA = ObjectArray::GetByIndex(rand() % 0x400).GetAddress();
const void* ObjB = ObjectArray::GetByIndex(rand() % 0x400).GetAddress();
while (Offset != OffsetNotFound)
{
// 查找有效指针偏移
Offset = GetValidPointerOffset(ObjA, ObjB, Offset + sizeof(void*), 0x50);
// 排除已知的 Class 偏移
if (Offset != Off::UObject::Class && Offset != Off::UObject::Index)
break;
}
// 取最小偏移
if (Offset != OffsetNotFound && Offset < LowestFoundOffset)
LowestFoundOffset = Offset;
}
return LowestFoundOffset == 0xFFFF ? OffsetNotFound : LowestFoundOffset;
}
为什么多次随机采样
- UPackage 对象的 Outer 为 nullptr
- 随机采样 16 次,大概率能选中有 Outer 的对象
- 取最小偏移是因为 Outer 通常紧跟在 Name 之后
6. FName 系统检测
FName 是 UE4 的名称系统,有多种变体,需要自动检测。
6.1 FName 的四种变体
| 变体 | 大小 | 布局 | 编译宏 |
|---|---|---|---|
| 默认 | 8字节 | {CompIdx, Number} |
- |
| OUTLINE_NUMBER | 4字节 | {CompIdx} |
FNAME_OUTLINE_NUMBER |
| CASE_PRESERVING | 12字节 | {CompIdx, DisplayIdx, Number} |
WITH_CASE_PRESERVING_NAME |
| CASE_PRESERVING + OUTLINE | 8字节 | {CompIdx, DisplayIdx} |
两者都有 |
6.2 InitFNameSettings - FName 布局检测
void OffsetFinder::InitFNameSettings()
{
UEObject FirstObject = ObjectArray::GetByIndex(0);
const uint8* NameAddress = static_cast<const uint8*>(FirstObject.GetFName().GetAddress());
// 读取 FName 的前两个 int32
const int32 FNameFirstInt = *reinterpret_cast<const int32*>(NameAddress); // ComparisonIndex
const int32 FNameSecondInt = *reinterpret_cast<const int32*>(NameAddress + 0x4); // Number 或 DisplayIndex
// 根据 UObject 布局推算 FName 大小
const int32 FNameSize = Off::UObject::Outer - Off::UObject::Name;
// 情况1: WITH_CASE_PRESERVING_NAME + FNAME_OUTLINE_NUMBER (8字节)
// 特征: FNameSize == 8 且 CompIdx == DisplayIdx
if (FNameSize == 0x8 && FNameFirstInt == FNameSecondInt)
{
Settings::Internal::bUseCasePreservingName = true;
Settings::Internal::bUseOutlineNumberName = true;
Off::FName::Number = -0x1; // 没有 Number 字段
Off::InSDK::Name::FNameSize = 0x8;
}
// 情况2: WITH_CASE_PRESERVING_NAME (16字节)
// 特征: FNameSize == 16
else if (FNameSize == 0x10)
{
Settings::Internal::bUseCasePreservingName = true;
Off::FName::Number = (FNameFirstInt == FNameSecondInt) ? 0x8 : 0x4;
Off::InSDK::Name::FNameSize = 0xC;
}
// 情况3: FNAME_OUTLINE_NUMBER (4字节)
// 特征: 很少有对象的 Number 在 [1,4] 范围
else if (GetNumNamesWithNumberOneToFour() < FNameNumberThreashold)
{
Settings::Internal::bUseOutlineNumberName = true;
Off::FName::Number = -0x1;
Off::InSDK::Name::FNameSize = 0x4;
}
// 情况4: 默认 (8字节)
else
{
Off::FName::Number = 0x4;
Off::InSDK::Name::FNameSize = 0x8;
}
}
6.3 Number 字段统计检测
auto GetNumNamesWithNumberOneToFour = []() -> int32
{
int32 NamesWithNumberOneToFour = 0;
for (UEObject Obj : ObjectArray())
{
const uint32 Number = Obj.GetFName().GetNumber();
// FName::Number 在 [1,4] 范围表示同名对象的序号
// 例如: Actor_1, Actor_2, Actor_3
if (Number > 0 && Number < 5)
NamesWithNumberOneToFour++;
}
return NamesWithNumberOneToFour;
};
// 如果低于 3%,说明 Number 字段不存在(OUTLINE_NUMBER 模式)
constexpr float MinPercentage = 0.03f;
const int32 FNameNumberThreashold = ObjectArray::Num() * MinPercentage;
统计学原理
- 正常游戏中,约 6%+ 的对象名称有 Number 后缀(如
Actor_1) - 如果 Number 字段不存在,该位置是随机数据
- 随机数据在 [1,4] 范围的概率约为 4/2³² ≈ 0.0000001%
6.4 PostInitFNameSettings - 后期修正
某些小型游戏的统计数据不准确,需要通过实际属性大小来修正:
void OffsetFinder::PostInitFNameSettings()
{
// 通过 PlayerStartTag 属性的大小来验证 FName 大小
const UEClass PlayerStart = ObjectArray::FindClassFast("PlayerStart");
const int32 FNameSize = PlayerStart.FindMember("PlayerStartTag").GetSize();
// 如果与之前检测的不一致,进行修正
if (Off::InSDK::Name::FNameSize != FNameSize)
{
// 修正 FField::Flags 偏移
Off::FField::Flags = (Off::FField::Flags - Off::InSDK::Name::FNameSize) + FNameSize;
// 重新检测 FName 布局
// ... (类似 InitFNameSettings 的逻辑)
}
}
7. UStruct 成员偏移查找
UStruct 是 UE4 中结构体和类的基类,包含继承、子成员等信息。
7.1 UStruct 内存布局
UStruct (继承自 UField):
┌─────────────────────────────────────┐
│ [UObject 成员] │
│ 偏移 0x30: Next (UField*) │ ← UField 成员
│ 偏移 0x38: SuperStruct (UStruct*) │
│ 偏移 0x40: Children (UField*) │ ← 子成员链表头
│ 偏移 0x48: ChildProperties (FField*)│ ← UE4.25+ FProperty 链表
│ 偏移 0x50: Size (int32) │ ← 结构体大小
│ 偏移 0x54: MinAlignment (int16) │ ← 最小对齐
│ ... │
└─────────────────────────────────────┘
7.2 FindSuperOffset - SuperStruct 偏移查找
启发式原理
利用已知的继承关系:
UStruct继承自UFieldUClass继承自UStruct
int32_t FindSuperOffset()
{
std::vector<std::pair<void*, void*>> Infos;
// Struct 的 Super 是 Field
Infos.push_back({
ObjectArray::FindObjectFast("Struct").GetAddress(),
ObjectArray::FindObjectFast("Field").GetAddress()
});
// Class 的 Super 是 Struct
Infos.push_back({
ObjectArray::FindObjectFast("Class").GetAddress(),
ObjectArray::FindObjectFast("Struct").GetAddress()
});
// 某些版本 UStruct 拼写为 Ustruct
if (Infos[0].first == nullptr)
Infos[0].first = Infos[1].second = ObjectArray::FindObjectFast("struct").GetAddress();
return FindOffset(Infos);
}
UE4 类型继承链
UObject
└── UField
└── UStruct
├── UClass
├── UScriptStruct
└── UFunction
7.3 FindChildOffset - Children 偏移查找
启发式原理
Children 指向结构体的第一个子成员。同时用于检测是否使用 FProperty 系统。
int32_t FindChildOffset()
{
std::vector<std::pair<void*, void*>> Infos;
// 检测是否使用 FProperty 系统(UE4.25+)
// 如果能找到 ObjectProperty,说明是旧版本
if (ObjectArray::FindObject("ObjectProperty Engine.Controller.TransformComponent",
EClassCastFlags::ObjectProperty))
{
// 旧版本:Children 指向 UProperty
// Vector 结构体的 Children 应该指向 X 属性
Infos.push_back({
ObjectArray::FindObjectFast("Vector").GetAddress(),
ObjectArray::FindObjectFastInOuter("X", "Vector").GetAddress()
});
Infos.push_back({
ObjectArray::FindObjectFast("Vector4").GetAddress(),
ObjectArray::FindObjectFastInOuter("X", "Vector4").GetAddress()
});
Infos.push_back({
ObjectArray::FindObjectFast("Guid").GetAddress(),
ObjectArray::FindObjectFastInOuter("A", "Guid").GetAddress()
});
Settings::Internal::bUseFProperty = false;
return FindOffset(Infos, 0x14);
}
else
{
// 新版本:Children 只指向 UFunction,属性在 ChildProperties
Infos.push_back({
ObjectArray::FindObjectFast("PlayerController").GetAddress(),
ObjectArray::FindObjectFastInOuter("WasInputKeyJustReleased", "PlayerController").GetAddress()
});
Infos.push_back({
ObjectArray::FindObjectFast("Controller").GetAddress(),
ObjectArray::FindObjectFastInOuter("UnPossess", "Controller").GetAddress()
});
Settings::Internal::bUseFProperty = true;
return FindOffset(Infos);
}
}
UProperty vs FProperty
| 版本 | 属性系统 | Children 内容 | ChildProperties |
|---|---|---|---|
| < UE4.25 | UProperty | UProperty + UFunction | 不存在 |
| >= UE4.25 | FProperty | 只有 UFunction | FProperty 链表 |
7.4 FindChildPropertiesOffset - ChildProperties 偏移查找
int32_t FindChildPropertiesOffset()
{
// 只在 FProperty 系统中使用
const void* ObjA = ObjectArray::FindStructFast("Color").GetAddress();
const void* ObjB = ObjectArray::FindStructFast("Guid").GetAddress();
// 从 Children 之后开始搜索
return GetValidPointerOffset(ObjA, ObjB, Off::UStruct::Children + 0x08, 0x80);
}
7.5 FindStructSizeOffset - Size 偏移查找
int32_t FindStructSizeOffset()
{
std::vector<std::pair<void*, int32_t>> Infos;
// FColor 的大小是 4 字节 (BGRA 各 1 字节)
Infos.push_back({ ObjectArray::FindObjectFast("Color").GetAddress(), 0x04 });
// FGuid 的大小是 16 字节 (4 个 uint32)
Infos.push_back({ ObjectArray::FindObjectFast("Guid").GetAddress(), 0x10 });
return FindOffset(Infos);
}
7.6 FindMinAlignmentOffset - MinAlignment 偏移查找
int32_t FindMinAlignmentOffset()
{
std::vector<std::pair<void*, int16_t>> Infos;
// FTransform 需要 16 字节对齐(包含 SIMD 向量)
Infos.push_back({ ObjectArray::FindObjectFast("Transform").GetAddress(), 0x10 });
// PlayerController 需要 8 字节对齐(64位指针)
if constexpr (Platform::Is64Bit())
{
Infos.push_back({ ObjectArray::FindObjectFast("PlayerController").GetAddress(), 0x8 });
}
else
{
Infos.push_back({ ObjectArray::FindObjectFast("InterpCurveLinearColor").GetAddress(), 0x04 });
}
return FindOffset(Infos);
}
8. UFunction 成员偏移查找
UFunction 表示 UE4 中的函数,包含函数标志和原生函数指针。
8.1 UFunction 内存布局
UFunction (继承自 UStruct):
┌─────────────────────────────────────┐
│ [UStruct 成员] │
│ 偏移 0x88: FunctionFlags (uint32) │
│ ... │
│ 偏移 0xB0: ExecFunction (void*) │ ← 原生函数指针
│ ... │
└─────────────────────────────────────┘
8.2 FindFunctionFlagsOffset - FunctionFlags 偏移查找
启发式原理
利用已知函数的标志组合。
int32_t FindFunctionFlagsOffset()
{
std::vector<std::pair<void*, EFunctionFlags>> Infos;
// WasInputKeyJustPressed: Final | Native | Public | BlueprintCallable | BlueprintPure | Const
Infos.push_back({
ObjectArray::FindObjectFast("WasInputKeyJustPressed", EClassCastFlags::Function).GetAddress(),
EFunctionFlags::Final | EFunctionFlags::Native | EFunctionFlags::Public |
EFunctionFlags::BlueprintCallable | EFunctionFlags::BlueprintPure | EFunctionFlags::Const
});
// ToggleSpeaking: Exec | Native | Public (控制台命令)
Infos.push_back({
ObjectArray::FindObjectFast("ToggleSpeaking", EClassCastFlags::Function).GetAddress(),
EFunctionFlags::Exec | EFunctionFlags::Native | EFunctionFlags::Public
});
// SwitchLevel: Exec | Native | Public
Infos.push_back({
ObjectArray::FindObjectFast("SwitchLevel", EClassCastFlags::Function).GetAddress(),
EFunctionFlags::Exec | EFunctionFlags::Native | EFunctionFlags::Public
});
// 某些游戏没有 SwitchLevel,用 FOV 替代
if (Infos[2].first == nullptr)
Infos[2].first = ObjectArray::FindObjectFast("FOV", EClassCastFlags::Function).GetAddress();
int32 Ret = FindOffset(Infos);
// 如果找不到,尝试添加 RequiredAPI 标志(某些 UE 版本)
if (Ret == OffsetNotFound)
{
for (auto& [_, Flags] : Infos)
Flags |= EFunctionFlags::RequiredAPI;
return FindOffset(Infos);
}
return Ret;
}
函数标志说明
| 标志 | 值 | 说明 |
|---|---|---|
| Final | 0x00000001 | 不能被重写 |
| Native | 0x00000400 | C++ 实现 |
| Public | 0x00020000 | 公开函数 |
| BlueprintCallable | 0x04000000 | 蓝图可调用 |
| BlueprintPure | 0x10000000 | 纯函数(无副作用) |
| Const | 0x40000000 | 常量函数 |
| Exec | 0x00000200 | 控制台命令 |
8.3 FindFunctionNativeFuncOffset - ExecFunction 偏移查找
启发式原理
Native 函数的 ExecFunction 指针指向进程内存范围内的代码。
int32_t FindFunctionNativeFuncOffset()
{
uintptr_t WasInputKeyJustPressed = (uintptr_t)ObjectArray::FindObjectFast(
"WasInputKeyJustPressed", EClassCastFlags::Function).GetAddress();
uintptr_t ToggleSpeaking = (uintptr_t)ObjectArray::FindObjectFast(
"ToggleSpeaking", EClassCastFlags::Function).GetAddress();
uintptr_t SwitchLevel_Or_FOV = (uintptr_t)ObjectArray::FindObjectFast(
"SwitchLevel", EClassCastFlags::Function).GetAddress();
if (SwitchLevel_Or_FOV == NULL)
SwitchLevel_Or_FOV = (uintptr_t)ObjectArray::FindObjectFast(
"FOV", EClassCastFlags::Function).GetAddress();
// 遍历可能的偏移范围 [0x30, 0x140)
for (int i = 0x30; i < 0x140; i += sizeof(void*))
{
// 检查三个函数在该偏移处的值是否都是有效的代码地址
if (Platform::IsAddressInProcessRange(*(uintptr_t*)(WasInputKeyJustPressed + i)) &&
Platform::IsAddressInProcessRange(*(uintptr_t*)(ToggleSpeaking + i)) &&
Platform::IsAddressInProcessRange(*(uintptr_t*)(SwitchLevel_Or_FOV + i)))
{
return i;
}
}
return 0x0;
}
验证逻辑
- ExecFunction 是函数指针,必须指向可执行代码
Platform::IsAddressInProcessRange检查地址是否在模块的代码段内- 三个不同函数都必须满足条件,避免误判
9. UClass 成员偏移查找
UClass 是 UE4 中类的元数据,包含类型转换标志、默认对象等信息。
9.1 UClass 内存布局
UClass (继承自 UStruct):
┌─────────────────────────────────────┐
│ [UStruct 成员] │
│ ... │
│ 偏移 0xE0: CastFlags (uint64) │ ← 类型转换标志
│ ... │
│ 偏移 0x110: ClassDefaultObject │ ← CDO 指针
│ ... │
│ 偏移 0x1B8: ImplementedInterfaces │ ← 接口数组
│ ... │
└─────────────────────────────────────┘
9.2 FindCastFlagsOffset - CastFlags 偏移查找
启发式原理
不同类型的 UClass 有不同的 CastFlags 标志。
int32_t FindCastFlagsOffset()
{
std::vector<std::pair<void*, EClassCastFlags>> Infos;
// Actor 类应该有 Actor 标志
Infos.push_back({
ObjectArray::FindObjectFast("Actor").GetAddress(),
EClassCastFlags::Actor
});
// Class 类应该有 Field | Struct | Class 标志
Infos.push_back({
ObjectArray::FindObjectFast("Class").GetAddress(),
EClassCastFlags::Field | EClassCastFlags::Struct | EClassCastFlags::Class
});
return FindOffset(Infos);
}
CastFlags 的作用
CastFlags 用于快速类型检查,避免遍历继承链:
// 传统方式(慢)
bool IsActor = Object->IsA(AActor::StaticClass());
// CastFlags 方式(快)
bool IsActor = (Object->GetClass()->CastFlags & EClassCastFlags::Actor) != 0;
9.3 FindDefaultObjectOffset - ClassDefaultObject 偏移查找
启发式原理
每个 UClass 都有一个 CDO(Class Default Object)。
int32_t FindDefaultObjectOffset()
{
std::vector<std::pair<void*, void*>> Infos;
// Object 类的 CDO 是 Default__Object
Infos.push_back({
ObjectArray::FindClassFast("Object").GetAddress(),
ObjectArray::FindObjectFast("Default__Object").GetAddress()
});
// Field 类的 CDO 是 Default__Field
Infos.push_back({
ObjectArray::FindClassFast("Field").GetAddress(),
ObjectArray::FindObjectFast("Default__Field").GetAddress()
});
// 搜索范围较大,因为 CDO 偏移在不同版本差异较大
return FindOffset(Infos, 0x28, 0x200);
}
CDO 命名规则
UE4 的 CDO 命名格式固定为 Default__ClassName,这是引擎的硬编码行为。
9.4 FindImplementedInterfacesOffset - ImplementedInterfaces 偏移查找
启发式原理
查找 TArray<FImplementedInterface> 数组,验证其内容。
int32_t FindImplementedInterfacesOffset()
{
// 找到一个已知实现了接口的类
UEClass Interface_AssetUserDataClass = ObjectArray::FindClassFast("Interface_AssetUserData");
const uint8_t* ActorComponentClassPtr = (uint8_t*)ObjectArray::FindClassFast("ActorComponent").GetAddress();
// 遍历可能的偏移
for (int i = Off::UClass::ClassDefaultObject; i <= (0x350 - 0x10); i += sizeof(void*))
{
const auto& ActorArray = *(TArray<FImplementedInterface>*)(ActorComponentClassPtr + i);
// 验证数组有效性
if (ActorArray.IsValid() && !Platform::IsBadReadPtr(ActorArray.GetDataPtr()))
{
// 验证第一个接口是否是 Interface_AssetUserData
if (ActorArray[0].InterfaceClass == Interface_AssetUserDataClass)
return i;
}
}
return OffsetNotFound;
}
FImplementedInterface 结构
struct FImplementedInterface
{
UClass* InterfaceClass; // 接口类
int32 PointerOffset; // 接口指针偏移
bool bImplementedByK2; // 是否由蓝图实现
};
10. Property 成员偏移查找
Property 是 UE4 中属性的基类,包含属性的大小、偏移、标志等信息。
10.1 Property 内存布局
UProperty / FProperty:
┌─────────────────────────────────────┐
│ [UObject/FField 成员] │
│ ... │
│ 偏移 0x38: ArrayDim (int32) │ ← 数组维度
│ 偏移 0x3C: ElementSize (int32) │ ← 元素大小
│ 偏移 0x40: PropertyFlags (uint64) │ ← 属性标志
│ ... │
│ 偏移 0x4C: Offset_Internal (int32) │ ← 在结构体中的偏移
│ ... │
└─────────────────────────────────────┘
10.2 FindElementSizeOffset - ElementSize 偏移查找
int32_t FindElementSizeOffset()
{
std::vector<std::pair<void*, int32_t>> Infos;
UEStruct Guid = ObjectArray::FindStructFast("Guid");
// FGuid 的 A, C, D 成员都是 uint32,大小为 4
Infos.push_back({ Guid.FindMember("A").GetAddress(), 0x04 });
Infos.push_back({ Guid.FindMember("C").GetAddress(), 0x04 });
Infos.push_back({ Guid.FindMember("D").GetAddress(), 0x04 });
return FindOffset(Infos);
}
10.3 FindArrayDimOffset - ArrayDim 偏移查找
int32_t FindArrayDimOffset()
{
std::vector<std::pair<void*, int32_t>> Infos;
UEStruct Guid = ObjectArray::FindStructFast("Guid");
// 普通成员的 ArrayDim 都是 1
Infos.push_back({ Guid.FindMember("A").GetAddress(), 0x01 });
Infos.push_back({ Guid.FindMember("C").GetAddress(), 0x01 });
Infos.push_back({ Guid.FindMember("D").GetAddress(), 0x01 });
// 在 ElementSize 附近搜索
const int32_t MinOffset = Off::Property::ElementSize - 0x10;
const int32_t MaxOffset = Off::Property::ElementSize + 0x10;
return FindOffset(Infos, MinOffset, MaxOffset);
}
10.4 FindPropertyFlagsOffset - PropertyFlags 偏移查找
int32_t FindPropertyFlagsOffset()
{
std::vector<std::pair<void*, EPropertyFlags>> Infos;
UEStruct Guid = ObjectArray::FindStructFast("Guid");
UEStruct Color = ObjectArray::FindStructFast("Color");
// FGuid 成员的标志组合
constexpr EPropertyFlags GuidMemberFlags =
EPropertyFlags::Edit | // 可在编辑器中编辑
EPropertyFlags::ZeroConstructor | // 零初始化
EPropertyFlags::SaveGame | // 保存到存档
EPropertyFlags::IsPlainOldData | // POD 类型
EPropertyFlags::NoDestructor | // 无析构函数
EPropertyFlags::HasGetValueTypeHash; // 有哈希函数
// FColor 成员的标志组合
constexpr EPropertyFlags ColorMemberFlags =
EPropertyFlags::Edit |
EPropertyFlags::BlueprintVisible | // 蓝图可见
EPropertyFlags::ZeroConstructor |
EPropertyFlags::SaveGame |
EPropertyFlags::IsPlainOldData |
EPropertyFlags::NoDestructor |
EPropertyFlags::HasGetValueTypeHash;
Infos.push_back({ Guid.FindMember("A").GetAddress(), GuidMemberFlags });
Infos.push_back({ Color.FindMember("R").GetAddress(), ColorMemberFlags });
// UE5 某些版本 FColor::R 变成了小写 r
if (Infos[1].first == nullptr)
Infos[1].first = Color.FindMember("r").GetAddress();
int FlagsOffset = FindOffset(Infos);
// 如果找不到,尝试添加 NativeAccessSpecifierPublic 标志
if (FlagsOffset == OffsetNotFound)
{
Infos[0].second |= EPropertyFlags::NativeAccessSpecifierPublic;
Infos[1].second |= EPropertyFlags::NativeAccessSpecifierPublic;
FlagsOffset = FindOffset(Infos);
}
return FlagsOffset;
}
10.5 FindOffsetInternalOffset - Offset_Internal 偏移查找
int32_t FindOffsetInternalOffset()
{
std::vector<std::pair<void*, int32_t>> Infos;
const UEStruct Color = ObjectArray::FindStructFast("Color");
const UEStruct Guid = ObjectArray::FindStructFast("Guid");
// FColor 的布局: { B, G, R, A } 各 1 字节
Infos.push_back({ Color.FindMember("B").GetAddress(), 0x00 }); // B 在偏移 0
Infos.push_back({ Color.FindMember("G").GetAddress(), 0x01 }); // G 在偏移 1
// FGuid 的布局: { A, B, C, D } 各 4 字节
Infos.push_back({ Guid.FindMember("C").GetAddress(), 0x08 }); // C 在偏移 8
return FindOffset(Infos);
}
结构体内存布局参考
FColor:
偏移 0x00: B (uint8)
偏移 0x01: G (uint8)
偏移 0x02: R (uint8)
偏移 0x03: A (uint8)
总大小: 4 字节
FGuid:
偏移 0x00: A (uint32)
偏移 0x04: B (uint32)
偏移 0x08: C (uint32)
偏移 0x0C: D (uint32)
总大小: 16 字节
11. 特殊属性类型偏移查找
不同类型的属性有额外的成员,需要单独查找。
11.1 FindBoolPropertyBaseOffset - BoolProperty 基础偏移
启发式原理
BoolProperty 有特殊的位域信息结构。
int32_t FindBoolPropertyBaseOffset()
{
std::vector<std::pair<void*, uint8_t>> Infos;
UEClass Engine = ObjectArray::FindClassFast("Engine");
// bIsOverridingSelectedColor: 独立 bool,FieldMask = 0xFF
Infos.push_back({
Engine.FindMember("bIsOverridingSelectedColor").GetAddress(),
0xFF // 完整字节掩码
});
// bEnableOnScreenDebugMessagesDisplay: 位域 bool,FieldMask = 0b00000010
Infos.push_back({
Engine.FindMember("bEnableOnScreenDebugMessagesDisplay").GetAddress(),
0b00000010 // 第 2 位
});
// bAutoManageActiveCameraTarget: 独立 bool
Infos.push_back({
ObjectArray::FindClassFast("PlayerController").FindMember("bAutoManageActiveCameraTarget").GetAddress(),
0xFF
});
// 找到 FieldMask 的偏移,然后减去 3 得到 Base 偏移
return FindOffset<1>(Infos, Off::Property::Offset_Internal) - 0x3;
}
UBoolPropertyBase 结构
struct UBoolPropertyBase
{
uint8 FieldSize; // 字段大小(1 或 4)
uint8 ByteOffset; // 在字节中的偏移
uint8 ByteMask; // 字节掩码
uint8 FieldMask; // 字段掩码 ← 我们搜索的值
};
位域 bool 示例
// 多个 bool 打包在一个字节中
struct Example
{
uint8 bFlag1 : 1; // FieldMask = 0b00000001
uint8 bFlag2 : 1; // FieldMask = 0b00000010
uint8 bFlag3 : 1; // FieldMask = 0b00000100
// ...
};
11.2 FindObjectPropertyClassOffset - ObjectProperty::PropertyClass 偏移
int32_t FindObjectPropertyClassOffset()
{
std::vector<std::pair<void*, void*>> Infos;
const UEClass Controller = ObjectArray::FindClassFast("Controller");
// Controller::PlayerState 的类型是 APlayerState*
Infos.push_back({
Controller.FindMember("PlayerState").GetAddress(),
ObjectArray::FindClassFast("PlayerState").GetAddress()
});
// Controller::Pawn 的类型是 APawn*
Infos.push_back({
Controller.FindMember("Pawn").GetAddress(),
ObjectArray::FindClassFast("Pawn").GetAddress()
});
// World::PersistentLevel 的类型是 ULevel*
Infos.push_back({
ObjectArray::FindClassFast("World").FindMember("PersistentLevel").GetAddress(),
ObjectArray::FindClassFast("Level").GetAddress()
});
return FindOffset(Infos, Off::Property::Offset_Internal);
}
11.3 FindEnumPropertyBaseOffset - EnumProperty 基础偏移
int32_t FindEnumPropertyBaseOffset()
{
std::vector<std::pair<void*, const void*>> Infos;
// 找到枚举类型
const void* ComponentCreationMethod = ObjectArray::FindObjectFast(
"EComponentCreationMethod", EClassCastFlags::Enum).GetAddress();
const void* AutoPossessAI = ObjectArray::FindObjectFast(
"EAutoPossessAI", EClassCastFlags::Enum).GetAddress();
// UE4.15 及以下没有 EnumProperty
if (!ComponentCreationMethod || !AutoPossessAI)
return OffsetNotFound;
// 找到使用这些枚举的属性
void* CreationMethodMember = ObjectArray::FindClassFast("ActorComponent")
.FindMember("CreationMethod", EClassCastFlags::EnumProperty).GetAddress();
void* AutoPossessAIMember = ObjectArray::FindClassFast("Pawn")
.FindMember("AutoPossessAI", EClassCastFlags::EnumProperty).GetAddress();
Infos.push_back({ CreationMethodMember, ComponentCreationMethod });
Infos.push_back({ AutoPossessAIMember, AutoPossessAI });
// EnumProperty::Enum 是第 2 个成员(第 1 个是 UnderlayingProperty)
return FindOffset(Infos, Off::Property::Offset_Internal) - sizeof(void*);
}
EnumProperty 结构
struct UEnumPropertyBase
{
UNumericProperty* UnderlayingProperty; // 底层数值类型
UEnum* Enum; // 枚举类型指针 ← 我们搜索的值
};
11.4 FindBytePropertyEnumOffset - ByteProperty::Enum 偏移
int32_t FindBytePropertyEnumOffset()
{
std::vector<std::pair<void*, const void*>> Infos;
// ECollisionResponse 枚举
const void* CollisionResponseEnum = ObjectArray::FindObjectFast(
"ECollisionResponse", EClassCastFlags::Enum).GetAddress();
// FCollisionResponseContainer 结构体使用了这个枚举
const UEStruct CollisionResponseContainer = ObjectArray::FindStructFast("CollisionResponseContainer");
// GameTraceChannel1 和 GameTraceChannel2 都是 ECollisionResponse 类型
const void* GameTraceChannel1 = CollisionResponseContainer
.FindMember("GameTraceChannel1", EClassCastFlags::ByteProperty).GetAddress();
const void* GameTraceChannel2 = CollisionResponseContainer
.FindMember("GameTraceChannel2", EClassCastFlags::ByteProperty).GetAddress();
Infos.push_back({ const_cast<void*>(GameTraceChannel1), CollisionResponseEnum });
Infos.push_back({ const_cast<void*>(GameTraceChannel2), CollisionResponseEnum });
return FindOffset(Infos, Off::Property::Offset_Internal);
}
11.5 FindStructPropertyStructOffset - StructProperty::Struct 偏移
int32_t FindStructPropertyStructOffset()
{
std::vector<std::pair<void*, const void*>> Infos;
// FVector 结构体
const void* VectorClass = ObjectArray::FindStructFast("Vector").GetAddress();
// FTwoVectors 包含两个 FVector 成员
const UEStruct TwoVectorsStruct = ObjectArray::FindStructFast("TwoVectors");
// v1 和 v2 都是 FVector 类型
const void* v1 = TwoVectorsStruct.FindMember("v1", EClassCastFlags::StructProperty).GetAddress();
const void* v2 = TwoVectorsStruct.FindMember("v2", EClassCastFlags::StructProperty).GetAddress();
Infos.push_back({ const_cast<void*>(v1), VectorClass });
Infos.push_back({ const_cast<void*>(v2), VectorClass });
return FindOffset(Infos, Off::Property::Offset_Internal);
}
11.6 FindDelegatePropertySignatureFunctionOffset - DelegateProperty::SignatureFunction 偏移
int32_t FindDelegatePropertySignatureFunctionOffset()
{
std::vector<std::pair<void*, const void*>> Infos;
// 委托签名函数
const void* DelegateSignature = ObjectArray::FindObjectFast(
"TimerDynamicDelegate__DelegateSignature", EClassCastFlags::Function).GetAddress();
// 使用该委托的属性
const void* Delegate1 = ObjectArray::FindObjectFast<UEFunction>(
"K2_GetTimerElapsedTimeDelegate", EClassCastFlags::Function)
.FindMember("Delegate", EClassCastFlags::DelegateProperty).GetAddress();
const void* Delegate2 = ObjectArray::FindObjectFast<UEFunction>(
"K2_GetTimerRemainingTimeDelegate", EClassCastFlags::Function)
.FindMember("Delegate", EClassCastFlags::DelegateProperty).GetAddress();
Infos.push_back({ const_cast<void*>(Delegate1), DelegateSignature });
Infos.push_back({ const_cast<void*>(Delegate2), DelegateSignature });
return FindOffset(Infos, Off::Property::Offset_Internal);
}
11.7 容器属性偏移检测
FindInnerTypeOffset - ArrayProperty::Inner 偏移
int32_t FindInnerTypeOffset(const int32 PropertySize)
{
// 只有 FProperty 系统需要检测
if (!Settings::Internal::bUseFProperty)
return PropertySize;
// 找一个 ArrayProperty
if (const UEProperty Property = ObjectArray::FindClassFast("GameViewportClient")
.FindMember("DebugProperties", EClassCastFlags::ArrayProperty))
{
// 检查 PropertySize 偏移处是否是有效指针
void* AddressToCheck = *(void**)(Property.GetAddress() + PropertySize);
// 如果不是有效指针,说明有额外的 8 字节填充
if (Platform::IsBadReadPtr(AddressToCheck))
return PropertySize + sizeof(void*);
}
return PropertySize;
}
背景说明
UE4.25+ 的 FProperty 系统中,某些属性类型有额外的填充字节,这是因为 FProperty 不再继承自 UObject,内存布局发生了变化。
12. FField 系统偏移查找
UE4.25 引入了 FField 系统,属性不再继承自 UObject。
12.1 FField 内存布局
FField:
┌─────────────────────────────────────┐
│ 偏移 0x00: VTable 指针 │
│ 偏移 0x08: Class (FFieldClass*) │
│ 偏移 0x10: Owner (FFieldVariant) │ ← 8 或 16 字节
│ 偏移 0x18/0x20: Next (FField*) │
│ 偏移 0x20/0x28: Name (FName) │
│ 偏移 0x28/0x30: Flags (uint32) │
└─────────────────────────────────────┘
12.2 FindFFieldNextOffset - FField::Next 偏移查找
int32_t FindFFieldNextOffset()
{
// FGuid 和 FVector 的第一个属性
const void* GuidChildren = ObjectArray::FindStructFast("Guid").GetChildProperties().GetAddress();
const void* VectorChildren = ObjectArray::FindStructFast("Vector").GetChildProperties().GetAddress();
// 从 Owner 偏移之后开始搜索
return GetValidPointerOffset(GuidChildren, VectorChildren, Off::FField::Owner + 0x8, 0x48);
}
12.3 FindFFieldNameOffset - FField::Name 偏移查找
int32_t FindFFieldNameOffset()
{
UEFField GuidChild = ObjectArray::FindStructFast("Guid").GetChildProperties();
UEFField VectorChild = ObjectArray::FindStructFast("Vector").GetChildProperties();
// FGuid 的第一个成员应该是 "A" 或 "D"
// FVector 的第一个成员应该是 "X" 或 "Z"
std::string GuidChildName = GuidChild.GetName();
std::string VectorChildName = VectorChild.GetName();
// 如果名称正确,返回当前偏移
if ((GuidChildName == "A" || GuidChildName == "D") &&
(VectorChildName == "X" || VectorChildName == "Z"))
return Off::FField::Name;
// 否则遍历搜索
for (Off::FField::Name = Off::FField::Owner; Off::FField::Name < 0x40; Off::FField::Name += 4)
{
GuidChildName = GuidChild.GetName();
VectorChildName = VectorChild.GetName();
if ((GuidChildName == "A" || GuidChildName == "D") &&
(VectorChildName == "X" || VectorChildName == "Z"))
return Off::FField::Name;
}
return OffsetNotFound;
}
为什么检查 A/D 和 X/Z
- FGuid 的成员顺序可能是
{A,B,C,D}或{D,C,B,A} - FVector 的成员顺序可能是
{X,Y,Z}或{Z,Y,X} - 取决于编译器和平台的字节序
12.4 FixupHardcodedOffsets - 修正硬编码偏移 ⭐
启发式原理
检测 FFieldVariant 的布局变化。
void FixupHardcodedOffsets()
{
// 1. 修正 CasePreservingName 的偏移
if (Settings::Internal::bUseCasePreservingName)
{
Off::FField::Flags += 0x8;
Off::FFieldClass::Id += 0x08;
Off::FFieldClass::CastFlags += 0x08;
Off::FFieldClass::ClassFlags += 0x08;
Off::FFieldClass::SuperClass += 0x08;
}
// 2. 检测 FFieldVariant 布局
if (Settings::Internal::bUseFProperty)
{
/*
* UE5.1.1 之前: class FFieldVariant { void* Ptr; bool bIsUObject; }
* 扩展为: { void*, bool, uint8[7] } = 16 字节
*
* UE5.1.1 之后: class FFieldVariant { void* Ptr; }
* 只有 8 字节,通过指针最低位判断类型
*/
const int32 OffsetToCheck = Off::FField::Owner + 0x8;
// 检查三个类的第一个属性
void* PossibleNextPtrOrBool0 = *(void**)((uint8*)ObjectArray::FindClassFast("Actor")
.GetChildProperties().GetAddress() + OffsetToCheck);
void* PossibleNextPtrOrBool1 = *(void**)((uint8*)ObjectArray::FindClassFast("ActorComponent")
.GetChildProperties().GetAddress() + OffsetToCheck);
void* PossibleNextPtrOrBool2 = *(void**)((uint8*)ObjectArray::FindClassFast("Pawn")
.GetChildProperties().GetAddress() + OffsetToCheck);
auto IsValidPtr = [](void* a) -> bool
{
return !Platform::IsBadReadPtr(a) && (uintptr_t(a) & 0x1) == 0;
};
// 如果三个都是有效指针,说明是新版本布局
if (IsValidPtr(PossibleNextPtrOrBool0) &&
IsValidPtr(PossibleNextPtrOrBool1) &&
IsValidPtr(PossibleNextPtrOrBool2))
{
Settings::Internal::bUseMaskForFieldOwner = true;
// 调整偏移
Off::FField::Next -= 0x08;
Off::FField::Name -= 0x08;
Off::FField::Flags -= 0x08;
}
}
}
FFieldVariant 布局变化图解
UE5.1.1 之前 (16字节):
┌────────────────────────────────────┐
│ void* Ptr (8 bytes) │
│ bool bIsUObject (1 byte) │
│ padding (7 bytes) │
└────────────────────────────────────┘
UE5.1.1 之后 (8字节):
┌────────────────────────────────────┐
│ void* Ptr (8 bytes) │
│ (最低位 = 1 表示是 UObject) │
└────────────────────────────────────┘
13. SDK 相关偏移查找
这些偏移用于生成的 SDK 中访问特定数据。
13.1 FindLevelActorsOffset - ULevel::Actors 偏移
int32_t FindLevelActorsOffset()
{
// 找到一个 ULevel 实例(非 CDO)
UEObject Level = nullptr;
uintptr_t Lvl = 0x0;
for (auto Obj : ObjectArray())
{
if (!Obj.HasAnyFlags(EObjectFlags::ClassDefaultObject) && Obj.IsA(EClassCastFlags::Level))
{
Level = Obj;
Lvl = reinterpret_cast<uintptr_t>(Obj.GetAddress());
break;
}
}
if (Lvl == 0x0)
return OffsetNotFound;
/*
* ULevel 布局:
* class ULevel : public UObject
* {
* FURL URL;
* TArray<AActor*> Actors; // <-- 我们要找的
* TArray<AActor*> GCActors;
* ...
* UWorld* OwningWorld;
* };
*/
// 获取搜索范围
UEClass UObjectClass = ObjectArray::FindClassFast("Object");
const UEStruct FURLStruct = ObjectArray::FindObjectFast<UEStruct>("URL", EClassCastFlags::Struct);
const UEProperty Level_OwningWorldProperty = Level.GetClass().FindMember("OwningWorld");
const int32 SearchStart = UObjectClass.GetStructSize() + FURLStruct.GetStructSize();
const int32 SearchEnd = Level_OwningWorldProperty.GetOffset();
// 遍历查找有效的 TArray
for (int i = SearchStart; i <= (SearchEnd - 0x10); i += sizeof(void*))
{
const TArray<void*>& ActorArray = *reinterpret_cast<TArray<void*>*>(Lvl + i);
// 验证数组有效性
if (ActorArray.IsValid() && !Platform::IsBadReadPtr(ActorArray.GetDataPtr()))
{
return i;
}
}
return OffsetNotFound;
}
13.2 FindDatatableRowMapOffset - UDataTable::RowMap 偏移
int32_t FindDatatableRowMapOffset()
{
const UEClass DataTable = ObjectArray::FindClassFast("DataTable");
constexpr int32 UObjectOuterSize = sizeof(void*);
constexpr int32 RowStructSize = sizeof(void*);
if (!DataTable)
{
// 使用默认布局
return Off::UObject::Outer + UObjectOuterSize + RowStructSize;
}
// 找到 RowStruct 属性
UEProperty RowStructProp = DataTable.FindMember("RowStruct", EClassCastFlags::ObjectProperty);
if (!RowStructProp)
{
return Off::UObject::Outer + UObjectOuterSize + RowStructSize;
}
// RowMap 紧跟在 RowStruct 之后
return RowStructProp.GetOffset() + RowStructProp.GetSize();
}
13.3 ProcessEvent 函数查找
void Off::InSDK::ProcessEvent::InitPE_Windows()
{
void** Vft = *(void***)ObjectArray::GetByIndex(0).GetAddress();
// 主要检测方法:在虚函数表中查找包含特定模式的函数
auto IsProcessEvent = [](const uint8_t* FuncAddress, int32_t Index) -> bool
{
// ProcessEvent 内部会检查 UFunction::FunctionFlags
// 查找访问 FunctionFlags 偏移的指令模式
return Platform::FindPatternInRange(
{ 0xF7, -0x1, Off::UFunction::FunctionFlags, 0x0, 0x0, 0x0, 0x0, 0x04, 0x0, 0x0 },
FuncAddress, 0x400)
&& Platform::FindPatternInRange(
{ 0xF7, -0x1, Off::UFunction::FunctionFlags, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0 },
FuncAddress, 0xF00);
};
// 遍历虚函数表查找匹配的函数
const auto [FuncPtr, FuncIdx] = Platform::IterateVTableFunctions(Vft, IsProcessEvent);
if (FuncPtr)
{
Off::InSDK::ProcessEvent::PEIndex = FuncIdx;
Off::InSDK::ProcessEvent::PEOffset = Platform::GetOffset(FuncPtr);
return;
}
// 备选方法:通过字符串引用查找
const void* StringRefAddr = Platform::FindByStringInAllSections(L"Accessed None");
const void* PossiblePEAddr = Architecture_x86_64::FindNextFunctionStart(StringRefAddr);
// ... 继续验证
}
13.4 GWorld 查找
void Off::InSDK::World::InitGWorld()
{
UEClass UWorld = ObjectArray::FindClassFast("World");
for (UEObject Obj : ObjectArray())
{
if (Obj.HasAnyFlags(EObjectFlags::ClassDefaultObject) || !Obj.IsA(UWorld))
continue;
// 在进程内存中查找指向这个 World 对象的指针
auto Results = Platform::FindAllAlignedValuesInProcess(Obj.GetAddress());
void* Result = nullptr;
if (Results.size() == 1)
{
Result = Results[0];
}
else if (Results.size() == 2)
{
// 可能找到了 GWorld 和 GActiveLogWorld
// 通过检查值是否变化来区分
auto ObjAddress = reinterpret_cast<uintptr_t>(Obj.GetAddress());
auto PossibleGWorld = reinterpret_cast<volatile uintptr_t*>(Results[0]);
auto CurrentValue = *PossibleGWorld;
for (int i = 0; CurrentValue == ObjAddress && i < 50; ++i)
{
::Sleep(1);
CurrentValue = *PossibleGWorld;
}
if (CurrentValue == ObjAddress)
Result = Results[0]; // 值没变,是 GWorld
else
Result = Results[1]; // 值变了,是 GActiveLogWorld
}
if (Result)
{
Off::InSDK::World::GWorld = Platform::GetOffset(Result);
break;
}
}
}
14. 算法设计原则总结
14.1 核心设计原则
1. 利用已知常量
| 类型 | 示例 | 用途 |
|---|---|---|
| 结构体大小 | FColor=4, FGuid=16, FTransform=48 | 查找 Size 偏移 |
| 枚举值数量 | ENetRole=5, ETraceTypeQuery=34 | 查找 UEnum::Names |
| 标志组合 | 0x43 (常见 ObjectFlags) | 查找 Flags 偏移 |
| 对齐要求 | FTransform=16, PlayerController=8 | 查找 MinAlignment |
2. 利用类型系统特性
| 特性 | 应用 |
|---|---|
| UClass 自引用循环 | 查找 Class 偏移 |
| 继承关系 (Struct→Field) | 查找 SuperStruct 偏移 |
| CDO 命名规则 (Default__X) | 查找 ClassDefaultObject 偏移 |
| 对象索引等于数组位置 | 查找 Index 偏移 |
3. 统计学方法
| 方法 | 应用 |
|---|---|
| 值出现频率 | Flags 值 0x43 的出现频率 |
| 平均值范围 | FName::ComparisonIndex 的平均值 |
| 分布特征 | FName::Number 在 [1,4] 范围的比例 |
4. 指针验证
| 验证类型 | 方法 |
|---|---|
| 指针可读性 | Platform::IsBadReadPtr() |
| 虚函数表验证 | 检查 **ptr 是否可读 |
| 进程内存范围 | Platform::IsAddressInProcessRange() |
| 指针对齐 | 检查最低位是否为 0 |
5. 多重验证
| 策略 | 目的 |
|---|---|
| 多对象交叉验证 | 避免单个对象的特殊情况 |
| 排除已知偏移 | 避免重复匹配 |
| 备选方案 | 处理版本差异 |
| 随机采样 | 避免选中特殊对象 |
14.2 版本兼容策略
UProperty vs FProperty (UE4.25 分界)
// 检测方法:查找 ObjectProperty 对象
if (ObjectArray::FindObject("ObjectProperty Engine.Controller.TransformComponent"))
{
Settings::Internal::bUseFProperty = false; // 旧版本
}
else
{
Settings::Internal::bUseFProperty = true; // 新版本
}
FName 变体检测
// 通过 FName 大小和内部值判断
if (FNameSize == 0x8 && FNameFirstInt == FNameSecondInt)
// CASE_PRESERVING + OUTLINE_NUMBER
else if (FNameSize == 0x10)
// CASE_PRESERVING
else if (GetNumNamesWithNumberOneToFour() < Threshold)
// OUTLINE_NUMBER
else
// 默认
FFieldVariant 布局检测 (UE5.1.1 分界)
// 检测方法:检查 Owner+0x8 处是否是有效指针
void* PossibleNextPtr = *(void**)(FField + Off::FField::Owner + 0x8);
if (IsValidPtr(PossibleNextPtr))
// 新版本:FFieldVariant 只有 8 字节
else
// 旧版本:FFieldVariant 有 16 字节
14.3 错误处理策略
1. 默认值回退
auto OverwriteIfInvalidOffset = [](int32& Offset, int32 DefaultValue)
{
if (Offset == OffsetNotFound)
{
std::cerr << std::format("Defaulting to offset: 0x{:X}\n", DefaultValue);
Offset = DefaultValue;
}
};
Off::UObject::Flags = OffsetFinder::FindUObjectFlagsOffset();
OverwriteIfInvalidOffset(Off::UObject::Flags, sizeof(void*)); // 默认在 VTable 之后
2. 备选搜索对象
// 某些游戏没有 SwitchLevel,用 FOV 替代
if (Infos[2].first == nullptr)
Infos[2].first = ObjectArray::FindObjectFast("FOV").GetAddress();
3. 后期修正
// PostInitFNameSettings 修正 InitFNameSettings 的错误
void PostInitFNameSettings()
{
const int32 ActualFNameSize = PlayerStart.FindMember("PlayerStartTag").GetSize();
if (Off::InSDK::Name::FNameSize != ActualFNameSize)
{
// 修正偏移
Off::FField::Flags = (Off::FField::Flags - Off::InSDK::Name::FNameSize) + ActualFNameSize;
// 重新检测设置
// ...
}
}
14.4 性能优化
1. 分段搜索
// 首先只扫描 .data 段(速度快)
GObjectsAddress = Platform::IterateSectionWithCallback(
Platform::GetSectionInfo(".data"), IsAddressValidGObjects);
// 如果没找到,再扫描所有段
if (!GObjectsAddress)
GObjectsAddress = Platform::IterateAllSectionsWithCallback(IsAddressValidGObjects);
2. 早期退出
// 找到足够多的匹配就返回
if (NumObjectsWithFlagAtOffset > MinNumFlagValuesRequiredAtOffset)
return Offset;
3. 范围限制
// 限制搜索范围
const int32_t MinOffset = Off::Property::ElementSize - 0x10;
const int32_t MaxOffset = Off::Property::ElementSize + 0x10;
return FindOffset(Infos, MinOffset, MaxOffset);
附录:偏移量汇总表
UObject 偏移
| 成员 | 默认偏移 (64位) | 查找方法 |
|---|---|---|
| VTable | 0x00 | 固定 |
| Flags | 0x08 | 统计 0x43 出现频率 |
| Index | 0x0C | 匹配数组索引 |
| Class | 0x10 | 循环引用检测 |
| Name | 0x18 | ComparisonIndex 统计 |
| Outer | 0x20 | 有效指针查找 |
UStruct 偏移
| 成员 | 查找方法 |
|---|---|
| SuperStruct | 匹配继承关系 |
| Children | 匹配子成员指针 |
| ChildProperties | 有效指针查找 |
| Size | 匹配已知结构体大小 |
| MinAlignment | 匹配已知对齐要求 |
UClass 偏移
| 成员 | 查找方法 |
|---|---|
| CastFlags | 匹配已知类型标志 |
| ClassDefaultObject | 匹配 CDO 指针 |
| ImplementedInterfaces | 验证接口数组内容 |
Property 偏移
| 成员 | 查找方法 |
|---|---|
| ArrayDim | 匹配值 1 |
| ElementSize | 匹配已知属性大小 |
| PropertyFlags | 匹配已知标志组合 |
| Offset_Internal | 匹配结构体内偏移 |
文档版本: 1.0 最后更新: 2026-01-09 基于 Dumper-7 源码分析
评论
- 还没有评论,来说点什么吧。