├── README.md └── bitlocker.py /README.md: -------------------------------------------------------------------------------- 1 | Volatility Framework: bitlocker 2 | =============================== 3 | 4 | This plugin finds and extracts Full Volume Encryption Key (FVEK) from memory dumps and/or hibernation files. This allows rapid unlocking of systems that had BitLocker encrypted volumes mounted at the time of acquisition. 5 | 6 | Supported memory images: 7 | - Windows 10 (*work in progress*) 8 | - Windows 8.1 9 | - Windows Server 2012 R2 10 | - Windows 8 11 | - Windows Server 2012 12 | - Windows 7 13 | - Windows Server 2008 R2 14 | - Windows Server 2008 15 | - Windows Vista 16 | 17 | 18 | Example case - Windows 7 SP1 x64 19 | -------------------------------- 20 | 21 | *Evidence: Raw HDD image* 22 | 23 | **1) Determine partition layout and identify BitLocker volume** 24 | 25 | ```console 26 | elceef@cerebellum:~$ fdisk -l john_win7_x64.dd 27 | Disk john_win7_x64.dd: 298.1 GiB, 320072933376 bytes, 625142448 sectors 28 | Units: sectors of 1 * 512 = 512 bytes 29 | Sector size (logical/physical): 512 bytes / 512 bytes 30 | I/O size (minimum/optimal): 512 bytes / 512 bytes 31 | Disklabel type: dos 32 | Disk identifier: 0x51c47769 33 | 34 | Device Boot Start End Sectors Size Id Type 35 | john_win7_x64.dd1 * 2048 1050623 1048576 512M 7 HPFS/NTFS/exFAT 36 | john_win7_x64.dd2 1050624 316475391 315424768 150.4G 7 HPFS/NTFS/exFAT 37 | john_win7_x64.dd3 316475392 625137663 308662272 147.2G 7 HPFS/NTFS/exFAT 38 | ``` 39 | 40 | The last one starting from sector 316475392 is BitLocker protected. It can be verified by lookig at the filesystem header. Volumes encrypted with BitLocker will have a different signature than the standard NTFS header. A BitLocker encrypted volume starts with the "-FVE-FS-" signature. 41 | 42 | ```console 43 | elceef@cerebellum:~$ hexdump -C -s $((512*316475392)) -n 16 john_win7_x64.dd 44 | 25ba100000 eb 58 90 2d 46 56 45 2d 46 53 2d 00 02 08 00 00 |.X.-FVE-FS-.....| 45 | ``` 46 | 47 | **2) Locate and convert hibernation file** 48 | 49 | Mount the system volume starting from sector 1050624 in read-only mode. 50 | 51 | ```console 52 | elceef@cerebellum:~$ sudo mount -o loop,ro,offset=$((512*1050624)) john_win7_x64.dd /mnt/1 53 | ``` 54 | 55 | Convert hibernation file *hiberfil.sys* for further forensic analysis. 56 | 57 | ```console 58 | elceef@cerebellum:~$ vol -f /mnt/1/hiberfil.sys --profile Win7SP1x64 imagecopy -O hiberfil.raw 59 | ``` 60 | 61 | **3) Use the bitlocker plugin to extract FVEK** 62 | 63 | The plugin scans the memory image for BitLocker cryptographic allocations (memory pools) and extracts AES keys (FVEK). 64 | 65 | ```console 66 | elceef@cerebellum:~$ vol -f hiberfil.raw --profile Win7SP1x64 bitlocker 67 | Volatility Foundation Volatility Framework 2.5 68 | 69 | Address : 0xfa8009958c10 70 | Cipher : AES-256 71 | FVEK : d5b6e71adb0c2e2d38dafdcedade8fc11e8be631b9fed5b2ba5b51ba32a57cd1 72 | TWEAK : 49f9ecd5ddffcae44cde7f7a578b9a3ca5e79087826779e147de89423ebdf3f3 73 | 74 | ``` 75 | 76 | **4) Decrypt and access the volume** 77 | 78 | Decrypt the volume on-the-fly using previously extracted FVEK. 79 | 80 | ```console 81 | elceef@cerebellum:~$ sudo bdemount -k d5b6e71adb0c2e2d38dafdcedade8fc11e8be631b9fed5b2ba5b51ba32a57cd1:49f9ecd5ddffcae44cde7f7a578b9a3ca5e79087826779e147de89423ebdf3f3 -o $((512*316475392)) john_win7_x64.dd /crypt/1 82 | ``` 83 | 84 | Finally mount and access the filesystem. 85 | 86 | ```console 87 | elceef@cerebellum:~$ sudo mount -o loop,ro /crypt/1/bde1 /mnt/2 88 | elceef@cerebellum:~$ ls /mnt/2 89 | CONFIDENTIAL 90 | ``` 91 | 92 | 93 | Example case - Windows 8.1 x86 94 | ------------------------------ 95 | 96 | *Evidence: Raw memory image* 97 | 98 | Windows 8 and newer versions use Cryptography API: Next Generation (CNG) which creates a lot of dynamically allocated memory pools. For this reason, the keys are often located in several places in the memory. 99 | 100 | ```console 101 | elceef@cerebellum:~$ vol -f john_win81_x86.raw --profile Win81U1x86 bitlocker 102 | Volatility Foundation Volatility Framework 2.5 103 | 104 | Address : 0x872db068 105 | Cipher : AES-128 106 | FVEK : 48286dcd34d3ff215d705d68c5df4f08 107 | 108 | Address : 0x9ef55b08 109 | Cipher : AES-128 110 | FVEK : 48286dcd34d3ff215d705d68c5df4f08 111 | 112 | Address : 0xa4748b08 113 | Cipher : AES-128 114 | FVEK : 48286dcd34d3ff215d705d68c5df4f08 115 | 116 | ``` 117 | 118 | 119 | Contact 120 | ------- 121 | 122 | To send questions, comments or a chocolate, just drop an e-mail at 123 | [marcin@ulikowski.pl](mailto:marcin@ulikowski.pl) 124 | 125 | You can also reach me via: 126 | 127 | - Twitter: [@elceef](https://twitter.com/elceef) 128 | - LinkedIn: [Marcin Ulikowski](https://pl.linkedin.com/in/elceef) 129 | 130 | -------------------------------------------------------------------------------- /bitlocker.py: -------------------------------------------------------------------------------- 1 | # Volatility Framework plugin: bitlocker 2 | # 3 | # 4 | # Author: 5 | # Marcin Ulikowski 6 | # 7 | # 8 | # Based on research by: 9 | # Jesse Kornblum 10 | # 11 | # AES key schedule crypto code dervied from SlowAES project: 12 | # https://code.google.com/p/slowaes/ 13 | # Modified to verify a key schedule, not just compare them. 14 | # 15 | # Special thanks: 16 | # Piotr Chmylkowski 17 | # Romain Coltel 18 | # 19 | # This plugin is free software; you can redistribute it and/or modify 20 | # it under the terms of the GNU General Public License as published by 21 | # the Free Software Foundation; either version 2 of the License, or 22 | # (at your option) any later version. 23 | # 24 | # This plugin is distributed in the hope that it will be useful, 25 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 27 | # GNU General Public License for more details. 28 | # 29 | # You should have received a copy of the GNU General Public License 30 | # along with this plugin. If not, see . 31 | 32 | """ 33 | @author: Marcin Ulikowski 34 | @license: GNU General Public License 2.0 35 | @contact: marcin@ulikowski.pl 36 | @organization: http://ulikowski.pl 37 | """ 38 | 39 | import os 40 | import volatility.plugins.common as common 41 | import volatility.utils as utils 42 | import volatility.obj as obj 43 | import volatility.poolscan as poolscan 44 | import volatility.debug as debug 45 | 46 | 47 | class bitlocker(common.AbstractWindowsCommand): 48 | '''Extracts BitLocker FVEK (Full Volume Encryption Key)''' 49 | 50 | sbox = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 51 | 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 52 | 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 53 | 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 54 | 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 55 | 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 56 | 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 57 | 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 58 | 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 59 | 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 60 | 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 61 | 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 62 | 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 63 | 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 64 | 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 65 | 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 66 | 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 67 | 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 68 | 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 69 | 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 70 | 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 71 | 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 72 | 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 73 | 0x54, 0xbb, 0x16] 74 | 75 | Rcon = [0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 76 | 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 77 | 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 78 | 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 79 | 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 80 | 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 81 | 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 82 | 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 83 | 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 84 | 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 85 | 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 86 | 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 87 | 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 88 | 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 89 | 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 90 | 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 91 | 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 92 | 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 93 | 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 94 | 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 95 | 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 96 | 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 97 | 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 98 | 0xe8, 0xcb ] 99 | 100 | def __init__(self, config, *args, **kwargs): 101 | common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs) 102 | config.add_option('DUMP-DIR', default = None, help = 'Directory in which to dump FVEK') 103 | 104 | @staticmethod 105 | def is_valid_profile(profile): 106 | return profile.metadata.get('major', 0) >= 6 107 | 108 | def rotate(self, word): 109 | return word[1:] + word[:1] 110 | 111 | def core(self, word, iteration): 112 | word = self.rotate(word) 113 | for i in range(4): 114 | word[i] = self.sbox[word[i]] 115 | word[0] = word[0] ^ self.Rcon[iteration] 116 | return word 117 | 118 | def validSchedule(self, keySchedule, size, expandedKeySize): 119 | keySchedule = map(ord, keySchedule) 120 | key = keySchedule[:size] 121 | currentSize = 0 122 | rconIteration = 1 123 | expandedKey = [0] * expandedKeySize 124 | 125 | for j in range(size): 126 | expandedKey[j] = key[j] 127 | currentSize += size 128 | 129 | while currentSize < expandedKeySize: 130 | t = expandedKey[currentSize-4:currentSize] 131 | 132 | if currentSize % size == 0: 133 | t = self.core(t, rconIteration) 134 | rconIteration += 1 135 | 136 | if size == 32 and ((currentSize % size) == 16): 137 | for l in range(4): 138 | t[l] = self.sbox[t[l]] 139 | 140 | for m in range(4): 141 | expandedKey[currentSize] = expandedKey[currentSize - size] ^ t[m] 142 | 143 | if expandedKey[currentSize] != keySchedule[currentSize]: 144 | return False 145 | 146 | currentSize += 1 147 | 148 | return True 149 | 150 | def calculate(self): 151 | addr_space = utils.load_as(self._config) 152 | 153 | windows_version = (addr_space.profile.metadata.get('major', 0), addr_space.profile.metadata.get('minor', 0)) 154 | 155 | if windows_version <= (6, 1): 156 | pool_tag = 'FVEc' 157 | else: 158 | pool_tag = 'Cngb' 159 | 160 | scanner = poolscan.SinglePoolScanner() 161 | scanner.checks = [ 162 | ('PoolTagCheck', dict(tag = pool_tag)), 163 | ('CheckPoolSize', dict(condition = lambda x: x > 184)), 164 | ] 165 | 166 | for addr in scanner.scan(addr_space): 167 | pool = obj.Object('_POOL_HEADER', offset = addr, vm = addr_space) 168 | 169 | pool_alignment = obj.VolMagic(pool.obj_vm).PoolAlignment.v() 170 | pool_size = int(pool.BlockSize * pool_alignment) 171 | 172 | debug.debug('Scanning potential BitLocker pool @ {0:#010x}'.format(pool.obj_offset)) 173 | 174 | aes = [] 175 | buf = addr_space.zread(addr, pool_size) 176 | 177 | for i in range(8, pool_size - 176): 178 | if self.validSchedule(buf[i:i+176], 16, 176): 179 | aes.append(buf[i:i+16]) 180 | 181 | for i in range(8, pool_size - 240): 182 | if self.validSchedule(buf[i:i+240], 32, 240): 183 | aes.append(buf[i:i+32]) 184 | 185 | debug.debug('AES keys found: {}'.format(len(aes))) 186 | 187 | if 0 < len(aes) <= 2: 188 | yield pool, aes[0], aes[1] if len(aes) > 1 else '' 189 | 190 | def render_text(self, outfd, data): 191 | data = sorted(data, key = lambda x: x[1]) 192 | 193 | outfd.write('\n') 194 | 195 | for pool, fvek, tweak in data: 196 | outfd.write('Address : {0:#010x}\n'.format(pool.obj_offset)) 197 | outfd.write('Cipher : AES-{}\n'.format(len(fvek) * 8)) 198 | outfd.write('FVEK : {}\n'.format(''.join('{:02x}'.format(ord(i)) for i in fvek))) 199 | if tweak: 200 | outfd.write('TWEAK : {}\n'.format(''.join('{:02x}'.format(ord(i)) for i in tweak))) 201 | 202 | if self._config.DUMP_DIR: 203 | full_path = os.path.join(self._config.DUMP_DIR, '{0:#010x}.fvek'.format(pool.obj_offset)) 204 | 205 | with open(full_path, "wb") as fvek_file: 206 | fvek_file.write(fvek + tweak) 207 | 208 | outfd.write('FVEK dumped to file: {}\n'.format(full_path)) 209 | 210 | outfd.write('\n') 211 | --------------------------------------------------------------------------------