前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

工具

  1. Unreal Engine

准备工作

首先需要在Epic Games Launcher中安装对应游戏版本的Unreal Engine。

一般来说,只要 安装的引擎版本 >= 游戏使用的引擎版本 都可以,个人推荐安装高版本的引擎,因为高版本的引擎会兼容低版本的Pak文件。但是如果解包时遇到问题,可以尝试安装对应游戏版本的引擎。

安装好引擎后,引擎目录\Engine\Binaries\Win64\UnrealPak.exe就是我们要用来解包和打包Pak文件的程序。

我们也可以把它和依赖一同复制出来独立使用,只需要把UnrealPak.exeUnrealPak.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,然后查看交叉引用,就可以找到这个函数了。

如果你没有搜到的话要么编译给你删掉了,要么游戏加了些东西,这时候就得靠你自己了。

IDA伪代码

通过观察我们可以看到这个地方其实是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.batRepack.bat和Pak文件放在一个文件夹下就行了。

结语

那就这样了,有缘再见~