https://www.52pojie.cn/thread-1838396-1-1.html 逆向 UE4 方法
- UnrealDumper-4.25,适用于 Windows,早期版本的 dump 工具
- UEDumper,适用于 Windows,强大的工具
- UE4Dumper,适用于 Android 的简单 dump 数据工具
- AndUE4Dumper,适用于 Android 的可扩展 dump 工具,可导入 Android Studio 食用
- iOS_UE4Dumper,适用于 iOS 的可扩展 dump 工具,可导入 Xcode 食用
- UE4SDKGenerator,适用于 Android,强大的工具
https://github.com/s4m33r89/UE4SDKGenerator
https://github.com/MJx0/AndUE4Dumper
尝试使用该工具 dump 出来东西(这事啥,源码吗),这么厉害啊
libUE4.so
还是从这里搞出来的,只是有别人编好的数据了
还是从底层原理看起
https://blog.csdn.net/jiangdengc/article/details/68064895
好像明白了,大概就是找到一些偏移,能够完整导出某些 UE4 的数据出来
https://iosre.com/t/ue4%E6%B8%B8%E6%88%8F%E4%BB%8E%E4%BD%95%E4%B8%8B%E6%89%8B%EF%BC%9F/17755/15
上面说到,World 是一个大容器,包含了游戏中的所有关卡,如果内存中找到这个地址,那么 Actor、Pawn 等都能遍历到。那么如何找这个基址呢?起初我也是费了老些劲,后来了解了一些 UE4 关键术语意思(Actor、Pawn、Character 等),加上各大论坛到处逛,最后总结到经验:通过一定的特征,找到大概的位置,然后参考 github 虚幻 4 源代码和 IDA 中汇编逻辑回溯找到 World 基址。 那这一定的特征是什么呢?字符串!!! 在源代码中找到引用 GWorld 变量的附近的字符串。 在源码
4.20/Engine/Source/Runtime/Engine/Private/World.cpp
中SeamlessTravel FlushLevelStreaming
字符串与 GWorld 引用非常近,可以作为特征
还是非常不懂,接下来的任务就是尝试自己搞一个 demo 出来看看逻辑是怎么样
只能搞出来 demo 源码,编译打包太耗时了,电脑爆炸了。不过这个也够看了
符号恢复: http://www.yxfzedu.com/article/670
现在才知道 CE 是有 mac 版本的,也是可以跑一个 netserver 在安卓上的,用这个来扫描可太方便了
https://www.bilibili.com/video/BV1N34y1M7eS 这个能够看明白怎么操作了
dump 需要的两个东西是 GUObjectArray、GName
https://www.win32k.cn/d/46-ue4zui-wan-zheng-de-ni-xiang-zhi-nan-ji-shi-pin-jiao-cheng
NamePoolData 的查找方式非常简单 你只需要用 CE 搜索字符串 MulticastDelegateProperty 注意并非宽字符 浏览找到的地址的内存区域,并找到您在其中读取类似“None,ByteProperty,someProperty ecc”的内容 None 的前面应该是 _.None 只需要搜索指向 _.None 的首地址 即可查找到 NamePoolData 此时的内存偏移指向了 uint8* Blocks[FNameMaxBlocks] = {}; 再将地址 -0x10 即 NamePoolData 地址 如果这个方法失效 可以使用其他方式查找 NamePoolData NamePoolData 并不太重要并且几乎所有游戏都可以使用这种办法 也就不再继续过多描述 这有一段 youtube 视频讲解了这个办法
Game Object 就有点复杂了,等会再看,先自己试一下
AndUE4Dumper 尝试编译这个项目
在 mac 环境下修改 build 脚本:
// ndk-build.bat
#!/bin/bash
# Remove build directory if it exists
if [ -d "build" ]; then
rm -rf build
fi
# Create build directory
mkdir -p build
# Run make
make
# Move build artifacts
mv libs build/libs
mv obj build/obj
更新依赖,修改环境变量
git submodule update --init --recursive
export NDK_HOME=/Users/cyril/Library/Android/sdk/ndk/27.0.12077973
然后运行构建脚本就行了
把 push 也改了
#!/bin/sh
echo "pushing dumper binaries..."
adb push build/libs/arm64-v8a/UE4Dump3r_arm64 /data/local/tmp
adb shell "su -c 'chmod 755 /data/local/tmp/UE4Dump3r_arm64'"
adb push build/libs/armeabi-v7a/UE4Dump3r_arm /data/local/tmp
adb shell "su -c 'chmod 755 /data/local/tmp/UE4Dump3r_arm'"
adb push build/libs/x86/UE4Dump3r_x86 /data/local/tmp
adb shell "su -c 'chmod 755 /data/local/tmp/UE4Dump3r_x86'"
adb push build/libs/x86_64/UE4Dump3r_x86_64 /data/local/tmp
adb shell "su -c 'chmod 755 /data/local/tmp/UE4Dump3r_x86_64'"
# test on arm64 device
adb shell "su -c '/data/local/tmp/UE4Dump3r_arm64 -o /sdcard'"
read -p "Press enter to continue"
试一圈下来都不行,还是要自己写了 DS_ERROR_INIT_GUOBJECTARRAY
首先是结构体的学习(略)
GNames 指的是 static uint8 NamePoolData[sizeof(FNamePool)];
查找 MulticastDelegateProperty
很奇怪,就是搜不到相应的字符串
https://www.cnblogs.com/revercc/p/17641855.html
这个教程好像有用,虽然是 UE5,但是好像对得上
- ida 中搜索字符串 ByteProperty,转到交叉引用处就是 FNamePool 构造函数,传入的参数就是 NamePool
- ida 找到
CloseDisregardForGC
字符串引用处,下面的函数传入的参数就是 GUObjectArray
#!/bin/sh
adb push libs/arm64-v8a/ue4dumper64 /data/local/tmp/ue4dumper64
adb shell "su -c 'chmod 755 /data/local/tmp/ue4dumper64'"
adb shell "su -c '/data/local/tmp/ue4dumper64 --newue+ --strings --gname 0xADF07C0 --package com.ACE2025.Game --output /sdcard/Download'"
adb shell "su -c '/data/local/tmp/ue4dumper64 --newue+ --objs --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game --output /sdcard/Download'"
dump 下来了!
但是还想导入 ida 里面看,还得改之前的那个
#pragma once
#include "../GameProfile.h"
// eFootBall
// UE 4.25+ ??
class PESProfile : public IGameProfile
{
public:
PESProfile() = default;
virtual bool ArchSupprted() const override
{
auto e_machine = GetUE4ELF().header().e_machine;
// arm & arm64
return e_machine == EM_AARCH64 || e_machine == EM_ARM;
}
std::string GetAppName() const override
{
return "eFootball 2023";
}
std::vector<std::string> GetAppIDs() const override
{
return { "jp.konami.pesam" };
}
bool IsUsingFNamePool() const override
{
return true;
}
uintptr_t GetGUObjectArrayPtr() const override
{
PATTERN_MAP_TYPE map_type = isEmulator() ? PATTERN_MAP_TYPE::ANY_R : PATTERN_MAP_TYPE::ANY_X;
std::string ida_pattern = "08 ? ? 91 E1 03 ? AA E0 03 08 AA E2 03 1F 2A";
int step = -4;
uintptr_t insn_address = findIdaPattern(map_type, ida_pattern, step);
if(insn_address == 0)
{
LOGE("GUObjectArray pattern failed.");
return 0;
}
int64_t adrp_pc_rel = 0;
int32_t add_imm12 = 0;
uintptr_t page_off = INSN_PAGE_OFFSET(insn_address);
uint32_t adrp_insn = 0, add_insn = 0;
kMgr.readMem((insn_address), &adrp_insn, sizeof(uint32_t));
kMgr.readMem((insn_address + sizeof(uint32_t)), &add_insn, sizeof(uint32_t));
if (adrp_insn == 0 || add_insn == 0)
return 0;
if (!KittyArm64::decode_adr_imm(adrp_insn, &adrp_pc_rel) || adrp_pc_rel == 0)
return 0;
add_imm12 = KittyArm64::decode_addsub_imm(add_insn);
return (page_off + adrp_pc_rel + add_imm12);
}
uintptr_t GetNamesPtr() const override
{
PATTERN_MAP_TYPE map_type = isEmulator() ? PATTERN_MAP_TYPE::ANY_R : PATTERN_MAP_TYPE::ANY_X;
std::string ida_pattern = "F4 4F 01 A9 FD 7B 02 A9 FD 83 ? 91 ? ? ? ? A8 02 ? 39";
int step = 0x24;
uintptr_t insn_address = findIdaPattern(map_type, ida_pattern, step);
if (insn_address == 0)
{
LOGE("NamePoolData pattern failed.");
return 0;
}
int64_t adrp_pc_rel = 0;
int32_t add_imm12 = 0;
uintptr_t page_off = INSN_PAGE_OFFSET(insn_address);
uint32_t adrp_insn = 0, add_insn = 0;
kMgr.readMem((insn_address), &adrp_insn, sizeof(uint32_t));
kMgr.readMem((insn_address + sizeof(uint32_t)), &add_insn, sizeof(uint32_t));
if (adrp_insn == 0 || add_insn == 0)
return 0;
if (!KittyArm64::decode_adr_imm(adrp_insn, &adrp_pc_rel) || adrp_pc_rel == 0)
return 0;
add_imm12 = KittyArm64::decode_addsub_imm(add_insn);
return (page_off + adrp_pc_rel + add_imm12);
}
UE_Offsets *GetOffsets() const override
{
... // 照抄其他的
}
};
https://github.com/Perfare/Il2CppDumper/blob/master/Il2CppDumper/ida_py3.py
把 script 字段改成 ScriptMethod,然后导入,就可以搜索到函数了
只不过大部分的函数都没有信息(算了也够了)
获取到那几个关键信息之后我们就可以着手内部结构了
GWorld
指向当前的 UWorld 对象。UWorld
是整个游戏世界的最高级别容器,包含当前关卡(Level)、玩家控制器(Controller)、角色(character)等GName
是全局名称表指针。UE 在内部不直接使用字符串表示对象的名称(如类名、属性名、函数名),而是使用一个唯一的整数FNameIndex
,来映射到 GName 表中的一个实际字符串(类似于 Android 的字符串资源)GObject
是 UE 的全局对象数组指针,是一个动态数组,包含所有已经加载到内存中的 UObject 实例,所有实体都是这个类的派生类- 获取到
UWorld
对象之后,可以获取到当前关卡下的所有 Actor 列表。Actor 是 UE 中基本的放置在世界中的对象
G → U
不同的版本下具体不一样,这里是 4.27。在内存中的 UObject 类关键内部字段的偏移量:
var offset_UObject_InternalIndex = 0xC;
是这个 UObject 在 GObject 数组中的索引var offset_UObject_ClassPrivate = 0x10;
是一个指向UClass
对象的指针,这个UClass
对象能够描述其类型和行为,获取它能够获得对象的运行时类型信息var offset_UObject_FNameIndex = 0x18;
是自身名称的 ID,是GName
表中的索引var offset_UObject_OuterPrivate = 0x20;
是指向外部对象 Outer 的指针,就是它的所属
ID→ name 脚本如下:
function getFNameFromID(index) {
var FNameStride = 0x2 // 虚幻引擎4.25+ 名称表Entry的步长,老版本可能是0x4
var offset_GName_FNamePool = 0x30; // GName 对象内部指向 FNamePool 的偏移
var offset_FNamePool_Blocks = 0x10; // FNamePool 内部指向 FNameEntry 块数组的偏移
var offset_FNameEntry_Info = 0; // FNameEntry 结构体中,名称信息(长度、宽字符标志)的偏移
var FNameEntry_LenBit = 6; // 名称长度信息存储在 FNameEntryHeader 的第6位及以上
var offset_FNameEntry_String = 0x2; // FNameEntry 结构体中,实际字符串数据区的偏移
var Block = index >> 16; // FNameIndex 的高16位表示其所在的内存块(Block)
var Offset = index & 65535; // FNameIndex 的低16位表示该块内的偏移量
var FNamePool = GName.add(offset_GName_FNamePool); // 获取 FNamePool 对象指针
// 从 FNamePool 内部的 Blocks 数组中,根据 Block 索引找到对应内存块的指针
// 每个块指针占用 8 字节(QWORD)
var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();
// 在找到的内存块中,根据 Offset 和 FNameStride(每个 FNameEntry 的大小)计算出具体 FNameEntry 的地址
var FNameEntry = NamePoolChunk.add(FNameStride * Offset);
try {
if (offset_FNameEntry_Info !== 0) {
// 读取 FNameEntryHeader。通常 FNameEntry_Info 偏移为 0,即直接从 FNameEntry 地址开始读取。
var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();
} else {
var FNameEntryHeader = FNameEntry.readU16();
}
} catch(e) {
return ""; // 发生错误时返回空字符串,避免崩溃
}
var str_addr = FNameEntry.add(offset_FNameEntry_String); // 实际字符串数据的起始地址
var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 通过右移 FNameEntryHeader 获取字符串长度
var wide = FNameEntryHeader & 1; // 通过与1操作判断是否为宽字符(Unicode)
if (wide) return "widestr"; // 如果是宽字符,目前只返回 "widestr" 占位符
if (str_length > 0 && str_length < 250) { // 检查字符串长度是否合理,避免读取无效内存
var str = str_addr.readUtf8String(str_length); // 读取 UTF8 编码的字符串
return str;
} else {
return "None"; // 长度不合理或为零时返回 "None"
}
}
反编译找到的三个 G,需要在运行时找到内存中的实例(文件偏移 →内存)
function set(modulename) {
moduleBase = Module.findBaseAddress(modulename);
GWorld = moduleBase.add(GWorld_Ptr_Offset).readPointer();
GName = moduleBase.add(GName_Offset);
GObjects = moduleBase.add(GObjects_Offset);
}
GWorld
对象中有一个指向当前 ULevel
对象的指针(偏移量 0x30
)。ULevel
代表游戏中的一个关卡实例。在 ULevel
对象中,有一个动态数组,存储了当前关卡中所有 AActor
实例的指针。
虚幻引擎的动态数组(TArray
)通常是 指针
+ Num
+ Max
的结构。所以,紧跟在数组指针后面 8 字节(sizeof(void*)
)的位置,存储着数组的当前元素数量 (Actors_Num
),是一个 32 位无符号整数。
GWorld → 遍历所有 Actor:
function getActorsAddr(){
var Level_Offset = 0x30 // UWorld 内部指向当前 ULevel 的偏移
var Actors_Offset = 0x98 // ULevel 内部指向 Actor 数组的偏移
var Level = GWorld.add(Level_Offset).readPointer() // 从 GWorld 获取当前 Level 指针
var Actors = Level.add(Actors_Offset).readPointer() // 从 Level 获取 Actor 数组的起始指针
var Actors_Num = Level.add(Actors_Offset).add(8).readU32() // Actor 数组的元素数量,通常紧跟在数组指针后面 8 字节
var actorsAddr = {}; // 创建一个空对象来存储 Actor 名称和地址的映射
for(var index = 0; index < Actors_Num; index++){
var actor_addr = Actors.add(index * 8).readPointer() // 遍历数组,每个 Actor 指针占用 8 字节
var actorName = GUObject.getName(actor_addr) // 获取 Actor 的名称
actorsAddr[actorName] = actor_addr; // 将 Actor 名称作为键,地址作为值存入对象
}
return actorsAddr; // 返回所有 Actor 的名称到地址的映射
}