├── README.md └── bitlocker.py /README.md: -------------------------------------------------------------------------------- 1 | # Volatility plugin: BitLocker 2 | 3 | Volatility plugin that retrieves the Full Volume Encryption Key (FVEK) in memory. The FVEK can then be used with [Dislocker](https://github.com/Aorimn/dislocker) to decrypt the volume. 4 | This plugin has been tested on every 64-bit Windows version from Windows 7 to Windows 10 and is fully compatible with Dislocker. 5 | 6 | This work was done during my internship at [Synetis](https://www.synetis.com/). 7 | 8 | Available options: 9 | 10 | - Dump-dir: Dump the key to use it with bdemount, requires an output path 11 | - Dislocker: Dump the key to use it with Dislocker, requires an output path 12 | - Verbose: Add more information about the memory pools currently reviewed 13 | - Debug: When the correct FVEK is not returned, it might help 14 | 15 | ## Installation 16 | 17 | Just copy the bitlocker.py file into the volatily plugin path: 18 | 19 | ``` 20 | cp bitlocker.py path/to/volatility/volatility/plugins/ 21 | ``` 22 | 23 | ## Example 24 | 25 | Dump a memory image (it can be done using FTK Imager for example), and type: 26 | 27 | ``` 28 | python vol.py -f ${DUMP.raw} bitlocker --profile=${Windows_Profile} 29 | ``` 30 | 31 | This will print the potential found FVEKs. The first returned should be the one as the plugin goes from the current Windows versions to the oldest. 32 | 33 | 34 | To test the FVEK with Dislocker, you can add the Dislocker option: 35 | 36 | ``` 37 | python vol.py -f ${DUMP.raw} bitlocker --profile=${Windows_Profile} --dislocker /path/to/dump 38 | ``` 39 | 40 | The output will look like this: 41 | 42 | ``` 43 | Volatility Foundation Volatility Framework 2.6.1 44 | 45 | [FVEK] Address : 0xb7811050c9a0 46 | [FVEK] Cipher : AES-XTS 128 bit (Win 10+) 47 | [FVEK] FVEK: 3ba9a1c2dde7c63e5f7851914a9dd120 48 | [DISL] FVEK for Dislocker dumped to file: path/to/dump/0xb7811050c9a0-Dislocker.fvek 49 | 50 | 51 | 52 | [FVEK] Address : 0xb78110504cc0 53 | [FVEK] Cipher : AES 128-bit (Win 8+) 54 | [FVEK] FVEK: 8002ed825cfe78a3148640365511c03b 55 | [DISL] FVEK for Dislocker dumped to file: path/to/dump/0xb78110504cc0-Dislocker.fvek 56 | 57 | 58 | 59 | [FVEK] Address : 0xb78110ad8580 60 | [FVEK] Cipher : AES 256-bit (Win 8+) 61 | [FVEK] FVEK: 5f75f4782de42f3df2c33b3a89a5d15775730e47327d4a1160c0559f3fd752d0 62 | [DISL] FVEK for Dislocker dumped to file: path/to/dump/0xb78110ad8580-Dislocker.fvek 63 | 64 | 65 | 66 | [FVEK] Address : 0xb781133b4990 67 | [FVEK] Cipher : AES 128-bit (Win 8+) 68 | [FVEK] FVEK: 922eff6970d6b214d81539297aea715f 69 | [DISL] FVEK for Dislocker dumped to file: path/to/dump/0xb781133b4990-Dislocker.fvek 70 | ``` 71 | 72 | After that, you can mount the disk by using Dislocker: 73 | 74 | ``` 75 | dislocker -k path/to/dump/0xb7811050c9a0-Dislocker.fvek /path/to/disk /path/to/dislocker && mount /path/to/dislocker/dislocker-file /path/to/mount 76 | ``` 77 | 78 | ## Issues with bdemount 79 | 80 | While Dislocker will mount the volume in read-write mode, bdemount will respect the hibernation flag and may mount it in read-only mode. Moreover, there is one known issue which makes bdemount and my output not compatible for AES-XTS 128-bit key. 81 | 82 | I recommend to use Dislocker. 83 | 84 | ## Credits 85 | 86 | Credits to Marcin Ulikowski (https://github.com/volatilityfoundation/community/tree/master/MarcinUlikowski) and TribalChicken (https://github.com/tribalchicken/volatility-bitlocker and https://tribalchicken.net/recovering-bitlocker-keys-on-windows-8-1-and-10) for previous works. 87 | -------------------------------------------------------------------------------- /bitlocker.py: -------------------------------------------------------------------------------- 1 | import volatility.plugins.common as common 2 | import volatility.utils as utils 3 | import volatility.obj as obj 4 | import volatility.win32.tasks as tasks 5 | from volatility.renderers import TreeGrid 6 | from volatility.renderers.basic import Address 7 | import volatility.poolscan as poolscan 8 | import binascii 9 | import os 10 | 11 | 12 | class KeyPoolScan(poolscan.SinglePoolScanner): 13 | """ Pool scanner """ 14 | 15 | 16 | class Bitlocker(common.AbstractWindowsCommand): 17 | """Extract Bitlocker FVEK. Supports Windows 7 - 10.""" 18 | 19 | def __init__(self, config, *args, **kwargs): 20 | common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs) 21 | config.add_option('DUMP-DIR', default=None, help='Directory in which to dump FVEK (can be used for bdemount)') 22 | config.add_option('DISLOCKER', default=None, help='Directory in which to dump FVEK for Dislocker') 23 | config.add_option('VERBOSE', default=None, help='Add more information') 24 | config.add_option('DEBUG', default=None, help='Here to Debug offset') 25 | 26 | def calculate(self): 27 | PoolSize = { 28 | 'Fvec128': 508, 29 | 'Fvec256': 1008, 30 | 'Cngb128': 632, 31 | 'Cngb256': 672, 32 | 'None128': 1230, 33 | 'None256': 1450, 34 | } 35 | BLMode = { 36 | '00': 'AES 128-bit with Diffuser', 37 | '01': 'AES 256-bit with Diffuser', 38 | '02': 'AES 128-bit', 39 | '03': 'AES 256-bit', 40 | '10': 'AES 128-bit (Win 8+)', 41 | '20': 'AES 256-bit (Win 8+)', 42 | '30': 'AES-XTS 128 bit (Win 10+)', 43 | '40': 'AES-XTS 256 bit (Win 10+)', 44 | } 45 | 46 | address_space = utils.load_as(self._config) 47 | winver = (address_space.profile.metadata.get("major", 0), address_space.profile.metadata.get("minor", 0), 48 | address_space.profile.metadata.get("build")) 49 | arch = address_space.profile.metadata.get("memory_model", 0) 50 | 51 | if winver >= (6, 4, 10241): 52 | mode = "30" 53 | if self._config.VERBOSE: 54 | print( 55 | "\n[INFO] Looking for some FVEKs inside memory pools used by BitLocker in Windows 10/2016/2019.\n") 56 | tweak = "Not Applicable" 57 | poolsize = lambda x: x >= PoolSize['None128'] and x <= PoolSize['None256'] 58 | scanner = KeyPoolScan() 59 | scanner.checks = [ 60 | ('PoolTagCheck', dict(tag="None")), 61 | ('CheckPoolSize', dict(condition=poolsize)), 62 | ('CheckPoolType', dict(paged=False, non_paged=True)), 63 | ] 64 | if (arch == '64bit'): 65 | fvek1OffsetRel = 0x9c 66 | fvek2OffsetRel = 0xe0 67 | fvek3OffsetRel = 0xc0 # Just for W2016 and W2019 using AES-CBC encryption method 68 | for offset in scanner.scan(address_space): 69 | pool = obj.Object("_POOL_HEADER", offset=offset, vm=address_space) 70 | f1 = address_space.zread(offset + fvek1OffsetRel, 64) 71 | f2 = address_space.zread(offset + fvek2OffsetRel, 64) 72 | f3 = address_space.zread(offset + fvek3OffsetRel, 64) 73 | if f1[0:16] == f2[0:16]: 74 | if f1[16:32] == f2[16:32]: 75 | if self._config.DISLOCKER: 76 | fbis = binascii.unhexlify("04") + binascii.unhexlify("80") + f1 77 | yield pool, BLMode['40'], tweak, f1[0:32], [fbis] 78 | else: 79 | yield pool, BLMode['40'], tweak, f1[0:32], [] 80 | else: 81 | if self._config.DISLOCKER: 82 | fbis = binascii.unhexlify("05") + binascii.unhexlify("80") + f1 83 | yield pool, BLMode['30'], tweak, f1[0:16], [fbis] 84 | else: 85 | yield pool, BLMode['30'], tweak, f1[0:16], [] 86 | if f1[0:16] == f3[0:16]: # Should be AES-CBC 87 | if f1[16:32] == f3[16:32]: 88 | if self._config.DISLOCKER: 89 | fbis = binascii.unhexlify("03") + binascii.unhexlify("80") + f1 90 | yield pool, BLMode['20'], tweak, f1[0:32], [fbis] 91 | else: 92 | yield pool, BLMode['20'], tweak, f1[0:32], [] 93 | else: 94 | if self._config.DISLOCKER: 95 | fbis = binascii.unhexlify("02") + binascii.unhexlify("80") + f1 96 | yield pool, BLMode['10'], tweak, f1[0:16], [fbis] 97 | else: 98 | yield pool, BLMode['10'], tweak, f1[0:16], [] 99 | if self._config.DEBUG: 100 | fvek = [] 101 | print("---------- START ----------") 102 | for o, h, c in utils.Hexdump(f1): 103 | fvek.append(h) 104 | print(fvek) 105 | fvek = [] 106 | for o, h, c in utils.Hexdump(f2): 107 | fvek.append(h) 108 | print(fvek) 109 | fvek = [] 110 | for o, h, c in utils.Hexdump(f3): 111 | fvek.append(h) 112 | print(fvek) 113 | 114 | if winver >= (6, 2): 115 | if self._config.VERBOSE: 116 | print( 117 | "\n[INFO] Looking for some FVEKs inside memory pools used by BitLocker in Windows 8/8.1/2012/older 10 versions.\n") 118 | tweak = "Not Applicable" 119 | poolsize = lambda x: x >= PoolSize['Cngb128'] and x <= PoolSize['Cngb256'] 120 | scanner = KeyPoolScan() 121 | scanner.checks = [ 122 | ('PoolTagCheck', dict(tag="Cngb")), 123 | ('CheckPoolSize', dict(condition=poolsize)), 124 | ('CheckPoolType', dict(paged=False, non_paged=True)), 125 | ] 126 | 127 | if (arch == '32bit'): 128 | modeOffsetRel = 0x5C 129 | fvek1OffsetRel = 0x4C 130 | fvek2OffsetRel = 0x9C 131 | 132 | if (arch == '64bit'): 133 | modeOffsetRel = 0x68 134 | fvek1OffsetRel = 0x6C 135 | fvek2OffsetRel = 0x90 136 | 137 | for offset in scanner.scan(address_space): 138 | pool = obj.Object("_POOL_HEADER", offset=offset, vm=address_space) 139 | f1 = address_space.zread(offset + fvek1OffsetRel, 64) 140 | f2 = address_space.zread(offset + fvek2OffsetRel, 64) 141 | if f1[0:16] == f2[0:16]: 142 | if f1[16:32] == f2[16:32]: 143 | if self._config.DISLOCKER: 144 | fbis = binascii.unhexlify("03") + binascii.unhexlify("80") + f1 145 | yield pool, BLMode['20'], tweak, f1[0:32], [fbis] 146 | else: 147 | yield pool, BLMode['20'], tweak, f1[0:32], [] 148 | else: 149 | if self._config.DISLOCKER: 150 | fbis = binascii.unhexlify("02") + binascii.unhexlify("80") + f1 151 | yield pool, BLMode['10'], tweak, f1[0:16], [fbis] 152 | else: 153 | yield pool, BLMode['10'], tweak, f1[0:16], [] 154 | if winver >= (6, 0): 155 | 156 | POOLSIZE_X86_AESDIFF = 976 157 | POOLSIZE_X86_AESONLY = 504 158 | POOLSIZE_X64_AESDIFF = 1008 159 | POOLSIZE_X64_AESONLY = 528 160 | 161 | OFFSET_DB = { 162 | POOLSIZE_X86_AESDIFF: { 163 | 'CID': 24, 164 | 'FVEK1': 32, 165 | 'FVEK2': 504 166 | }, 167 | POOLSIZE_X86_AESONLY: { 168 | 'CID': 24, 169 | 'FVEK1': 32, 170 | 'FVEK2': 336 171 | }, 172 | POOLSIZE_X64_AESDIFF: { 173 | 'CID': 44, 174 | 'FVEK1': 48, 175 | 'FVEK2': 528 176 | }, 177 | POOLSIZE_X64_AESONLY: { 178 | 'CID': 44, 179 | 'FVEK1': 48, 180 | 'FVEK2': 480 181 | }, 182 | } 183 | 184 | addr_space = utils.load_as(self._config) 185 | 186 | scanner = poolscan.SinglePoolScanner() 187 | scanner.checks = [ 188 | ('PoolTagCheck', dict(tag='FVEc')), 189 | ('CheckPoolSize', dict(condition=lambda x: x in list(OFFSET_DB.keys()))), 190 | ] 191 | 192 | for addr in scanner.scan(addr_space): 193 | pool = obj.Object('_POOL_HEADER', offset=addr, vm=addr_space) 194 | 195 | pool_alignment = obj.VolMagic(pool.obj_vm).PoolAlignment.v() 196 | pool_size = int(pool.BlockSize * pool_alignment) 197 | 198 | cid = addr_space.zread(addr + OFFSET_DB[pool_size]['CID'], 2) 199 | fvek1 = addr_space.zread(addr + OFFSET_DB[pool_size]['FVEK1'], 32) 200 | fvek2 = addr_space.zread(addr + OFFSET_DB[pool_size]['FVEK2'], 32) 201 | 202 | if ord(cid[1]) == 0x80 and ord(cid[0]) <= 0x03: 203 | if ord(cid[0])==0x02 or ord(cid[0])==0x00: 204 | length = 16 205 | else: 206 | length=32 207 | fvek = fvek1 + fvek2 208 | mode = '{:02x}'.format(ord(cid[0])) 209 | yield pool, BLMode[mode], fvek2[0:length] if mode!="02" and mode!="03" else "Not Applicable", fvek1[0:length], [binascii.unhexlify(mode) + binascii.unhexlify("80") + fvek] 210 | 211 | def unified_output(self, data): 212 | return TreeGrid([("Address", Address), 213 | ("Cipher", str), 214 | ("FVEK", str), 215 | ("TWEAK Key", str), 216 | ], self.generator(data)) 217 | 218 | def generator(self, data): 219 | for (pool, BLMode, tweak, fvek_raw, fbis) in data: 220 | fvek = [] 221 | for o, h, c in utils.Hexdump(fvek_raw): 222 | fvek.append(h) 223 | yield ( 224 | 0, [Address(pool), BLMode, str(''.join(fvek).replace(" ", "")), str(''.join(tweak).replace(" ", "")), ]) 225 | 226 | def render_text(self, outfd, data): 227 | for (pool, BLMode, tweak_raw, fvek_raw, fbis) in data: 228 | fvek = [] 229 | for o, h, c in utils.Hexdump(fvek_raw): 230 | fvek.append(h) 231 | if tweak_raw != "Not Applicable": 232 | tweak = [] 233 | for o, h, c in utils.Hexdump(tweak_raw): 234 | tweak.append(h) 235 | else: 236 | tweak = tweak_raw 237 | if tweak != "Not Applicable": 238 | outfd.write("\n" + 239 | "[FVEK] Address : " + '{0:#010x}'.format(pool.obj_offset) + "\n" + 240 | "[FVEK] Cipher : " + BLMode + "\n" + 241 | "[FVEK] FVEK : " + ''.join(fvek).replace(" ", "") + "\n" + 242 | "[FVEK] Tweak : " + ''.join(tweak).replace(" ", "") + "\n") 243 | else: 244 | outfd.write("\n" + 245 | "[FVEK] Address : " + '{0:#010x}'.format(pool.obj_offset) + "\n" + 246 | "[FVEK] Cipher : " + BLMode + "\n" + 247 | "[FVEK] FVEK: " + ''.join(fvek).replace(" ", "") + "\n") 248 | if self._config.DUMP_DIR: 249 | full_path = os.path.join(self._config.DUMP_DIR, '{0:#010x}.fvek'.format(pool.obj_offset)) 250 | with open(full_path, "wb") as fvek_file: 251 | if tweak == "Not Applicable": 252 | fvek_file.write(''.join(fvek).replace(" ", "") + "\n") 253 | else: 254 | fvek_file.write(''.join(fvek).replace(" ", "") + ":" + ''.join(tweak).replace(" ", "") +"\n") 255 | outfd.write('[DUMP] FVEK dumped to file: {}\n'.format(full_path)) 256 | if self._config.DISLOCKER: 257 | full_path_dislocker = os.path.join(self._config.DISLOCKER, 258 | '{0:#010x}-Dislocker.fvek'.format(pool.obj_offset)) 259 | with open(full_path_dislocker, "wb") as fvek_file_dis: 260 | if fbis != []: 261 | fvek_file_dis.write(fbis[0]) 262 | outfd.write('[DISL] FVEK for Dislocker dumped to file: {}\n'.format(full_path_dislocker)) 263 | if self._config.DISLOCKER or self._config.DUMP_DIR: 264 | print('\n') 265 | --------------------------------------------------------------------------------