UE如何存储PAK文件的AES密码
Intro
UE的大部分content资产都放在了.PAK文件中,为了避免资产被破解,最好对文件进行加密。由于pak文件在运行时需要解密,所以运行时必然需要知道明文密码。或许是出于效率考虑,Unreal使用的是AES这种对称加密,也就是加密和解密使用的是相同的key。
如果把密码以明文的形式存储在文件系统中,那么既然能获得pak文件,也可以从安装包中获得密码文件,所以这个密码应该如何存储就是一个比较棘手的现实问题。
打包
在配置project时,可以在Project Settings>>Project>>Encryption中设置加密和签名配置,其中就包括了“Encryption Key”配置。
在执行工程打包的时候,编辑器会读取这里的配置,并根据配置的不同来选择设置打包命令(UnrealPak)的参数。
例如,下面的代码中可以看到对cryptokeys命令行参数的设置。
@file: Engine\Source\Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs
namespace AutomationScripts
{
/// <summary>
/// Helper command used for cooking.
/// </summary>
/// <remarks>
/// Command line parameters used by this command:
/// -clean
/// </remarks>
public partial class Project : CommandUtils
{
///...
/// <summary>
/// Writes a pak response file to disk
/// </summary>
/// <param name="Filename"></param>
/// <param name="ResponseFile"></param>
private static void WritePakResponseFile(string Filename, Dictionary<string, string> ResponseFile, bool Compressed, bool RehydrateAssets, EncryptionAndSigning.CryptoSettings CryptoSettings, bool bForceFullEncryption)
{
using (var Writer = new StreamWriter(Filename, false, new System.Text.UTF8Encoding(true)))
{
foreach (var Entry in ResponseFile)
{
string Extension = Path.GetExtension(Entry.Key);
string Line = String.Format("\"{0}\" \"{1}\"", Entry.Key, Entry.Value);
// explicitly exclude some file types from compression
if (Compressed && !Path.GetExtension(Entry.Key).Contains(".mp4") && !Extension.Contains("ushaderbytecode") && !Path.GetExtension(Entry.Key).Contains("upipelinecache"))
{
Line += " -compress";
}
// todo: Ideally we would know if the package is virtualized and only opt to rehydrate those packages, but we'd need to be able
// to pipe that info this far.
if(RehydrateAssets && (Extension.Contains(".uasset") || Extension.Contains(".umap")))
{
Line += " -rehydrate";
}
if (CryptoSettings != null)
{
bool bEncryptFile = bForceFullEncryption || CryptoSettings.bEnablePakFullAssetEncryption;
bEncryptFile = bEncryptFile || (CryptoSettings.bEnablePakUAssetEncryption && Extension.Contains(".uasset"));
bEncryptFile = bEncryptFile || (CryptoSettings.bEnablePakIniEncryption && Extension.Contains(".ini"));
if (bEncryptFile)
{
Line += " -encrypt";
}
}
Writer.WriteLine(Line);
}
}
}
/// <summary>
/// Loads streaming install chunk manifest file from disk
/// </summary>
/// <param name="Filename"></param>
/// <returns></returns>
private static HashSet<string> ReadPakChunkManifest(string Filename)
{
var ResponseFile = ReadAllLines(Filename);
var Result = new HashSet<string>(ResponseFile, StringComparer.InvariantCultureIgnoreCase);
return Result;
}
private static string GetCommonUnrealPakArguments(List<OrderFile> PakOrderFileLocations, string AdditionalOptions, EncryptionAndSigning.CryptoSettings CryptoSettings, FileReference CryptoKeysCacheFilename, List<OrderFile> SecondaryPakOrderFileLocations, bool bUnattended)
{
StringBuilder CmdLine = new StringBuilder();
if (CryptoKeysCacheFilename != null)
{
CmdLine.AppendFormat(" -cryptokeys={0}", CommandUtils.MakePathSafeToUseWithCommandLine(CryptoKeysCacheFilename.FullName));
}
if (PakOrderFileLocations != null && PakOrderFileLocations.Count() > 0)
{
CmdLine.AppendFormat(" -order={0}", CommandUtils.MakePathSafeToUseWithCommandLine(string.Join(",", PakOrderFileLocations.Select(u => u.File.FullName).ToArray())));
}
if (SecondaryPakOrderFileLocations != null && SecondaryPakOrderFileLocations.Count() > 0)
{
CmdLine.AppendFormat(" -secondaryOrder={0}", CommandUtils.MakePathSafeToUseWithCommandLine(string.Join(",", SecondaryPakOrderFileLocations.Select(u => u.File.FullName).ToArray())));
}
if (CryptoSettings != null && CryptoSettings.bDataCryptoRequired)
{
if (CryptoSettings.bEnablePakIndexEncryption)
{
CmdLine.AppendFormat(" -encryptindex");
}
if (CryptoSettings.bDataCryptoRequired && CryptoSettings.bEnablePakSigning && CryptoSettings.SigningKey.IsValid())
{
CmdLine.AppendFormat(" -sign");
}
}
if (bUnattended)
{
// We don't want unrealpak popping up interactive dialogs while we're running a build
CmdLine.AppendFormat(" -unattended");
}
CmdLine.Append(AdditionalOptions);
return CmdLine.ToString();
}
static private string GetPakFileSpecificUnrealPakArguments(Dictionary<string, string> UnrealPakResponseFile, FileReference OutputLocation, string AdditionalOptions, bool Compressed, bool RehydrateAssets, EncryptionAndSigning.CryptoSettings CryptoSettings, String PatchSourceContentPath, string EncryptionKeyGuid)
{
StringBuilder CmdLine = new StringBuilder(MakePathSafeToUseWithCommandLine(OutputLocation.FullName));
// Force encryption of ALL files if we're using specific encryption key. This should be made an option per encryption key in the settings, but for our initial
// implementation we will just assume that we require maximum security for this data.
bool bForceEncryption = !string.IsNullOrEmpty(EncryptionKeyGuid);
string PakName = Path.GetFileNameWithoutExtension(OutputLocation.FullName);
string ResponseFilesPath = CombinePaths(CmdEnv.EngineSavedFolder, "ResponseFiles");
InternalUtils.SafeCreateDirectory(ResponseFilesPath);
string UnrealPakResponseFileName = CombinePaths(ResponseFilesPath, "PakList_" + PakName + ".txt");
WritePakResponseFile(UnrealPakResponseFileName, UnrealPakResponseFile, Compressed, RehydrateAssets, CryptoSettings, bForceEncryption);
CmdLine.AppendFormat(" -create={0}", CommandUtils.MakePathSafeToUseWithCommandLine(UnrealPakResponseFileName));
if (!String.IsNullOrEmpty(PatchSourceContentPath))
{
CmdLine.AppendFormat(" -generatepatch={0} -tempfiles={1}", CommandUtils.MakePathSafeToUseWithCommandLine(PatchSourceContentPath), CommandUtils.MakePathSafeToUseWithCommandLine(CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "TempFiles" + Path.GetFileNameWithoutExtension(OutputLocation.FullName))));
}
if (CryptoSettings != null && CryptoSettings.bDataCryptoRequired)
{
if (!string.IsNullOrEmpty(EncryptionKeyGuid))
{
CmdLine.AppendFormat(" -EncryptionKeyOverrideGuid={0}", EncryptionKeyGuid);
}
}
return CmdLine.ToString();
}
对应地,在打包程序中也包含了对于这些选项(例如cryptokeys)的解析。
void LoadKeyChain(const TCHAR* CmdLine, FKeyChain& OutCryptoSettings)
{
OutCryptoSettings.SetSigningKey( InvalidRSAKeyHandle );
OutCryptoSettings.GetEncryptionKeys().Empty();
// First, try and parse the keys from a supplied crypto key cache file
FString CryptoKeysCacheFilename;
if (FParse::Value(CmdLine, TEXT("cryptokeys="), CryptoKeysCacheFilename))
{
UE_LOG(LogPakFile, Display, TEXT("Parsing crypto keys from a crypto key cache file"));
KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, OutCryptoSettings);
}
else if (FParse::Param(CmdLine, TEXT("encryptionini")))
{
///...
}
else
{
UE_LOG(LogPakFile, Display, TEXT("Using command line for crypto configuration"));
FString EncryptionKeyString;
FParse::Value(CmdLine, TEXT("aes="), EncryptionKeyString, false);
}
FString EncryptionKeyOverrideGuidString;
FGuid EncryptionKeyOverrideGuid;
if (FParse::Value(CmdLine, TEXT("EncryptionKeyOverrideGuid="), EncryptionKeyOverrideGuidString))
{
FGuid::Parse(EncryptionKeyOverrideGuidString, EncryptionKeyOverrideGuid);
}
OutCryptoSettings.SetPrincipalEncryptionKey(OutCryptoSettings.GetEncryptionKeys().Find(EncryptionKeyOverrideGuid));
}
编译
这个可能相对比较隐晦,因为这些是通过宏定义实现的。
同样是在构建脚本中,读取Editor中的配置,然后定义了构建target时的定制化C++宏。这里我们关心的是
ProjectDefinitions.Add(String.Format("IMPLEMENT_ENCRYPTION_KEY_REGISTRATION()=UE_REGISTER_ENCRYPTION_KEY({0})", FormatHexBytes(CryptoSettings.EncryptionKey!.Key!)));
这个宏定义。下面是相对完整的宏定义生成代码
///@file:Engine/Source/Programs/UnrealBuildTool/Configuration/TargetRules.cs
/// <summary>
/// Constructor.
/// </summary>
/// <param name="Target">Information about the target being built</param>
public TargetRules(TargetInfo Target)
{
///...
// Setup macros for signing and encryption keys
EncryptionAndSigning.CryptoSettings CryptoSettings = EncryptionAndSigning.ParseCryptoSettings(CryptoSettingsDir, Platform, Logger);
if (CryptoSettings.IsAnyEncryptionEnabled())
{
ProjectDefinitions.Add(String.Format("IMPLEMENT_ENCRYPTION_KEY_REGISTRATION()=UE_REGISTER_ENCRYPTION_KEY({0})", FormatHexBytes(CryptoSettings.EncryptionKey!.Key!)));
}
else
{
ProjectDefinitions.Add("IMPLEMENT_ENCRYPTION_KEY_REGISTRATION()=");
}
if (CryptoSettings.IsPakSigningEnabled())
{
>> ProjectDefinitions.Add(String.Format("IMPLEMENT_SIGNING_KEY_REGISTRATION()=UE_REGISTER_SIGNING_KEY(UE_LIST_ARGUMENT({0}), UE_LIST_ARGUMENT({1}))", FormatHexBytes(CryptoS ettings.SigningKey!.PublicKey.Exponent!), FormatHexBytes(CryptoSettings.SigningKey.PublicKey.Modulus!)));
}
else
{
ProjectDefinitions.Add("IMPLEMENT_SIGNING_KEY_REGISTRATION()=");
}
}
在ModuleManager.h中包含了IMPLEMENT_ENCRYPTION_KEY_REGISTRATION和UE_REGISTER_ENCRYPTION_KEY的宏定义。
主要是UE_REGISTER_ENCRYPTION_KEY函数内通过
const unsigned char Key[32] = { VA_ARGS };
将构建中宏定义的__VA_ARGS__将key保存在了局部变量Key[32] 中,并且可以通过Callback返回。
这意味着:在构建生成的可执行文件中包含了密码的明文。
///@file:Engine\Source\Runtime\Core\Public\Modules\ModuleManager.h
#if IS_PROGRAM
/**
* Macro for registering encryption key for a project.
*/
#define UE_REGISTER_ENCRYPTION_KEY(...) \
struct FEncryptionKeyRegistration \
{ \
FEncryptionKeyRegistration() \
{ \
extern CORE_API void RegisterEncryptionKeyCallback(void (*)(unsigned char OutKey[32])); \
RegisterEncryptionKeyCallback(&Callback); \
} \
static void Callback(unsigned char OutKey[32]) \
{ \
const unsigned char Key[32] = { __VA_ARGS__ }; \
for(int ByteIdx = 0; ByteIdx < 32; ByteIdx++) \
{ \
OutKey[ByteIdx] = Key[ByteIdx]; \
} \
} \
} GEncryptionKeyRegistration;
#if IS_MONOLITHIC
#define IMPLEMENT_APPLICATION( ModuleName, GameName ) \
/* For monolithic builds, we must statically define the game's name string (See Core.h) */ \
TCHAR GInternalProjectName[64] = TEXT( GameName ); \
IMPLEMENT_FOREIGN_ENGINE_DIR() \
IMPLEMENT_LIVE_CODING_ENGINE_DIR() \
IMPLEMENT_LIVE_CODING_PROJECT() \
IMPLEMENT_SIGNING_KEY_REGISTRATION() \
IMPLEMENT_ENCRYPTION_KEY_REGISTRATION() \
IMPLEMENT_GAME_MODULE(FDefaultGameModuleImpl, ModuleName) \
PER_MODULE_BOILERPLATE \
FEngineLoop GEngineLoop;
解包
前面提到在
///@file:Engine\Source\Runtime\Core\Private\Misc\CoreDelegates.cpp
CORE_API void RegisterEncryptionKeyCallback(TEncryptionKeyFunc InCallback)
{
FCoreDelegates::GetPakEncryptionKeyDelegate().BindLambda([InCallback](uint8 OutKey[32])
{
InCallback(OutKey);
});
}
在挂载pak文件时,会获得key的内容并用来解密。注意下面的FCoreDelegates::GetPakEncryptionKeyDelegate()和前面宏定义操作的是相同变量。
///@file:Engine\Source\Runtime\PakFile\Private\IPlatformFilePak.cpp
bool FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath /*= NULL*/, bool bLoadIndex /*= true*/)
{
///...
if (bPakSuccess && IoDispatcherFileBackend.IsValid())
{
FGuid EncryptionKeyGuid = Pak->GetInfo().EncryptionKeyGuid;
FAES::FAESKey EncryptionKey;
if (!GetRegisteredEncryptionKeys().GetKey(EncryptionKeyGuid, EncryptionKey))
{
if (!EncryptionKeyGuid.IsValid() && FCoreDelegates::GetPakEncryptionKeyDelegate().IsBound())
{
FCoreDelegates::GetPakEncryptionKeyDelegate().Execute(EncryptionKey.Key);
}
}
FString UtocPath = FPaths::ChangeExtension(InPakFilename, TEXT(".utoc"));
///...
}
Outro
尽管加密PAK文件可以提升资产的安全性,但是在能够反汇编可执行文件的前提下,这种安全的提升其实并不高。例如这篇How to extract decryption key for Unreal Engine 4 *.pak files文件就是简单的搜索特定指令,并获得保存在程序中的密码。