纯静态分析DumpSDK所需的加密数据,动态定位加密UWorld、GameInstance、Object的未加密地址

0、演示demo为国外过气游戏Arc

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的解密算法:

unsigned long UE_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);   

    __m128i v11 = _mm_shufflelo_epi16(*(__m128i *)(FieldClass + 4), 0x93);
        __int64 v14 =__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 可以拿到SuperStruct的偏移

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
        * ((unsigned int)(16777619
                        * __ROL4__(
                            ((unsigned __int64)(this + 16) >> 32)
                          + 16777619 * __ROL4__(16777619 * ((unsigned int)(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 + (unsigned int)(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的位置。

__m128i xmmword_14E7ADFF0 = *(__m128i*)(GameBase + 0xE7ADFF0);
__m128i xmmword_14B523060 = *(__m128i*)(GameBase + 0xB523060);
__m128i xmmword_14B5764F0 = *(__m128i*)(GameBase + 0xB5764F0);

__m128i v42 = _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);

__int64 v46 = v44[8].m128i_i64[0];
__m128i v53 = _mm_load_si128(v44 + 11);
double result = (*(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 57 48 83 EC 28 48 89 CE 48 81 C6 ?? ?? ?? ?? 48 89 F1 E8
if判断内拿到骨骼数组大小,将加数-0x8就是骨骼数组的位置