溺的文档
Dumper-7 · 第 1 篇 / 共 4 篇

Dumper-7 偏移推断算法

2026-06-30 · 阅读 11

目录

  1. 项目概述
  2. 整体架构
  3. 核心工具函数
  4. GObjects 全局对象数组查找
  5. UObject 成员偏移查找
  6. FName 系统检测
  7. UStruct 成员偏移查找
  8. UFunction 成员偏移查找
  9. UClass 成员偏移查找
  10. Property 成员偏移查找
  11. 特殊属性类型偏移查找
  12. FField 系统偏移查找
  13. SDK 相关偏移查找
  14. 算法设计原则总结

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;
}

为什么这个方法可靠

  1. 唯一性:只有 Class 指针会形成这种循环引用
  2. 普遍性:所有 UE4 版本都有这个特性
  3. 验证性:通过两个不同对象交叉验证,避免误判

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 继承自 UField
  • UClass 继承自 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 源码分析

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。