Example of Windows Warbird Encryption/Decryption
Everything in this post was done on a Windows 10 22H2 machine. Kernel version was: 10.0.19041.2486
Introduction
Microsoft Warbird is an undocumented encryption technology generally used for things relating to software licensing (DRM) and security mechanisms. There has been some, but not much, previous open source research. Some links which provide further insight:
- https://github.com/KiFilterFiberContext/warbird-obfuscator
- https://github.com/KiFilterFiberContext/microsoft-warbird/
In addition, Alex Ionescu talked about Warbird in depth during this presentation.
The Warbird technology is appears to be designed to be integrated at compile time, and could function either as an obfuscation approach on the existing code, or as some type of “enclave” block encryptor. This second approach is what this post will dive into.
SystemControlFlowTransition
There is a semi-undocumented system information class for NtQuerySystemInformation
called SystemControlFlowTransition
(0xB9) which when called ends up in the WbDispatchOperation
function. Placing a breakpoint on this function will show that the sppsvc.exe
process periodically calls this. More on this later. WbDispatchOperation
will branch into several different functions depending on the operation
value passed when calling NtQuerySystemInformation
. The struct looks something like this:
These are the operations:
- 1 = WbDecryptEncryptionSegment
- 2 = WbReEncryptEncryptionSegment
- 3 = WbHeapExecuteCall
- 4 = non symbol name function
- 5 = non symbol name function.
- 6 = same as case 5
- 7 = WbRemoveWarbirdProcess
- 8 = WbProcessStartup
- 9 = WbProcessModuleUnload
Each one of these operations has some type of unique operation dependent data attached to the initial struct. Reversing thesppsvc.exe
can give us hints on how these structures should be formatted and how they are called. The decrypt and re-encrypt steps can occur multiple times. The rough pseudocode based onsppsvc.exe
for callingWbProcessStartup
looks like this:
Where buffer
:
The name WbProcessStartup
seems to suggest that sometype this call does some form of initialization which is required before decrypting/reencrypting data. However, this does not appear to be the case, and the calls to decrypt/reencrypt seem to work without.
The rough pseudocode based on sppsvc.exe
for calling WbDecyptEncryptionSegment
looks like this:
It’s important to note that the WarbirdPayload
is actually embedded in the sppsvc.exe
binary in a section named ?g_Encry
. There are multiple of these sections.
Payload Format
For decryption (WbDecyptEncryptionSegment
) the payload is in the format of WB_PAYLOAD
structure.
The most important field is the Segments
, an array of WB_SEGMENT
structures. These point (using RVA) to the encrypted blocks of code to be decrypted. The flags field in the WB_SEGMENT
specify what protection the segment should be decrypted as. If any value is present, it is a PAGE_EXECUTE_READ
else it is PAGE_READONLY
.
How to Encrypt
As you may have noticed in the supported operations values, and from the description of the sppsvc.exe
usage, there is no encrypt. This is most likely because this API is intended to be used only after a binary is compiled with the Warbird encrypted chunks. To get around this, you can use the WbReEncryptEncryptionSegment
functionality to first decrypt some random data, replace that data with the bytes we want to encrypt. Then, reencrypt this same memory. If you then save this strucutre (the segment bytes as well as the payload structure) you can then have memory that when restored, can simply be decrypted.
The Mitigation
Note that sppsvc.exe
is a Windows signed binary. This brings us to a problem. In Alex Ionescu’s talk he explained that part of the patch Microsoft made to fix the bug he found was only allow decryption of payloads that were signed by the Windows team at Microsoft. The kernel does this by calling ZwQueryVirtualMemory
with the MemoryImageInformation
class on the memory passed as the payload. Process Hacker’s NT headers have the structure for this undocumented memory class. The ImageFlags
is then compared to ensure the memory was backed with the appropriate signature.
The Bypass
This however, is not a perfect mitigation as at runtime memory which has been backed by a PE with specific signatures can be modified simply by changing the existing virtual memory protections (RX to RW or RWX).
Putting it all Together
Here is a simplified view of how this whole process will work. The “code” resides within the address space of a signed image.
PoC Code
This PoC will simply follow the steps above. In summary, this will load a signed DLL as the scratch space, then decrypting, writing code, reencrypting, and finally decrypting again.