文章总结: 本文介绍纯静态分析dumpSDK加密数据的方法,动态定位加密地址,修复UE引擎结构体如fproperty、ffield、fname等,并给出各字段的解密算法。通过定位关键函数和字符串,逐步还原结构体偏移与解密逻辑,为游戏逆向提供可操作步骤。
综合评分: 85
文章分类: 逆向分析,二进制安全,实战经验
纯静态分析DumpSDK所需的加密数据,动态定位加密UWorld、GameInstance、Object的未加密地址
DannysMask
DannysMask
看雪学苑
2026年9月25日 10:00
上海
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
0、如何Dump
因为游戏采用了一种执行时解密技术,相关技术介绍文章:击败 Theia 反篡改完整脱壳方案(Arc Raiders PioneerGame.exe),当然内容仅提供思路,如果照搬原文的解法,其实已经很早失效了。后续考虑单开一章进行该技术的讲解。其实直接Dump就可以获取到我们所需的所有数据的。
1、修复结构体
FProperty.Size:
在之前的文章中我们已经演示了通过字符串:aMissingtablein定位(定位到函数execGetDataTableRowFromName),原理这里我们就不再赘述。
那么我们这里就拿到了FProperty结构体的大小。
FProperty.ElementSize:
我们直接进入我们的TheSameLayout函数后他的内部会调用ArePropertiesTheSame函数,从中可以拿到我们的ElementSize。
因为ArePropertiesTheSame基本在所有UE引擎游戏中都是直接内联的,所以在ida中就是以下这种状态:
FORCEINLINE int32 GetSize() const
{
return ArrayDim * ElementSize;
}
我们对这一行按下tab键后获取ArrayDim和ElementSize的偏移:
我们这里并不能确定ElementSize是其中哪一个,只能通过注入后看ElementSize的值或者动态分析才能确定。当然肯定是两者其一。
这里的第一个if对应函数确实是我们FField结构体下Name的偏移,但是这里并不好直接复制解密算法,我们在其他位置找能直接复制的。
FORCEINLINE FName GetFName() const
{
if (this != nullptr)
{
return NamePrivate;// FField.NamePrivate
}
else
{
return NAME_None;
}
}
UStruct.PropertiesSize
我们回到execGetDataTableRowFromName中依次定位->Generic_GetDataTableRowFromName->CopyScriptStruct
其中调用了:
int32 Stride = GetStructureSize();
FORCEINLINE int32 GetStructureSize() const
{
return Align(PropertiesSize,MinAlignment);
}
template <typename T>
FORCEINLINE constexpr T Align(T Val, uint64 Alignment){
static_assert(TIsIntegral<T>::Value || TIsPointer<T>::Value, "Align expects an integer or pointer type");
return (T)(((uint64)Val + Alignment - 1) & ~(Alignment - 1));
}
那么MinAlignment出现两次,PropertiesSize出现一次对应IDA中:
UStruct.ChildProperties
当然这里存在默认构造函数,导致我们如果用UE源码跟的话篇幅会变大,所以我们用有pdb的对照观看:
FProperty.Offset
execGetDataTableRowFromName->CopyScriptStruct->CopyCompleteValue_InContainer->ContainerPtrToValuePtr->ContainerVoidPtrToValuePtrInternal
FORCEINLINE void* ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}
对应我们的ida中的位置:
这样我们就获取到了Offset的解密算法:
unsignedlongUE_FField::GetOffset(){
uint32_t OffsetTemp = *(uint32_t*)(this + Offsets.FProperty.Offset) ^ 0x1DB80D6;
return _byteswap_ulong(OffsetTemp);
}
FField.class和FField.Next
在类TFieldIterator的构造函数中
对应ida的样式:
FField.Name的解密算法:
我们在UE引擎中可以定位到函数:FArrayProperty::ImportTextInnerItem
函数内有字符串。
text “UTF-16LE”, ‘%s is a fixed-sized array of %i values. Additional ‘
我们在ida中定位:
string UE_FField::GetName(){
__m128i xmmword_A9D33C0= *(__m128i*)(GameBase + 0xA9D33C0);
__m128i v30 = _mm_xor_si128(_mm_load_si128((const __m128i*)this + 11), (__m128i)xmmword_A9D33C0);
__int64 v43 = __ROL8__(_mm_or_si128(_mm_slli_epi16(v30, 0xEu), _mm_srli_epi16(v30, 2u)).m128i_u64[0] ^ 0x88EED3571FCF4D01uLL,32);
std::string Name = UKismetStringLibrary::Name2String((uint8_t*)&v43);
return Name;
}
FieldClass.FName 的解密算法
可以看到UE引擎在没有魔改的情况下:
class FFieldClass
{
UE_NONCOPYABLE(FFieldClass);
/** Name of this field class */
FName Name;
/** Unique Id of this field class (for casting) */
uint64 Id;
/** Cast flags used for casting to other classes */
uint64 CastFlags;
/** Class flags */
EClassFlags ClassFlags;
.......
我们的FName是在+0x0的位置,但是这款游戏将结构进行了修改。我们只需要在源码内找到任何一个使用了FFieldClass.Name的位置即可定位偏移。
这里我们提供字符串:aFboolproperty text “UTF-16LE”, ‘FBoolProperty’,可以定位到函数 FBoolProperty::StaticClass()或者FBoolProperty::Construct(),然后我们定位到函数FFieldClass::FFieldClass,FFieldClass的构造函数中使用到了FFieldClass.Name。
这样我们就拿到了FFieldClass.Name的解密算法:
__int64 UE_FField::GetClassFName()
{
__m128i* FieldClass = *(__m128i**)(this + Offsets.FField.Class);
__m128iv11 = _mm_shufflelo_epi16(*(__m128i *)(FieldClass + 4), 0x93);
__int64v14 =__ROL8__(_mm_shuffle_epi8(_mm_or_si128(_mm_add_epi16(v11, v11), _mm_srli_epi16(v11, 0xFu)), (__m128i)xmmword_A9D31A0).m128i_u64[0],
32);
return v14;
}
FName::ToString外部移植注意的问题
我们如果直接看伪代码发现所需参数v10并没有给它赋值.
但是函数内部却对这个变量进行了解引用取值,这时候我们看汇编代码即可。
可以看到rcx=rsp+58h,那么我们模拟两次+0x10解引用,rsp+0x68解引用是我们这里的rax,rax=rsp+30,然后再+0x10解引用,讲就是我们的xmm0,也就是这里的参数其实就是我们的v9,我们外部移植时去掉多余的解引用即可。
在ProcessEvent和StaticFindObject中用到了同样的方式,我们不管是通过注入调用还是使用vt断点断在主线程上下文调用,都需要构建相同的多层结构体进行传参即可。剩余的FName::ToString基本照抄即可,能最快还原。
UFunction.Func的获取
定位方式依旧是之前文章提到的先定位ProcessEvent然后在内部找到UFunction::Invoke。
该游戏对UFunction::Invoke进行了混淆:
不过只要不是vmp我们静态逐句分析都能定位到我们要的数据,快速定位方式就是一个一个分支的查看2个参数的函数(针对该游戏分析了一年多依旧适用的方式,仅针对该游戏).
这个函数内部依旧混淆。我们只需要找到一个形同ProcessEvent调用特征的(函数指针,三个参数),
我们查找v43的来源即可:
这样我们就轻松的定位到了Func的偏移0x150.
UEnum结构体修复
依旧是和以前文章一样的路径,通过aSTime3f3f3f定位函数UEnum::GetNameByValue。一次性就可以修复完UEnum结构体。在此不再赘述。
UStruct.SuperStruct
字符串:aFindremappedfi text “UTF-16LE”, ‘FindRemappedFieldImpl’,0
定位到函数:FindRemappedFieldImpl
UObject结构体修复:
字符串:aSHadRfNeedload
text “UTF-16LE”, ‘%s had RF_NeedLoad when being set up as an archetyp’
text “UTF-16LE”, ‘e of %s in %s’,0
定位到函数:GetArchetypeFromRequiredInfoImpl,函数内部调用了GetArchetype.
在GetArchetype函数的返回值的位置,可以让我们一次性修复UObject类。
我们只要按照顺序,依次复制拿到getClass、getOuter、GetName的算法即可。
v15 = *(_DWORD *)(this + 8);
v16 = 16777619
* ((unsignedint)(16777619
* __ROL4__(
((unsigned __int64)(this + 16) >> 32)
+ 16777619 * __ROL4__(16777619 * ((unsignedint)(this + 16) >> 4) + 208911565, 21)
+ 208911565,
28)
+ 208911565) >> 11)
+ 208911565;
v17 = v16 ^ BYTE2(v16);
v18 = ((unsigned __int8)v16 ^ BYTE2(v16)) & 3;
si128 = _mm_load_si128((const __m128i *)&xmmword_A9EDF10);
v20 = _mm_xor_si128(_mm_load_si128((const __m128i *)(this + 32 * (v18 ^ 2u) + 32)), si128);
v21 = __ROL8__(
_mm_shufflelo_epi16(_mm_or_si128(_mm_slli_epi64(v20, 0x2Au), _mm_srli_epi64(v20, 0x16u)), 27).m128i_u64[0],
32);
v22 = _mm_xor_si128(_mm_load_si128((const __m128i *)(this + 32 * ((v17 + 1) & 3u) + 32)), si128);
v23 = _mm_xor_si128(si128, *(__m128i *)(this + (unsignedint)(32 * v18) + 32));
class = (_QWORD *)_mm_shufflelo_epi16(_mm_or_si128(_mm_slli_epi64(v23, 0x2Au), _mm_srli_epi64(v23, 0x16u)), 27).m128i_i64[0];
outer = _mm_shufflelo_epi16(_mm_or_si128(_mm_slli_epi64(v22, 0x2Au), _mm_srli_epi64(v22, 0x16u)), 27).m128i_i64[0];
NamePrivate = v21,
Object遍历算法获取
NumElements和ObjectsBase的解密算法
同样在这个GetArchetype函数内,往下滑定位if else,找到有不等号判断并且一个分支赋值为0的位置。
__m128ixmmword_14E7ADFF0 = *(__m128i*)(GameBase + 0xE7ADFF0);
__m128ixmmword_14B523060 = *(__m128i*)(GameBase + 0xB523060);
__m128ixmmword_14B5764F0 = *(__m128i*)(GameBase + 0xB5764F0);
__m128iv42 = _mm_xor_si128(xmmword_14E7ADFF0, (__m128i)xmmword_14B523060);
const __m128i* v44 = (const __m128i*)_mm_shufflelo_epi16(
_mm_or_si128(_mm_slli_epi32(v42, 7u), _mm_srli_epi32(v42, 0x19u)),
27).m128i_u64[0];
NumElements = (int)(__ROL4__(
_mm_cvtsi128_si32(_mm_shuffle_epi8(_mm_load_si128(v44 + 1), (__m128i)xmmword_14B5764F0)) ^ 0xB237E018,
29) ^ 0xF3E47D2D);
__int64v46 = v44[8].m128i_i64[0];
__m128iv53 = _mm_load_si128(v44 + 11);
doubleresult = (*(double(__fastcall**)(const __m128i*, __m128i*))(v46 + 48))(v44 + 8, &v53);
Objects = *(_QWORD*)&result;
return true;
这里可以注意到,图片中v47 = *(_QWORD )(((__int64 (__fastcall **)(const __m128i *, __m128i *))(v46 + 48))(v44 + 8, &v53)
+ 8LL * HIWORD(v43))
使用的返回值类型是__int64但是我们的代码中修改为了double,我们对这一行按下tab查看反汇编:
可以明显看到ida的伪代码分析的不准确,使用__int64作为返回值类型你的结果自然是错误的。
UWorld、GameInstance、Object 未加密数据如何定位
我们通过aNullObjectWas字符搜索是可以定位到使用解密算法解密的UWorld的:
那么定位未加密最通用的方式就是使用注入,模拟一次解密过程,拿到解密数据后进行指针扫描,重复上游戏,重启电脑,多版本校验,就可以拿到稳定的基址和多级偏移。需要注意的就是这里的UWorld要挂主线程才能正常解密。
骨骼数组解密
通过DumpSDK中的Function.cpp找到函数UPoseableMeshComponent::GetBoneTransformByName,
函数内部可以找到函数 USkinnedMeshComponent::AdditionalStatObject
根据3次调用特征找到UPoseableMeshComponent::AllocateTransformData
紧接着在函数内部找到函数UPoseableMeshComponent::FillComponentSpaceTransforms(Mesh)
可能你会问为什么要找FillComponentSpaceTransforms函数,因为通过UE源码能够定位到的保存有骨骼数组的函数就找到了FillComponentSpaceTransforms函数,自然还有别的函数,你可以自行寻找,是在FillComponentSpaceTransforms的基础上不断查找交叉引用才总结出来的稳定使用的方法。
内部就找到了骨骼数组。
只要定位到一次后那么就可以考虑使用特征码定位了:
直接定位骨骼数组解密特征码:
56 574883EC284889CE4881C6????????4889F1 E8
if判断内拿到骨骼数组大小,将加数-0x8就是骨骼数组的位置
看雪ID:DannysMask
https://bbs.kanxue.com/user-home-907221.htm
*本文为看雪论坛优秀文章,由 DannysMask 原创,转载请注明来自看雪社区
火热售票中!1.25折门票即将售罄
往期推荐
当高频观测不再经过异常路径:Shadow Cave 与常驻式插桩架构
D3CTF 2026 d3llvm.apk 反调试定位与加密 SO的Dump
一串反引号,十层突破:n1ctf‑2018‑easy_harder_php 完整利用链
实现一个EDR不可见的网络通信(将lwip移植到nt内核中)
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 DannysMask
DannysMask《纯静态分析DumpSDK所需的加密数据,动态定位加密UWorld、GameInstance、Object的未加密地址》