前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
工具
准备工作
首先需要在Epic Games Launcher中安装对应游戏版本的Unreal Engine。
一般来说,只要 安装的引擎版本 >= 游戏使用的引擎版本 都可以,个人推荐安装高版本的引擎,因为高版本的引擎会兼容低版本的Pak文件。但是如果解包时遇到问题,可以尝试安装对应游戏版本的引擎。
安装好引擎后,引擎目录\Engine\Binaries\Win64\UnrealPak.exe
就是我们要用来解包和打包Pak文件的程序。
我们也可以把它和依赖一同复制出来独立使用,只需要把UnrealPak.exe
和UnrealPak.modules
里记录的依赖文件复制到同一目录下即可。
前置
Pak文件是否加密
我们先把Pak文件复制到自己需要的目录下,然后打开命令行执行下面的命令:
UnrealPak "绝对路径\文件名.pak" -List
如果执行成功,会输出Pak文件中包含的文件列表,如果失败,则说明Pak文件被加密了,一般会有下面的日志输出
LogPaths: Warning: No paths for game localization data were specifed in the game configuration.
LogInit: Warning: No paths for engine localization data were specifed in the engine configuration.
LogPakFile: Display: Using command line for crypto configuration
LogWindows: Error: appError called: Fatal error: [File:D:\build\++UE5\Sync\Engine\Source\Runtime\PakFile\Private\IPlatformFilePak.cpp] [Line: 355]
Failed to find requested encryption key 00000000000000000000000000000000
0x00007ffcfae613d0 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae35e99 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae2c9d7 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae3dce6 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae3d93e UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae3b389 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfae1c626 UnrealPak-PakFile.dll!UnknownFunction []
0x00007ffcfb820774 UnrealPak-PakFileUtilities.dll!UnknownFunction []
0x00007ffcfb8136e9 UnrealPak-PakFileUtilities.dll!UnknownFunction []
0x00007ff7b844be27 UnrealPak.exe!UnknownFunction []
0x00007ff7b844ce2c UnrealPak.exe!UnknownFunction []
0x00007ffd45ef7c24 KERNEL32.DLL!UnknownFunction []
0x00007ffd4616d721 ntdll.dll!UnknownFunction []
LogWindows: Error: === Critical error: ===
LogWindows: Error:
LogWindows: Error: Fatal error: [File:D:\build\++UE5\Sync\Engine\Source\Runtime\PakFile\Private\IPlatformFilePak.cpp] [Line: 355]
LogWindows: Error: Failed to find requested encryption key 00000000000000000000000000000000
LogWindows: Error: [Callstack] 0x00007ffcfae613d0 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae35e99 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae2c9d7 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae3dce6 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae3d93e UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae3b389 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfae1c626 UnrealPak-PakFile.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfb820774 UnrealPak-PakFileUtilities.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffcfb8136e9 UnrealPak-PakFileUtilities.dll!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ff7b844be27 UnrealPak.exe!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ff7b844ce2c UnrealPak.exe!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffd45ef7c24 KERNEL32.DLL!UnknownFunction []
LogWindows: Error: [Callstack] 0x00007ffd4616d721 ntdll.dll!UnknownFunction []
LogWindows: Error:
LogWindows: Error:
LogWindows: Error:
LogWindows: Error:
如果文件加密了,我们需要找到AES-256的Key,然后使用-cryptokeys
参数来指定Key描述文件(PaksCrypto.json),Key描述文件的内容格式如下:
{
"$types":{
"UnrealBuildTool.EncryptionAndSigning+CryptoSettings, UnrealBuildTool, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null":"1",
"UnrealBuildTool.EncryptionAndSigning+EncryptionKey, UnrealBuildTool, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null":"2",
"UnrealBuildTool.EncryptionAndSigning+SigningKeyPair, UnrealBuildTool, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null":"3",
"UnrealBuildTool.EncryptionAndSigning+SigningKey, UnrealBuildTool, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null":"4"
},
"$type":"1",
"EncryptionKey":{
"$type":"2",
"Name":"null",
"Guid":"null",
"Key":"Q8UcwjabndGV7c9CbHjjDpnXUU3BTowDqDHhKKOUEBA="
},
"SigningKey": null,
"bEnablePakSigning":true,
"bEnablePakIndexEncryption":true,
"bEnablePakIniEncryption":true,
"bEnablePakUAssetEncryption":true,
"bEnablePakFullAssetEncryption":false,
"bDataCryptoRequired":true,
"PakEncryptionRequired":true,
"PakSigningRequired":true,
"SecondaryEncryptionKeys":[
]
}
其中,EncryptionKey
字段中的Key
值就是AES-256的Key使用Base64编码的结果。
如果存在多个Key,可以在SecondaryEncryptionKeys
字段中添加其他的多个Key。
本篇文章主要演示加密的Pak文件,所以后续操作都会加上-cryptokeys
参数,如果是没加密的Pak文件,可以不加这个参数。
例如我这里加密的文件查看文件列表是:
UnrealPak "绝对路径\文件名.pak" -List -cryptokeys="绝对路径\PaksCrypto.json"
打包所需的参数
打包时我们需要用-compressionformats
参数指定压缩格式,具体用什么格式可以看列出来的文件列表里显示的压缩格式,例如:
LogPaths: Warning: No paths for game localization data were specifed in the game configuration.
LogInit: Warning: No paths for engine localization data were specifed in the engine configuration.
LogPakFile: Display: Using command line for crypto configuration
LogPakFile: Display: Mount point ../../../Client/Content/Aki/
LogPakFile: Display: "JavaScript/Core/Actor/ActorPoolGuard.js" offset: 0, size: 706 bytes, sha1: B435DB8AB50F39E695236C217C29F032A9201D1E, compression: Oodle.
LogPakFile: Display: "JavaScript/Core/Actor/ActorPoolGuard.js.map" offset: 779, size: 775 bytes, sha1: 5139133123D430E85B66A9C536DACE1E4821C7E0, compression: Oodle.
LogPakFile: Display: 12213 files (21760871 bytes), (0 filtered bytes).
LogPakFile: Display: UnrealPak executed in 1.767414 seconds
这里显示的是Oodle
压缩格式,所以打包时需要加上-compressionformats=Oodle
参数。
然后我们还需要用-Create
参数指定一个文件列表,文件列表中包含需要打包的文件,文件列表的格式如下:
本地文件路径 挂载到的文件路径
以上面的文件列表为例就是(pack.txt):
绝对路径/ActorPoolGuard.js ../../../Client/Content/Aki/JavaScript/Core/Actor/ActorPoolGuard.js
绝对路径/ActorPoolGuard.js.map ../../../Client/Content/Aki/JavaScript/Core/Actor/ActorPoolGuard.js.map
然后我们可以写个Gen.bat
脚本自动导出list.txt然后再从list.txt自动生成pack.txt
@echo off
setlocal enabledelayedexpansion
@REM 文件名不包含扩展名
set "fileName=文件名"
UnrealPak "%cd%\%fileName%.pak" -List > "list.txt" -cryptokeys="%cd%\PaksCrypto.json"
for /f "tokens=*" %%a in ('findstr /C:"Mount point " "list.txt"') do (
set "line=%%a"
set "mountpoint=!line:*Mount point =!"
set "extractpoint=!mountpoint:../=!"
)
(for /f "tokens=*" %%a in ('findstr /C:"offset:" "list.txt"') do (
set "line=%%a"
set "files=!line:*LogPakFile: Display: "=!"
set "files=!files:"=!"
for /f "tokens=1,* delims= " %%s in ("!files!") do (
set "file=%%s"
echo %cd%/%fileName%/!extractpoint!!file! !mountpoint!!file!
)
)) > pack.txt
这样子我们就可以自动生成pack.txt了。
解包
执行下面的命令即可解包
UnrealPak "绝对路径\文件名.pak" -Extract "绝对路径\文件名" -extracttomountpoint -cryptokeys="绝对路径\PaksCrypto.json"
参数解释:
- :指定要解包的文件
-Extract
:解包并指定解包到的路径-extracttomountpoint
:开启解包挂载点路径-cryptokeys
:指定加密密钥描述文件
解包完成后我们再执行Gen.bat
脚本来生成pack.txt就行了,记得修改脚本里的文件名再执行。
打包
执行下面的命令即可打包
UnrealPak "绝对路径\文件名.pak" -Create="绝对路径\pack.txt" -compressionformats=Oodle -compress -encrypt -encryptindex -signed -cryptokeys="绝对路径\PaksCrypto.json"
参数解释:
- :指定打包文件保存的路径
-Create
:指定要打包的文件列表(指定我们前面用Gen.bat
生成的pack.txt文件)-compressionformats
:指定压缩格式-compress
:开启压缩-encrypt
:开启加密-encryptindex
:开启加密索引-signed
:开启签名-cryptokeys
:指定加密密钥描述文件
其中-signed
、-encrypt
、-encryptindex
和-cryptokeys
参数看情况使用,一般情况下都不需要添加(个人不推荐添加),除非游戏加载有问题。
打包完成后我们就可以把文件替换回游戏目录里加载测试了。
寻找AES-Key
UE引擎中默认是通过FPakPlatformFile::DecryptData
函数来解密Pak文件的,我们进入这个函数
void DecryptData(uint8* InData, uint32 InDataSize, FGuid InEncryptionKeyGuid)
{
if (FPakPlatformFile::GetPakCustomEncryptionDelegate().IsBound())
{
FPakPlatformFile::GetPakCustomEncryptionDelegate().Execute(InData, InDataSize, InEncryptionKeyGuid);
}
else
{
SCOPE_SECONDS_ACCUMULATOR(STAT_PakCache_DecryptTime);
FAES::FAESKey Key;
FPakPlatformFile::GetPakEncryptionKey(Key, InEncryptionKeyGuid);
check(Key.IsValid());
FAES::DecryptData(InData, InDataSize, Key);
}
}
可以看到有个叫FPakPlatformFile::GetPakEncryptionKey
的函数,进去看下
void FPakPlatformFile::GetPakEncryptionKey(FAES::FAESKey& OutKey, const FGuid& InEncryptionKeyGuid)
{
OutKey.Reset();
if (!GetRegisteredEncryptionKeys().GetKey(InEncryptionKeyGuid, OutKey))
{
if (!InEncryptionKeyGuid.IsValid() && FCoreDelegates::GetPakEncryptionKeyDelegate().IsBound())
{
FCoreDelegates::GetPakEncryptionKeyDelegate().Execute(OutKey.Key);
}
else
{
UE_LOG(LogPakFile, Fatal, TEXT("Failed to find requested encryption key %s"), *InEncryptionKeyGuid.ToString());
}
}
}
可以看到第一个参数OutKey
就是我们要找的AES-Key,函数的下面有一条Fatal
日志,一般情况下这种类型的日志是不会被编译去掉的,所以我们在IDA中直接搜索这个关键字Failed to find requested encryption key
,然后查看交叉引用,就可以找到这个函数了。
如果你没有搜到的话要么编译给你删掉了,要么游戏加了些东西,这时候就得靠你自己了。
通过观察我们可以看到这个地方其实是FPakPlatformFile::DecryptData
函数,而FPakPlatformFile::GetPakEncryptionKey
的代码已经被编译器内联过来了,调用还全是虚表调用的,搞起来要费点力。
不过我们可以看下面的sub_1423FAFE0
函数,这个很明显是对应的FAES::DecryptData
函数,也就是使用AES-Key解密数据的函数,它的函数原型为
void FAES::DecryptData(uint8* Contents, uint32 NumBytes, const FAESKey& Key)
{
checkf(Key.IsValid(), TEXT("No valid decryption key specified"));
DecryptData(Contents, NumBytes, Key.Key, sizeof(Key.Key));
}
我们可以hook这个函数,然后打印第三个参数就行了,一样能达到我们的目的。
写段代码来试试
std::vector<std::string> s_aesKeys;
std::unordered_set<std::string_view> s_aesKeyFilters;
void *(*FAES__DecryptDataOri)(uint8_t *Contents, uint32_t NumBytes, void *Key) = nullptr;
void *FAES__DecryptDataHook(uint8_t *Contents, uint32_t NumBytes, void *Key)
{
std::string_view aesKey{reinterpret_cast<char *>(Key), 32};
if (!s_aesKeyFilters.contains(aesKey))
{
auto &value = s_aesKeys.emplace_back(aesKey.begin(), aesKey.end());
s_aesKeyFilters.insert(value);
LogDebug("New AES key found: %s", StringUtils::Base64Encode(value).data());
}
return FAES__DecryptDataOri(Contents, NumBytes, Key);
}
日志:
可以看到我们这里已经成功Dump出了AES-Key了,然后就是愉快的解包和打包了。
批量操作
实际操作中我们肯定不可能一个一个文件去解包和打包,命令多还不方便管理,所以我们需要写两个批处理脚本来实现批量操作。
批量解包
Extract.bat
@echo off
setlocal enabledelayedexpansion
set "UnrealPakPath=UnrealPak.exe"
for %%f in (*.pak) do (
echo Extracting %%f to extracts\%%~nf...
set "extractfolder=%cd%\extracts\%%~nf"
"%UnrealPakPath%" "%cd%\%%f" -Extract "!extractfolder!" -extracttomountpoint -cryptokeys="%cd%\PaksCrypto.json"
"%UnrealPakPath%" "%cd%\%%f" -List > "!extractfolder!\list.txt" -cryptokeys="%cd%\PaksCrypto.json"
for /f "tokens=*" %%a in ('findstr /C:"Mount point " "!extractfolder!\list.txt"') do (
set "line=%%a"
set "mountpoint=!line:*Mount point =!"
set "extractpoint=!mountpoint:../=!"
)
(for /f "tokens=*" %%a in ('findstr /C:"offset:" "!extractfolder!\list.txt"') do (
set "line=%%a"
set "files=!line:*LogPakFile: Display: "=!"
set "files=!files:"=!"
for /f "tokens=1,* delims= " %%s in ("!files!") do (
set "file=%%s"
echo !extractfolder!\!extractpoint!!file! !mountpoint!!file!
)
)) > !extractfolder!\pack.txt
)
echo Extraction complete.
pause
批量打包
Repack.bat
@echo off
setlocal enabledelayedexpansion
set "UnrealPakPath=UnrealPak.exe"
for /d %%a in ("extracts\*") do (
set "OutputFile=repacks\%%~na.pak"
echo Packing %%a to !OutputFile!...
"%UnrealPakPath%" "%cd%\!OutputFile!" -Create="%cd%\%%a\pack.txt" -compressionformats=Oodle -compress
)
echo Repack complete.
pause
使用时将Extract.bat
和Repack.bat
和Pak文件放在一个文件夹下就行了。
结语
那就这样了,有缘再见~