unity游戏资产保护分析

2023-4-2|最后更新: 2023-4-2|
type
status
date
slug
summary
tags
category
icon
password

背景

unity-il2cpp保护中除了dat的元数据保护就是资源保护了,首先需要了解unity中有哪些资源类型,然后就是需要了解下当前unity资源保护中有哪些方式,关于il2cpp的dat加解密的资料很多,但是关于资源加解密的文章很少

unity资源解析工具测试

很多开源工具,举例:AssetStudio、AssetRipper
AssetStudio测试
load folder是指打开exe根目录的文件夹,然后自动分析其中的资源文件,当然如果有加密的需要提前解密,*.assets为资源文件
这里通过对_data目录的load发现了所有的资源,并且理所当然的都是未加密状态,查看开头的提示如下
notion image
右键查看对应的文件发现在sharedassets0.assets中

unity中有哪些资源类型

依然是使用unity官方提供的FPSdemo的话,xx_Data目录下的*.assets被解析为静态资源,如果不特地设计是不会有AssetBundle这种所谓的需要动态加载的资源的
unity资源保护这里保护的类型可以认为主要有两个,一个是单纯的静态加载的asset一个是动态加载的AssetBundle包,通常后者的保护更为重要,这也就导致网络对资源保护的讨论一般默认指的都是AB资源保护,不是静态资源。静态资源的加载逻辑是unity引擎的默认加载,代码需要反汇编unityplayer.dll分析

关于AssetBundle

  1. 需要主动决定将需要打包到AssetBundle中的资源,并写打包AssetBundle的脚本定义哪些资源需要打包,需要使用什么压缩方式,打包名称是什么……(需额外搜索了解ab资源打包)
  1. AssetBundle中的资源加载首先需要将AssetBundle包加载,使用的C#导出api包括LoadFromFile,LoadFromMemory,LoadFromStream……

AB包的几种解密方式概述

  1. LoadFromMemory函数传入bytes因此可以先读取再解密后调用该api加载,LoadFromFile中可以传递一个offset参数指定从文件的哪个偏移开始解析……,但以上这些方式的解密代码均放在了C#中,对元数据dat解析完成后可以根据字符串字面量或者符号字符串搜索找到关键函数实现,或者直接钩挂unityplayer中对C#导出的这些api接口的c++函数从而获取到解密后的字节流或者offset,这类方式应用最广,但是相对而言对抗难度较低
  1. 各个安全加固厂商的保护逻辑:网易易盾等方案结合ab包的加载逻辑根据符号数据定位关键函数后二进制介入钩挂配合对ab结果包的后期修改配合完成加密,具体应该钩挂那里如何分析,后续会进行测试
  1. 当前最顶级的资源保护方式,如米哈游在原神中使用的方式,解密代码并未在C#脚本中实现,而是通过修改unity闭源代码重新定义asb加载逻辑,编译出了unityplayer,这种方式对于买断unity闭源代码修改权限的厂商才能使用
  1. 更更强的设想方案,即使是原神中的资源加密,出于性能原因也只是重新定义ab包的结构,定义解密逻辑,但从uncompress后的texture以及各种gameobj解析环节并未做定制,所以当前最顶级的加密也仅限于ab包的加密和部分特定gameobj的引入

unity资源的二进制结构解析

想要搞清楚各个资源的二进制结构请阅读并调试AssetStudio,这里看代码就行不深入展开,这里记下几个关键点
  1. AssetsManager.cs的LoadFiles处理函数加载入口点
  1. CheckFileType对文件分类,再分发到各个类型的游戏处理函数中读取数据
  1. 注意unity资源这里很多都有嵌套依赖,会在加载一个文件的时候递归加载多个关联依赖资源
  1. 需要注意一点即"UnityWeb""UnityRaw""UnityArchive""UnityFS"均为BundleFile
  1. ……

静态资源加载点分析

分析静态资源加载点就是在找今天资源解密时机点,虽然是闭源但unityplayer的pdb都是公开的,结合ida分析即可
实验方式
  1. 结合ida搜索sharedassets字符串定位到关键函数PlayerStartFirstScene
  1. 设置好pdb目录后windbg调试,bp unityplayer!PlayerStartFirstScene
  1. 断下后bp kernelbase!CreateFileW
  1. 然后du中断打印rcx指向的内存"D:\Code\FirstFps\VS\build\bin\FirstFps_Data\sharedassets0.assets"
  1. 回溯堆栈打印加载asset的调用链
00 00000074`9838eb38 00007fff`85659829 : 00000000`00120089 0000024f`70096dc8 00000000`00000003 0000024f`70096dc8 : KERNELBASE!CreateFileW 01 00000074`9838eb40 00007fff`85f2a39f : 0000024f`70096dc0 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!LocalFileSystemWindowsShared::Open+0x1a9 02 00000074`9838ec00 00007fff`85e5d49c : 00000000`00000000 00000074`9838ecc9 0000024f`70096dc0 00000000`00000000 : UnityPlayer!FileAccessor::Open+0x4f 03 00000074`9838ec30 00007fff`85e5d2fa : 0000024e`b0119c90 00000000`00000000 00000000`00000040 00000000`00000040 : UnityPlayer!File::OpenFileSystemEntry+0x16c 04 00000074`9838ed30 00007fff`859922e9 : 00000250`000004d0 0000024f`30004db0 00000000`00000003 0000024f`30004cc8 : UnityPlayer!File::Open+0x9a 05 00000074`9838f1f0 00007fff`85993620 : 0000024f`30004cc8 00000250`000004d0 00000250`0000000a 00000000`00000003 : UnityPlayer!OpenFileCache::OpenCached+0x2e9 06 00000074`9838f310 00007fff`85cf06cf : 00000000`00000001 00000000`00001c00 00000250`00000370 00000000`00000000 : UnityPlayer!SyncReadRequest+0x60 07 00000074`9838f360 00007fff`85cf03bf : 00000000`00000001 00000000`ffffffff 00000000`00000001 00000250`00000370 : UnityPlayer!FileCacherRead::Request+0xff 08 00000074`9838f3a0 00007fff`85cf72b7 : 00000074`9838f550 00000000`00000014 00000250`00000370 00007fff`872439c0 : UnityPlayer!FileCacherRead::LockCacheBlock+0x10f 09 00000074`9838f400 00007fff`860f64bd : 00000250`00000370 00000074`9838f6b0 00000000`00000000 00000074`9838f4e9 : UnityPlayer!ReadFileCache+0x67 0a 00000074`9838f450 00007fff`860f4bb9 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!SerializedFile::ReadHeader+0xad 0b 00000074`9838f550 00007fff`860e181b : 00000000`00000001 00000074`9838f6b0 00000000`00000000 0000024f`106b07a0 : UnityPlayer!SerializedFile::InitializeRead+0x139 0c 00000074`9838f5b0 00007fff`860e0f05 : 0000024f`00000000 00000000`00000004 ffffffff`ffffffff 00000000`00000000 : UnityPlayer!PersistentManager::GetStreamNameSpaceInternal+0x2ab 0d 00000074`9838f730 00007fff`860e495f : 00000000`00000000 00000000`00000000 00000000`00000000 0000024f`00000001 : UnityPlayer!PersistentManager::GetSerializedFileIfObjectAvailable+0x95 0e 00000074`9838f770 00007fff`860e4f74 : 00000000`00000001 0000024f`101cbd70 00000000`00000001 00007fff`85f790f2 : UnityPlayer!PersistentManager::ReadAndActivateObjectThreaded+0xaf 0f 00000074`9838f820 00007fff`85ca3c2c : 0000024e`b0043a40 0000024f`00000618 0000024f`00000001 0000024e`d0052b50 : UnityPlayer!PersistentManager::ReadObjectThreaded+0x1b4 10 00000074`9838f8a0 00007fff`85ca4d65 : 0000000f`00000618 0000024f`101cbd70 0000024f`1000ed40 0000024f`10aa4e20 : UnityPlayer!LoadSceneOperation::Perform+0x15c 11 00000074`9838faa0 00007fff`85ca51b9 : 00000000`0000000f 0000024e`f0fc0010 0000024e`f0fc0018 0000024f`10aa4e20 : UnityPlayer!PreloadManager::ProcessSingleOperation+0x165 12 00000074`9838fad0 00007fff`85f7656f : 00000000`0000000e 00000074`9838fbc9 00000000`0000000f 0000024f`10aa4e50 : UnityPlayer!PreloadManager::Run+0xf9 13 00000074`9838fb30 00007fff`e8f63e2d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!Thread::RunThreadWrapper+0x7bf 14 00000074`9838fc30 00007fff`eab1ef48 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x1d 15 00000074`9838fc60 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x28
unity引擎内部加载异步管理器(C++类: PreloadManager )负责处理所有的异步加载请求,单独的线程负责异步的对文件进行加载
但由于静态资源加密并不是资源保护的主线逻辑,因此这里仅仅做一个初期的实验,并未深入,在ab包的加载逻辑处再做深入测试

AssetBundle加载点分析

实验方式
  1. 对LoadFromFile下断,回溯调用栈
00 0000002e`3a71f058 00007ffb`de85f99d : 00000000`00000039 00000000`00000000 00000000`00000000 000001cd`f9868e60 : UnityPlayer!LoadFromFile 01 0000002e`3a71f060 00007ffb`f539ac24 : 00000000`00000000 000001cf`5afd4378 000001cf`5afd4378 00007ffc`4507751e : UnityPlayer!AssetBundle_CUSTOM_LoadFromFile_Internal+0x18d 02 0000002e`3a71f140 00007ffb`f4dffe3c : 000001cf`5afd4378 00007ffc`4507282b 000001cf`61459230 000001cf`df0400db : GameAssembly!TestLoad_Start_m427FBDA4B697587ABE473275D1C41EF8BD02DF2D+0x94 [D:\Code\Test2D\Library\Il2cppBuildCache\Windows\x64\il2cppOutput\Assembly-CSharp.cpp @ 406] 03 0000002e`3a71f170 00007ffb`f4da6780 : 000001cf`61459230 000001cd`d7df0000 000001cd`d7df0000 000001cf`6144bebf : GameAssembly!RuntimeInvoker_TrueVoid_t700C6383A2A510C2CF4DD86DABD5CA9FF70ADAC5+0xc [D:\Code\Test2D\Library\Il2cppBuildCache\Windows\x64\il2cppOutput\Il2CppInvokerTable.cpp @ 37112] 04 0000002e`3a71f1a0 00007ffb`ded990b8 : 000001cf`5afd4378 0000002e`3a71f370 000001cf`6144bebf 0000002e`3a71f630 : GameAssembly!il2cpp::vm::Runtime::Invoke+0x80 [C:\Program Files\Unity\Hub\Editor\2020.3.33f1c2\Editor\Data\il2cpp\libil2cpp\vm\Runtime.cpp @ 568] 05 0000002e`3a71f200 00007ffb`ded9c122 : 0000002e`3a71f678 00000000`00000000 0000002e`3a71f630 0000002e`3a71f370 : UnityPlayer!scripting_method_invoke+0x58 06 0000002e`3a71f250 00007ffb`dedba00f : 000001cd`f9868e60 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!ScriptingInvocation::Invoke+0x92 07 0000002e`3a71f2b0 00007ffb`dedba09e : 000001cf`7000bc50 000001cf`5afd4378 000001cf`7000c2e0 00000000`00000003 : UnityPlayer!MonoBehaviour::InvokeMethodOrCoroutineChecked+0x5ff 08 0000002e`3a71f600 00007ffb`dedb921a : 000001cd`f9868e60 000001ce`809e0230 00000000`00000017 00000000`00000000 : UnityPlayer!MonoBehaviour::InvokeMethodOrCoroutineChecked+0x6e 09 0000002e`3a71f660 00007ffb`deb179e4 : 000001ce`809e0230 000001ce`400b7f80 00000000`00000000 00007ffb`dea1e423 : UnityPlayer!MonoBehaviour::DelayedStartCall+0x3a 0a 0000002e`3a71f690 00007ffb`dec4abaa : 000001cf`000004f0 000001ce`00000004 00000000`00000001 000001cf`5aa97c20 : UnityPlayer!DelayedCallManager::Update+0x174 0b 0000002e`3a71f740 00007ffb`dec4ac50 : 00000000`00000000 00000000`00000000 000001cf`5aa97c20 00000000`00000001 : UnityPlayer!ExecutePlayerLoop+0x5a 0c 0000002e`3a71f8e0 00007ffb`dec4eef8 : 00000000`00000adc 00007ffc`00000001 00000000`00000adc 00000000`00000000 : UnityPlayer!ExecutePlayerLoop+0x100 0d 0000002e`3a71fa80 00007ffb`dea0d0ba : 00000000`00000001 00000000`00000000 00000000`00000adc 00000000`00000000 : UnityPlayer!PlayerLoop+0xb8 0e 0000002e`3a71fb00 00007ffb`dea0b4db : 00000000`00000adc 00000000`00000000 00000000`0000000a 00000000`00000000 : UnityPlayer!PerformMainLoop+0x30a 0f 0000002e`3a71fb30 00007ffb`dea10b62 : 00000000`0000000a ffffffff`ffffffff 00000000`0000000b 00007ffb`dfb5bed0 : UnityPlayer!MainMessageLoop+0x10b 10 0000002e`3a71fba0 00007ffb`dea123bb : 00000000`0000000a 00000000`00000000 000001cd`d7df49f6 00007ff6`0000000a : UnityPlayer!UnityMainImpl+0x1552 11 0000002e`3a72fe40 00007ff6`9dd311f2 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!UnityMain+0xb 12 0000002e`3a72fe70 00007ffc`43f63e2d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : WindowsPlayer+0x11f2 13 0000002e`3a72feb0 00007ffc`44fbef48 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x1d 14 0000002e`3a72fee0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x28
调用链:PerformMainLoop >> PlayerLoop >> ExecutePlayerLoop >> TestLoad::Start(脚本逻辑) >> LoadFromFile >> UnityPlayer!LoadFromFile
  1. 再对CreateFileW下断点通过查看打开的文件名确定时ab包,查看堆栈
00 0000002e`3a71e838 00007ffb`de9d8778 : 000001ce`a0070690 000001ce`a0070690 00000000`00120089 000001ce`a00001a0 : KERNELBASE!CreateFileW 01 0000002e`3a71e840 00007ffb`decfb26f : 000001ce`a0070ae8 000001ce`a0070688 00000000`00000000 00000000`00000000 : UnityPlayer!LocalFileSystemWindowsShared::Open+0xe8 02 0000002e`3a71e8d0 00007ffb`ded0347c : 000001ce`a0070ae8 0000002e`3a71eaf9 00000000`00000001 00007ffb`f4dbc0ad : UnityPlayer!FileAccessor::Open+0x4f 03 0000002e`3a71e900 00007ffb`ded029ed : 00000000`00000001 0000002e`3a71eaf9 00000000`00000001 00007ffb`df3e8f64 : UnityPlayer!ArchiveStorageReader::MakeStorageUsed+0x5c 04 0000002e`3a71e9b0 00007ffb`de74cc18 : 00000000`00000001 0000002e`3a71eaf9 0000002e`3a71efe8 000001ce`600000b0 : UnityPlayer!ArchiveStorageReader::Initialize+0x4d 05 0000002e`3a71ea60 00007ffb`de74d11f : 00000000`00000070 00000000`00000000 00000000`00000000 00000000`00000001 : UnityPlayer!AssetBundleLoadFromAsyncOperation::InitializeAssetBundleStorage+0x98 06 0000002e`3a71eb60 00007ffb`de735226 : 000001ce`600000b0 00000000`00000000 000001ce`60000097 00000000`00000002 : UnityPlayer!AssetBundleLoadFromAsyncOperation::InitializeAssetBundleStorage+0x5f 07 0000002e`3a71efe0 00007ffb`de740ea6 : 00000000`00000000 0000002e`3a71f080 0000002e`3a71f158 0000002e`3a71f0d9 : UnityPlayer!AssetBundleLoadFromFileAsyncOperation::ExecuteSynchronously+0x26 08 0000002e`3a71f010 00007ffb`de85f99d : 00000000`00000000 00000000`00000039 0000002e`3a71f0d9 000001cd`f9868e60 : UnityPlayer!LoadFromFile+0x86 09 0000002e`3a71f060 00007ffb`f539ac24 : 00000000`00000000 000001cf`5afd4378 000001cf`5afd4378 00007ffc`4507751e : UnityPlayer!AssetBundle_CUSTOM_LoadFromFile_Internal+0x18d
  1. 对ReadFile下断查看读取点
00 0000002e`3a71de68 00007ffb`de9d90b7 : 0000002e`3a71df10 00007ffb`de9d70cd 00000000`00000001 00007ffb`dfaac004 : KERNELBASE!ReadFile 01 0000002e`3a71de70 00007ffb`decfb677 : 00000000`00000000 00007ffb`00000000 000001ce`a0070688 0000002e`3a71eb70 : UnityPlayer!LocalFileSystemWindowsShared::Read+0x97 02 0000002e`3a71ded0 00007ffb`decfcd26 : 00000000`00000000 000001ce`a0070688 000001ce`a00700c0 000001cd`d7df02c0 : UnityPlayer!FileAccessor::Read+0x37 03 0000002e`3a71df10 00007ffb`ded068c5 : 000001ce`a0070688 00000000`00000000 00000000`00000000 00000000`00000000 : UnityPlayer!`anonymous namespace'::ReadString<core::basic_string<char,core::StringStorageDefault<char> > >+0xb6 04 0000002e`3a71dfd0 00007ffb`ded05865 : 00000000`00000000 00000000`00000000 00000000`00000000 0000002e`3a71e030 : UnityPlayer!ArchiveStorageHeader::ReadHeaderSignature+0x15 05 0000002e`3a71e000 00007ffb`ded02a06 : 00000000`00000000 00000000`00000000 00000000`00000001 00007ffb`df3e8f64 : UnityPlayer!ArchiveStorageReader::ReadHeader+0x55 06 0000002e`3a71e9b0 00007ffb`de74cc18 : 00000000`00000001 0000002e`3a71eaf9 0000002e`3a71efe8 000001ce`600000b0 : UnityPlayer!ArchiveStorageReader::Initialize+0x66 07 0000002e`3a71ea60 00007ffb`de74d11f : 00000000`00000070 00000000`00000000 00000000`00000000 00000000`00000001 : UnityPlayer!AssetBundleLoadFromAsyncOperation::InitializeAssetBundleStorage+0x98 08 0000002e`3a71eb60 00007ffb`de735226 : 000001ce`600000b0 00000000`00000000 000001ce`60000097 00000000`00000002 : UnityPlayer!AssetBundleLoadFromAsyncOperation::InitializeAssetBundleStorage+0x5f 09 0000002e`3a71efe0 00007ffb`de740ea6 : 00000000`00000000 0000002e`3a71f080 0000002e`3a71f158 0000002e`3a71f0d9 : UnityPlayer!AssetBundleLoadFromFileAsyncOperation::ExecuteSynchronously+0x26 0a 0000002e`3a71f010 00007ffb`de85f99d : 00000000`00000000 00000000`00000039 0000002e`3a71f0d9 000001cd`f9868e60 : UnityPlayer!LoadFromFile+0x86 0b 0000002e`3a71f060 00007ffb`f539ac24 : 00000000`00000000 000001cf`5afd4378 000001cf`5afd4378 00007ffc`4507751e : UnityPlayer!AssetBundle_CUSTOM_LoadFromFile_Internal+0x18d
  1. 结合ida看到关键字符比较点确认逻辑
notion image
  1. 并且在ReadHeader函数中发现了疑似官方ab资源解密的逻辑点GetAssetBundleKey & ArchiveStorageDecrypt::InitDecryptor

测试KSignature修改还原

  1. 尝试在ReadHeaderSignature后对readfile出来的结果处模拟还原被修改后的“UnityFs”测试,但均以加载失败告终
  1. 上述失败能想到有两个可能,1是ab资源有crc校验,2是选取的KSignature不止这一处有读取(因为通过ida交叉引用发现了多个引用点)
  1. 结合二进制修改unityplayer中的字符串“UnityFs”,并且同时修改所有ab包的对应数据,成功加载资源确认并非是crc校验导致的失败
  1. 为什么一开始没有考虑到第二种可能呢,虽然通过ida交叉引用发现了多个引用点,但以为是只读一次然后多地对比导致的

尝试更合理的修改点

KSignature的修改既不能称为加密也不能实际应用,只能说是一个机制的尝试,但由于其引用点过多,patch起来不方便,所以干脆更深入去看有没有实际生产中能用到的数据,并且最好是单一patch点,对了这里会有一个前提就是当前ab资源加固常规都是做得轻加固,即只加密头部header中的数据,针对性的对抗解包软件的分析,处于性能考虑很少大面积加密的方案,那么首先就得明确下ab包的各个数据字段,找到一个或者多个核心点进行加密,对ab包header中的数据分析如下
notion image
0x20开始为Header区域,先是8字节的size = 0x67914, compressedBlocksInfoSize = 0x58, uncompressedBlocksInfoSize = 0x99, flags = 0x43
UnityFS\0后的4字节为version,如果大于等于7,则header读取flags后需要16字节对齐,此处版本号为7对齐后0x40处继续解析,其为BlocksInfoAndDirectory数据
前面读取到的compressedBlocksInfoSize = 0x58也即BlocksInfoAndDirectory数据的大小,后续的数据为待解压数据(此处仅考虑BlocksAndDirectoryInfoCombined情况)
notion image
根据flags中选择的解压算法和uncompressedBlocksInfoSize对这里的0x58数据解压出0x99长度的数据,解出后再对解出后的buffer做block和node的解析
思考
  1. 这里再考虑如果仅就安全加密而言的话我们不需要确保加密后的数据依然可以被正确的解密,并到解析那一步再做干扰
  1. 但更简单的做法是直接对flags数据进行干扰,如果就最小化修改而言的话flags数据修改的效果最佳
  1. 因此需要结合ida去分析到底是哪里读取了0x30~0x33出的flags字段,另外还要分析是否有多处读取点
  1. 分析结果确认关键点在ArchiveStorageHeader::ReadHeader中,这里标注了几个关键信息的读取点,这里就不放过程了,放一下最终标注的ida结果
notion image
结合调试器测试在flags读取点手动还原数据可以实现正常的加载,但是需要多次修改,不过这里肯定是一个有效的patch点

原神的资源保护逻辑

原神1.x时期很明显的是他们的blk文件在streamxxx这种ab资源文件常见的路径中,所以就很明显是ab资源的自定义加密格式
BLK被开源的解密项目被DMA了仔细找下能看到备份和一直在更新的,是基于AssetStudio,没什么涉及原始结构中做了小调整的恶心区域,主要是BLK格式解析和一个新的BundleFile类型,加密过于复杂,结构重写+分段解密异或,固定解密算法,key变换啥的,想了解的自己去学习下项目代码,但是核心逻辑也只有前期解析起来不一致,到bundlefile层面其实大的核心逻辑还是一样的,并没有做彻底的修改,对比下图所示
notion image
notion image
 
windows上的LLVM pass瞎折腾记录原神1.x时期的il2cpp保护思路分析