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文件就是简单的搜索特定指令,并获得保存在程序中的密码。

posted on 2024-04-28 17:20  tsecer  阅读(269)  评论(0编辑  收藏  举报

导航