Runtime Retrieval of Decryption Key
For the encryption and signing of Pak, it is necessary to acquire the corresponding key at runtime.
As seen in the definitions of UE_REGISTER_ENCRYPTION_KEY and UE_REGISTER_SIGNING_KEY above, there are two extern declarations:
1 | extern void RegisterEncryptionKeyCallback(void (*)(unsigned char OutKey[32])); |
These two functions are methods for the engine to obtain the key:
1 | typedef void(*TSigningKeyFunc)(TArray<uint8>&, TArray<uint8>&); |
When these two functions are invoked (that is, from the constructors of FEncryptionKeyRegistration and FSigningKeyRegistration).
Recall the initialization process for the Monolithic module described in a previous article: UE Plugins and Tool Development: Basic Concepts #Monolithic Mode.
They will be created at the time the module starts.
When loading PAKE, FCoreDelegates::GetPakEncryptionKeyDelegate() is called to obtain the keys:
1 | void FPakPlatformFile::GetPakEncryptionKey(FAES::FAESKey& OutKey, const FGuid& InEncryptionKeyGuid) |
This key is then used to decrypt data:
1 | void DecryptData(uint8* InData, uint32 InDataSize, FGuid InEncryptionKeyGuid) |
This completes the whole process from compile-time encryption to runtime decryption. ## Serialization Structure of Pak
In a previous article, it was mentioned that UE’s PAK is a typical archive structure:
Generated through specific serialization rules, and reading at runtime also follows formatting rules:
- When mounting the PAK, first read the PakInfo from the end of the file, then retrieve the index’s offset.
- Obtain the EncodePakEntrys based on the index offset, which contains the PakEntry information for each file.
- Each PakEntry records the current file’s information to guide how to read and decompress.
It is also an important part of UE’s own virtual file system.
However, since the UE source code is open, the serialization structure of the PAK is not confidential. If the encryption key is acquired through reverse engineering, the files within the PAK can be decrypted directly using the logic of the official UnrealPak engine.
Additionally, some third-party tools, like UModel or UnrealPakViewer, can also achieve this. Therefore, aside from key and decryption obfuscation, it is also necessary to modify the serialization structure of the PAK itself.
Hardening Solutions
Disadvantages of the Default Solution
Based on the previous sections, the following conclusions can be drawn:
- The decryption key is directly compiled into code as a macro definition.
- Through engine source code + static analysis, it’s relatively easy to locate the function that retrieves the key.
- Exported symbols in the code can directly expose the location of decryption functions.
- PAK strings in the code (logs/ProfilingTag) can assist in locating key decryption functions.
- AES encryption is used by default, with obvious code characteristics that are easy to analyze.
- The serialization format of the PAK is quite generic, and given the key, it can be decrypted by the official UnrealPak or tools like UModel/UnrealPakViewer.
Therefore, based on these situations, we need to implement various modifications to the encryption scheme, increasing the cost of decryption as much as possible. However, due to security requirements, I can only introduce some encryption ideas without providing specific code.
Key Hardening
As mentioned in the first section, the key is generated by Base64 encoding into a macro definition, and retrieved at runtime. We can implement some unique key obfuscation logic to add a layer of processing to the key itself, preventing the encoded key from being easily obtained through static analysis.
Symbol Stripping
By default, if symbols are not stripped, using reverse engineering tools like IDA to open the executable can directly reveal function names:
This makes it easy to see the decryption function, and therefore obtain the key. Therefore, all distribution packages need to have symbols stripped.
After stripping, the symbol situation is:
Refer to the content of my previous article: Ultimate Optimization of UE Android APK Size
PAK String Stripping
For the same reasons, the engine logs extensively during the mounting of PAK. Analyzing the accessed locations of these log strings can assist in locating the decryption logic:

After finding the decryption function, attaching a debugger can reveal the actual key stored in memory.
Therefore, stripping PAK-related strings is crucial.
Log Stripping
The removal strategy should retain logs in Dev but strip them in Shipping:
1 | //++[lipengzha] Remove PAK related strings in shipping |
Thus, the UE_LOG macro can be wrapped such that it is forwarded in Dev but replaced with an empty macro in Shipping.
Boot Timing Stripping
The SCOPED_BOOT_TIMING introduced strings also need to be stripped:
1 | //++[lipengzha] Remove PAK related strings in shipping |
Then replace all UE_LOG with UE_PAK_LOG, and all SCOPED_BOOT_TIMING with UE_PAK_SCOPED_BOOT_TIMING in both IPlatformFilePak.cpp and IPlatformFilePak.h.
This will ensure that these strings are automatically stripped in Shipping.
PAK Structure Obfuscation
In the previous section (Serialization Structure of Pak), I introduced the serialization method of the PAK structure itself. Thus, for commercial projects, obfuscating the structure itself is also necessary.
Improvements can be made to the serialization of the PAK based on the existing UFS file system, modifying the organization of the structure and encrypting or altering data to prevent the public version engine and open-source tools from being able to decrypt after obtaining the key. This can also ensure the uniqueness of each PAK structure and decryption, avoiding a scenario where one being compromised leads to a mass breach.
This content won’t be elaborated on further, as each project needs to implement it uniquely.
Decompression Algorithm Obfuscation
Because files are usually compressed with a compression algorithm after being packaged into the PAK, they need to be decompressed at runtime. The selected compression algorithm for each file is recorded in the PakInfo and noted in each PakEntry’s index.
Therefore, some processing can also be implemented in this area. Even if the key and PAK structure are reverse engineered, additional obfuscation can be added when decompressing actual files:
Replacing AES Algorithm
As AES is standard in UE, it can be located by feature codes during reverse engineering. Thus, AES can be replaced or the code logic can be modified. The specific algorithm to replace it with depends on the project’s situation.
Dynamic Key Registration
According to the previous analysis, if all assets use a single default key, it is directly compiled into the code. If that key is compromised, all resources are at risk of exposure. Local decryption indeed has low security.
In such cases, for critical assets or files (such as unreleased commercial content), sometimes assets are pre-embedded in the client before their release time. This situation presents a significant risk if the same key is shared.
The engine itself provides logic for dynamically registering decryption keys:
1 | class CORE_API FCoreDelegates |
Thus, a dynamic key mechanism can be implemented in the update process. The encrypted PAK does not contain a decryption key in the program itself; only after the designated version releases the key can the client use it, ensuring no premature unpacking and leakage occurs. This can be combined according to the project’s needs.
Conclusion
This article introduces the default encryption logic of UE, analyzes potential risks, and provides some feasible hardening strategies for reference.
Note: The article analyzes the engine’s encryption process and reverse engineering techniques solely to explore safer methods, not as a crack tutorial. Any behavior related to game reverse engineering is not related to the author. Thank you!