Feature: BitLocker FVEK extraction plugin#1950
Feature: BitLocker FVEK extraction plugin#1950forensicxlab wants to merge 3 commits intovolatilityfoundation:developfrom
Conversation
ikelos
left a comment
There was a problem hiding this comment.
Thanks, this is pretty nice, but there's two major points that stick out to me. Firstly it writes files directly, which is bad if the plugin's run via a web UI or some other mechanism that doesn't have local access, so plugins should always write out file using the self.open mechanism, rather than just plain open. They should be similar (although not identical, so please test it) and that will allow web UIs to provide the files back to users if they wish.
The second thing is there's a lot of manual image reading and processing of raw bytes, which sometimes is an indication of things that should really be done by volatility but aren't for some reason. A lot of hardcoded specific values is also generally bad (in case they change in the future, figuring out where those numbers came from or why they were chosen, etc). If they're structure offsets, ideally the structure would be defined in a local JSON symbol table that would be loaded up when this plugin starts (see windows.getsids as an example). Otherwise comments referencing where those values came from would be useful. Otherwise it's all a bit black magic and difficult for people to audit/verify...
| class Bitlocker(interfaces.plugins.PluginInterface): | ||
| """Extracts BitLocker FVEK keys from Windows memory.""" | ||
|
|
||
| _required_framework_version = (2, 0, 0) |
There was a problem hiding this comment.
Does this definitely work with frameworks right back to 2.0.0? If you're not sure, then this should be the current framework version, so...
| _required_framework_version = (2, 0, 0) | |
| _required_framework_version = (2, 28, 0) |
| os.makedirs(dump_dir, exist_ok=True) | ||
| dump_file = os.path.join(dump_dir, f"{result.offset:#010x}.fvek") |
There was a problem hiding this comment.
| os.makedirs(dump_dir, exist_ok=True) | |
| dump_file = os.path.join(dump_dir, f"{result.offset:#010x}.fvek") | |
| dump_file = f"{result.offset:#010x}.fvek" |
There was a problem hiding this comment.
We shouldn't be writing to a directory since we'll be using self.open...
| text = self._key_hex(result.fvek) | ||
| if result.tweak is not None: | ||
| text = f"{text}:{self._key_hex(result.tweak)}" | ||
| with open(dump_file, "w", encoding="utf-8") as fvek_file: |
There was a problem hiding this comment.
| with open(dump_file, "w", encoding="utf-8") as fvek_file: | |
| with self.open(dump_file, "w", encoding="utf-8") as fvek_file: |
I don't recall whether self.open supports modes or encodings, so worth testing, but we shouldn't be directly opening files in plugins for the reasons listed above (support as a library).
| dislocker_file = os.path.join( | ||
| dislocker_dir, f"{result.offset:#010x}-Dislocker.fvek" | ||
| ) | ||
| with open(dislocker_file, "wb") as dislocker_handle: |
There was a problem hiding this comment.
| with open(dislocker_file, "wb") as dislocker_handle: | |
| with self.open(dislocker_file, "wb") as dislocker_handle: |
| os.makedirs(dislocker_dir, exist_ok=True) | ||
| dislocker_file = os.path.join( | ||
| dislocker_dir, f"{result.offset:#010x}-Dislocker.fvek" | ||
| ) |
There was a problem hiding this comment.
| os.makedirs(dislocker_dir, exist_ok=True) | |
| dislocker_file = os.path.join( | |
| dislocker_dir, f"{result.offset:#010x}-Dislocker.fvek" | |
| ) | |
| dislocker_file = f"{result.offset:#010x}-Dislocker.fvek" |
| name="dump-dir", | ||
| description="Directory in which to dump FVEK values", | ||
| optional=True, | ||
| default=None, |
There was a problem hiding this comment.
| default=None, | |
| action="store_true", |
| description="Windows kernel", | ||
| architectures=["Intel32", "Intel64"], | ||
| ), | ||
| requirements.StringRequirement( |
There was a problem hiding this comment.
We don't open or dump to directories in plugins anymore, since that would break if the plugin were executed through a web API, so this should be producing files using the self.open() mechanism. I've provided suggestions below but they should be tested locally to ensure they work correctly, rather than just applied.
| @staticmethod | ||
| def _version_tuple( | ||
| context: interfaces.context.ContextInterface, symbol_table_name: str | ||
| ) -> Tuple[int, int, int, int]: |
There was a problem hiding this comment.
| ) -> Tuple[int, int, int, int]: | |
| ) -> Tuple[int, int, int, int]: | |
| """Extracts the kernel symbol table's windows version""" |
|
|
||
| def _iter_none_pools( | ||
| self, kernel: interfaces.context.ModuleInterface, is_64bit: bool | ||
| ) -> Iterable[FvekResult]: |
There was a problem hiding this comment.
This could do with a docstring to explain what the function's purpose is, _iter_none_pools isn't immediately obvious.
| layer = self.context.layers[header.vol.layer_name] | ||
| base_offset = header.vol.offset | ||
|
|
||
| f1 = layer.read(base_offset + 0x9C, 64, pad=True) |
There was a problem hiding this comment.
These offsets seem a little brittle? Would it not be better to store them as data, with in a JSON file, or perhaps as a data structure that can be instantiated (and therefore updated based on the version of windows, in case they ever change?).
Bitlocker plugin ported from the Volatility 2 BitLocker key extractor to recover FVEKs from Windows memory pools (Windows 7 through Windows 10/Server generations) and inspired by the latest version in memprocfs.
Example output:
Then works to decrypt the volume:
Note that another bitlocker container was present on the machine. Which was also retrieved.