├── README.md └── WarbirdModern.md /README.md: -------------------------------------------------------------------------------- 1 | # Warbird Docs 2 | 3 | Documentation of Microsoft's Warbird obfuscation 4 | 5 | - [Modern Warbird (Windows 10 1703+)](./WarbirdModern.md) 6 | -------------------------------------------------------------------------------- /WarbirdModern.md: -------------------------------------------------------------------------------- 1 | # Modern Warbird 2 | 3 | By WitherOrNot 4 | 5 | ## Introduction 6 | 7 | Warbird in modern Windows is a surprisingly shallow system, with relatively few tricks that are actually in use. For this writeup, only the obfuscation methods that have been observed in actual binaries will be discussed for brevity. 8 | 9 | ## Encryption Segments 10 | 11 | Warbird encrypts and decrypts sets of functions, known as "segments", collectively at runtime. Functions contained in particular segments are not necessarily related, and often functions from one segment will reference functions in another segment. The main purpose of this system is to prevent dumping all encrypted functions at once through runtime unpacking. 12 | 13 | Within binaries that have encryption segments, like `sppsvc.exe`, there are multiple sections named `?g_Encry`, and each contains an `ENCRYPTION_SEGMENT` struct describing the segment as follows: 14 | 15 | ```c 16 | typedef struct _FEISTEL_ROUND_DATA { 17 | DWORD round_function_index; // Index of function used for round 18 | // Parameters for round functions 19 | DWORD param1; 20 | DWORD param2; 21 | DWORD param3; 22 | } FEISTEL_ROUND_DATA; 23 | 24 | typedef struct _ENCRYPTION_BLOCK { 25 | DWORD flags; 26 | DWORD rva; 27 | DWORD length; 28 | } ENCRYPTION_BLOCK; 29 | 30 | struct PRIVATE_RELOCATION_ITEM 31 | { 32 | ULONG rva: 28; 33 | ULONG reloc_type: 4; 34 | }; 35 | 36 | typedef struct _ENCRYPTION_SEGMENT { 37 | BYTE hash[32]; // SHA-256 hash of all segment data that follows 38 | ULONG segment_size; 39 | ULONG _padding0; 40 | ULONG segment_offset; 41 | ULONG reloc_table_offset; // Offset to a global table of PRIVATE_RELOCATION_ITEMs 42 | ULONG reloc_count; // Length of table 43 | ULONG _padding1; 44 | UINT64 preferred_image_base; // Preferred base address of PE as specified in headers 45 | ULONG segment_id; // Internal segment ID as described in Warbird configuration 46 | ULONG _padding2; 47 | UINT64 key; // Feistel cipher key 48 | FEISTEL_ROUND_DATA rounds[10]; 49 | DWORD num_blocks; // Length of blocks array 50 | ENCRYPTION_BLOCK blocks[1]; // Blocks of data within module to be decrypted 51 | } ENCRYPTION_SEGMENT; 52 | ``` 53 | 54 | During runtime, encryption segments are decrypted by a system call using `NtQuerySystemInformation`. The arguments provided for this call are contained in the following struct: 55 | 56 | ```c 57 | typedef struct _ENCRYPTION_SEGMENT_ARGS { 58 | UINT64 operation; // 1 (decrypt) or 2 (re-encrypt) 59 | PVOID segment; // Pointer to ENCRYPTION_SEGMENT 60 | PVOID base; // Base address of module 61 | UINT64 preferred_base; // Same as segment preferred_image_base 62 | PVOID reloc_table; // Address of the same table referred to by reloc_table_offset 63 | UINT64 reloc_count; // Number of relocations in global table 64 | } ENCRYPTION_SEGMENT_ARGS 65 | ``` 66 | 67 | and the system call to decrypt the segment is done like so, with `operation` set to `1`: 68 | 69 | ```c 70 | NtQuerySystemInformation(0xB9, &segment_args, sizeof(segment_args), 0); 71 | ``` 72 | 73 | After the system call, the encrypted data is decrypted in-place, and the binary is able to jump or call to any formerly encrypted functions as usual. Once the encrypted functions are no longer needed, another system call is done with `operation` set to `2` to re-encrypt the segment. 74 | 75 | ## Heap Executes 76 | 77 | While encryption segments may deter basic static analysis, they may be easily defeated by placing a breakpoint within the encrypted code, waiting for it to be called, dumping the process from memory, and piecing together the decrypted segments. Thus, for more sensitive functions, another method of code encryption is used, called "heap execute". This method allocates decrypted code in the process heap and jumps to it, preventing simple methods of breakpointing and disrupting attempts at control flow analysis or code tracing. 78 | 79 | Heap executes use structs typically placed together near the beginning of the `.text` section, described as follows: 80 | 81 | ```c 82 | // Struct is aligned to 8 bytes 83 | typedef struct _HEAP_EXECUTE { 84 | BYTE hash[32]; 85 | ULONG hexec_size; 86 | ULONG virt_stack_limit; 87 | ULONG hexec_offset:28; 88 | ULONG _padding0; 89 | ULONG checksum:8; // Unused 90 | ULONG unused0:8; // Unused 91 | ULONG rva:28; 92 | ULONG size:28; 93 | ULONG unused1:28; // Unused 94 | ULONG unused2:28; // Unused 95 | UINT64 key; 96 | FEISTEL_ROUND_DATA rounds[10]; 97 | } HEAP_EXECUTE; 98 | ``` 99 | 100 | Each `HEAP_EXECUTE` provides only 1 function to be decrypted. Similar to encryption segments, heap executes are invoked with a call to `NtQuerySystemInformation`, using the following arguments struct: 101 | 102 | ```c 103 | typedef struct _HEAP_EXECUTE_ARGS { 104 | UINT64 operation; // 3 (heap execute) 105 | PVOID heap_execute; // Pointer to HEAP_EXECUTE 106 | PVOID return_val; // Pointer to variable that will hold return value 107 | ULONG arguments[1]; // Unbounded array of arguments to be passed to function 108 | } 109 | ``` 110 | 111 | The system call is the same as with encryption segments. Once called, process execution will jump to the decrypted code in the heap. 112 | 113 | Letting `rip` be the address of the start of the decrypted code, the heap layout is as follows: 114 | 115 | ``` 116 | rip - 0x10: Call offset 117 | rip - 0x08: NtQuerySystemInformation syscall number (usually 0x36) 118 | rip: Decrypted code 119 | ``` 120 | 121 | Letting `rsp` be the stack pointer, the stack layout is as follows: 122 | 123 | ``` 124 | rsp: Heap execute struct address 125 | rsp + 0x08: Argument 1 126 | rsp + 0x10: Argument 2 127 | rsp + 0x18: Argument 3 128 | ... 129 | ``` 130 | 131 | Code within heap executes has some oddities. For instance, all external function calls must be offset with the "call offset" value placed on the heap. For example, to call `GetProcAddress`, letting `rdx` contain the call offset: 132 | 133 | ```asm 134 | lea rax, GetProcAddress 135 | call [rdx+rax] 136 | ``` 137 | 138 | For ease of reading, the instruction `lea rdx, [rip - 7]` at the beginning of the heap-executed code may be replaced with `xor rdx, rdx` to eliminate call offsets in decompilation. 139 | 140 | Additionally, calls to `NtQuerySystemInformation` are replaced with `syscall` instructions. The register setup prior to `syscall` is as follows: 141 | 142 | ``` 143 | rax = syscall number from heap 144 | r10d = 0xB9 145 | rdx = address of arguments 146 | r8d = size of arguments 147 | r9 = 0 148 | ``` 149 | 150 | Prior to returning, a `NtQuerySystemInformation` system call is done with `rdx`, `r8`, and `r9` set to 0. This resets the stack frame before returning to prevent a crash. 151 | 152 | ## Kernel-Level Decryption 153 | 154 | When `NtQuerySystemInformation` is called with a `SYSTEM_INFORMATION_CLASS` value of `0xB9`, the call proceeds to `WbDispatchOperation`. From here, the call proceeds to a specific function depending on the `operation` value provided in the arguments struct. The possible operations are summarized in the table below. 155 | 156 | |Operation Number|Operation| 157 | |-|-| 158 | |0|No-op| 159 | |1|Decrypt encryption segment| 160 | |2|Re-encrypt encryption segment| 161 | |3|Heap execute| 162 | |4|Heap execute return| 163 | |5|Unused| 164 | |6|Unused| 165 | |7|Remove process| 166 | |8|Unload module| 167 | 168 | All routines ensure that code decryption operations are done on read-only executable pages belonging to an binary signed as a "Windows System Component", presumably as a form of [exploit](https://bugs.chromium.org/p/project-zero/issues/detail?id=1391) [mitigation](https://www.youtube.com/watch?v=gu_i6LYuePg). The intrepid may easily bypass this restriction by simply mapping such a binary in their own code, as no other integrity checks are done by the kernel. 169 | 170 | Code decryption is done within the kernel, and the round functions are executed by the function `WarbirdCrypto::CCipherFeistel64::CallRoundFunction`, which is called by a feistel decryption routine whose name is not available in public symbols. This routine has arguments as follows: 171 | 172 | ```c 173 | void FeistelDecrypt( 174 | FEISTEL_ROUND_DATA* rounds, 175 | BYTE* input, 176 | BYTE* output, 177 | ULONG length, 178 | UINT64 key, 179 | ULONG iv, 180 | BYTE* checksum 181 | ); 182 | ``` 183 | 184 | Knowing the address of this routine, a reverse-engineer can trivially use the `ntoskrnl.exe` binary to decrypt Warbird-encrypted binaries. Implementation of this method is left as an exercise for the reader. 185 | --------------------------------------------------------------------------------