├── 3_decode_vmk ├── __init__.py ├── bitlocker │ ├── __init__.py │ ├── structure │ │ ├── entries │ │ │ ├── validation.py │ │ │ ├── __init__.py │ │ │ ├── unicode_string.py │ │ │ ├── key.py │ │ │ ├── aes_ccm_encrypted_key.py │ │ │ └── vmk.py │ │ ├── header.py │ │ ├── constant.py │ │ ├── block.py │ │ └── entry.py │ ├── utils.py │ └── tpm_and_pin.py ├── Pipfile ├── info.py └── decode_tpm_data.py ├── docs ├── extract_kp.png ├── bitlocker_info.png ├── bitlocker_key.png ├── bitlocker_vmk.png └── spi_capture_export_options.png ├── .gitmodules ├── 2_extract_KP ├── Pipfile ├── extract_kp.py └── transaction_tpm.py ├── README.md └── .gitignore /3_decode_vmk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/__init__.py: -------------------------------------------------------------------------------- 1 | from .tpm_and_pin import BitLockerTPMwithPIN -------------------------------------------------------------------------------- /docs/extract_kp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/post-cyberlabs/VMK-extractor-for-bitlocker-with-tpm-and-pin/HEAD/docs/extract_kp.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "1_extract_KP/tpmstream"] 2 | path = 2_extract_KP/tpmstream 3 | url = https://github.com/joholl/tpmstream.git 4 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entries/validation.py: -------------------------------------------------------------------------------- 1 | class Validation: 2 | def __repr__(self) -> str: 3 | return "Validation" -------------------------------------------------------------------------------- /docs/bitlocker_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/post-cyberlabs/VMK-extractor-for-bitlocker-with-tpm-and-pin/HEAD/docs/bitlocker_info.png -------------------------------------------------------------------------------- /docs/bitlocker_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/post-cyberlabs/VMK-extractor-for-bitlocker-with-tpm-and-pin/HEAD/docs/bitlocker_key.png -------------------------------------------------------------------------------- /docs/bitlocker_vmk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/post-cyberlabs/VMK-extractor-for-bitlocker-with-tpm-and-pin/HEAD/docs/bitlocker_vmk.png -------------------------------------------------------------------------------- /docs/spi_capture_export_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/post-cyberlabs/VMK-extractor-for-bitlocker-with-tpm-and-pin/HEAD/docs/spi_capture_export_options.png -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entries/__init__.py: -------------------------------------------------------------------------------- 1 | from .validation import Validation 2 | from .unicode_string import UnicodeString 3 | from .vmk import VolumeMasterKey 4 | from .aes_ccm_encrypted_key import AesCcmEncryptedKey 5 | from .key import Key -------------------------------------------------------------------------------- /3_decode_vmk/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "*" 8 | pycryptodome = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.11" 14 | -------------------------------------------------------------------------------- /2_extract_KP/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | tpmstream = {path = "./tpmstream"} 8 | click = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.11" 14 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entries/unicode_string.py: -------------------------------------------------------------------------------- 1 | class UnicodeString: 2 | def __init__(self, data, label='UNK') -> None: 3 | self.label = label 4 | self.name = data.decode('UTF-16LE').rstrip('\x00') 5 | def __repr__(self) -> str: 6 | return f"""Label: {self.label} 7 | - Unicode string: {self.name}""" -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entries/key.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | from binascii import hexlify 3 | from datetime import datetime 4 | from ..constant import EncryptionMethod 5 | 6 | class Key: 7 | encryption_method : EncryptionMethod = None 8 | def __init__(self, data) -> None: 9 | self.encryption_method = EncryptionMethod(unpack(" str: 12 | return f"""Key 13 | - Encryption method: {self.encryption_method.name} 14 | - Key data: {hexlify(self.key_data).decode('utf-8')}""" -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entries/aes_ccm_encrypted_key.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | from binascii import hexlify 3 | from datetime import datetime 4 | 5 | class AesCcmEncryptedKey: 6 | def __init__(self, data) -> None: 7 | self.nonce = data[:12] 8 | self.nonce_time = datetime.fromtimestamp(unpack(" str: 12 | return f"""AesCcmEncryptedKey 13 | - Nonce: {hexlify(self.nonce).decode('utf-8')} 14 | - Nonce counter: {hex(self.nonce_counter)} 15 | - Nonce time: {self.nonce_time} 16 | - Encrypted data: {hexlify(self.encrypted_data).decode('utf-8')}""" -------------------------------------------------------------------------------- /3_decode_vmk/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import click 3 | from bitlocker.utils import * 4 | from bitlocker.structure.header import FveHeader 5 | 6 | CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} 7 | 8 | @click.command(context_settings=CONTEXT_SETTINGS) 9 | @click.option('--volume', prompt='Targeted volume', help='Target bitlocker volume') 10 | @click.option('--block-id', help='Block id (must be between 1 and 3)', default=1) 11 | def main(volume, block_id): 12 | if not is_bitlocker_volume(volume): 13 | raise TypeError(f"volume {volume} is not in NTFS") 14 | else: 15 | click.echo(f"{volume} is a bitlocker volume") 16 | 17 | res = get_fve_block_offset(volume) 18 | click.echo("Block offset: %s" % ", ".join([hex(i) for i in res])) 19 | 20 | header = FveHeader.load_from_volume(volume, block_id-1) 21 | click.echo(header.block) 22 | 23 | if __name__ == "__main__": 24 | main() -------------------------------------------------------------------------------- /2_extract_KP/extract_kp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import click 3 | from binascii import hexlify 4 | from transaction_tpm import TPMTransactionPulseView 5 | from tpmstream.spec.structures.constants import TPM_CC 6 | 7 | @click.command() 8 | @click.argument('csv_file', type=click.Path(exists=True, dir_okay=False)) 9 | def main(csv_file): 10 | """Extract TPM transaction data of CSV file that are come from PulseView SPI decoder 11 | """ 12 | transaction_pulseview_output = TPMTransactionPulseView(csv_file) 13 | 14 | for transaction in transaction_pulseview_output.transactions: 15 | if transaction.command.commandCode == TPM_CC.Unseal: 16 | break 17 | click.echo(transaction) 18 | click.echo("Encrypted KP from TPM: ", nl=False) 19 | click.secho(hexlify(bytes(transaction.response.parameters.outData.buffer)).decode('utf-8'), bold=True, reverse=True) 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/utils.py: -------------------------------------------------------------------------------- 1 | from struct import pack,unpack 2 | 3 | # From https://www.oreilly.com/library/view/python-cookbook/0596001673/ch03s12.html 4 | def reindent(s: str, numSpaces): 5 | s = s.split('\n') 6 | s = [(numSpaces * ' ') + line for line in s] 7 | s = '\n'.join(s) 8 | return s 9 | 10 | def is_bitlocker_volume(volume): 11 | with open(volume, 'rb') as fp: 12 | fp.seek(3) 13 | oemID = fp.read(8) 14 | if oemID == b'-FVE-FS-': 15 | return True 16 | return False 17 | 18 | def read_fve_block(): 19 | """ 20 | FVE_BLOCK_HEADER block_header = { 0 }; 21 | FVE_HEADER header = { 0 }; 22 | std::vector>> entries; 23 | """ 24 | pass 25 | 26 | def get_fve_block_offset(volume): 27 | with open(volume, 'rb') as fp: 28 | fp.seek(0xb0) 29 | block1 = unpack(" None: 15 | self.key_guid = uuid.UUID(bytes_le=data[:16]) 16 | self.last_modification = datetime.fromtimestamp(unpack(" str: 20 | repr = f"""Volume Master Key: 21 | - Key identifier: {self.key_guid} 22 | - Last modification: {self.last_modification} 23 | - Protection type: 0x{self.protection_type.value:04X} {self.protection_type.name} 24 | - Properties (total: {len(self.properties)})""" 25 | for property in self.properties: 26 | repr += '\n' 27 | repr += reindent(str(property), 8) 28 | return repr -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/header.py: -------------------------------------------------------------------------------- 1 | """ 2 | FVE_BLOCK_HEADER block_header = { 0 }; 3 | FVE_HEADER header = { 0 }; 4 | std::vector>> entries; 5 | 6 | typedef struct _FVE_BLOCK_HEADER 7 | { 8 | CHAR signature[8]; 8 9 | WORD size; 2 10 | WORD version; 2 11 | WORD curr_state; 2 12 | WORD next_state; 2 13 | DWORD64 encrypted_volume_size; 8 14 | DWORD convert_size; 4 15 | DWORD nb_sectors; 4 16 | DWORD64 block_header_offsets[3]; 8*3 = 24 17 | DWORD64 backup_sector_offset; 8 18 | } FVE_BLOCK_HEADER, * PFVE_BLOCK_HEADER; 19 | 20 | typedef struct _FVE_HEADER 21 | { 22 | DWORD size; 23 | DWORD version; 24 | DWORD header_size; 25 | DWORD copy_size; 26 | GUID volume_guid; 27 | DWORD next_counter; 28 | WORD algorithm; 29 | WORD algorithm_unused; 30 | FILETIME timestamp; 31 | } FVE_HEADER, * PFVE_HEADER; 32 | 33 | typedef struct _FVE_ENTRY 34 | { 35 | WORD size; 36 | WORD entry_type; 37 | WORD value_type; 38 | WORD version; 39 | CHAR data[1]; 40 | } FVE_ENTRY, * PFVE_ENTRY; 41 | 42 | """ 43 | from struct import unpack 44 | from typing import List 45 | 46 | from ..utils import * 47 | from .block import FveBlock 48 | 49 | class FveHeader: 50 | block: FveBlock = None 51 | 52 | def __init__(self, block): 53 | self.block = block 54 | 55 | @classmethod 56 | def load_from_volume(cls, volume, block_id=0): 57 | assert(block_id>=0 and block_id < 3) 58 | 59 | block_offset = get_fve_block_offset(volume)[block_id] 60 | 61 | block = FveBlock.load_from_volume(volume, block_offset) 62 | 63 | return cls(block) 64 | 65 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class EntryType(Enum): 4 | """ 5 | """ 6 | PROPERTY = 0x0000 7 | UNK1 = 0x0001 8 | VMK = 0x0002 9 | FKEV = 0x0003 10 | VALIDATION = 0x0004 11 | UNK5 = 0x0005 12 | STARTUP_KEY = 0x0006 13 | DRIVE_LABEL = 0x0007 14 | UNK8 = 0x0008 15 | UNK9 = 0x0009 16 | UNKA = 0x000a 17 | AUTO_UNLOCK = 0x000b 18 | UNKC = 0x000c 19 | UNKD = 0x000d 20 | UNKE = 0x000e 21 | VOLUME_HEADER_BLOCK = 0x000f 22 | UNK10 = 0x0010 23 | UNK11 = 0x0011 24 | 25 | class EntryValueType(Enum): 26 | """ 27 | """ 28 | ERASED = 0x0000 29 | KEY = 0x0001 30 | UNICODE_STRING = 0x0002 31 | STRETCH_KEY = 0x0003 32 | USE_KEY = 0x0004 33 | AES_CCM_ENCRYPTED_KEY = 0x0005 34 | TPM_ENCODED_KEY = 0x0006 35 | VALIDATION = 0x0007 36 | VOLUME_MASTER_KEY = 0x0008 37 | EXTERNAL_KEY = 0x0009 38 | UPDATE = 0x000a 39 | ERROR = 0x000b 40 | ASYMMETRIC_ENCRYPTION = 0x000c 41 | EXPORTED_KEY = 0x000d 42 | PUBLIC_KEY = 0x000e 43 | OFFSET_AND_SIZE = 0x000f 44 | UNK10 = 0x0010 45 | UNK11 = 0x0011 46 | CONCAT_HASH_KEY = 0x0012 47 | UNK13 = 0x0013 48 | UNK14 = 0x0014 49 | RECOVERY_BACKUP = 0x0015 50 | 51 | class KeyProtectionType(Enum): 52 | UNPROTECTED = 0x0000 53 | TPM = 0x0100 54 | STARTUP_KEY = 0x0200 55 | TPM_WITH_PIN = 0x0500 56 | RECOVERY_KEY = 0x0800 57 | PASSWORD = 0x2000 58 | 59 | class EncryptionMethod(Enum): 60 | EXTERNAL_KEY = 0x0000 61 | STRETCH_KEY_UNK1000 = 0x1000 62 | STRETCH_KEY_UNK1001 = 0x1001 63 | AES_CCM_256_UNK2000 = 0x2000 64 | AES_CCM_256_UNK2001 = 0x2001 65 | AES_CCM_256_UNK2002 = 0x2002 66 | AES_CCM_256_UNK2003 = 0x2003 67 | AES_CCM_256_UNK2004 = 0x2004 68 | AES_CCM_256_UNK2005 = 0x2005 69 | AES_CBC_128_ELEPHANT = 0x8000 70 | AES_CBC_256_ELEPHANT = 0x8001 71 | AES_CBC_128 = 0x8002 72 | AES_CBC_256 = 0x8003 73 | AES_XTS_128 = 0x8004 74 | AES_XTS_256 = 0x8005 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMK extractor for bitlocker with tpm and pin 2 | 3 | **Article**: https://post-cyberlabs.github.io/Offensive-security-publications/posts/2024_09_tpmandpin/ 4 | 5 | # Overview 6 | 7 | Technic to extract VMK from bitlocker volume that are protected by TPM are already documented in different publication. 8 | 9 | The purpose of this GitHub repo is giving toolset to extract VMK in the case of **TPMAndPIN** that is different from **TPM** only case 10 | 11 | The usage of **TPMAndPIN** protector rely on TPM to release the encrypted *Key Protector*(aka *KP*) and the PIN code to decrypt *KP*. 12 | Then using the *KP*, the *VMK* can be decrypted and the BitLocker volume can be mount. 13 | 14 | ![Schema of decrypting process](docs/bitlocker_key.png) 15 | 16 | One practical use of this tool is the Local Privilege Escalation as a valid PIN code is required. 17 | 18 | # Requirement 19 | 20 | * Laptop/desktop volume protected by BitLocker using TPMAndPin protector 21 | * TPM configured to use the motherboard TPM (dTPM) 22 | * Knownledge of PIN code 23 | * TPM capture done with [DSview](https://github.com/DreamSourceLab/DSView) a fork of [PulseView](https://sigrok.org/wiki/PulseView) to be compatible with DreamSource logic analyzer. Not tested on PulseView. 24 | 25 | # Usage 26 | 27 | ## Global Steps 28 | 29 | 1. Capture dTPM traffic 30 | 2. Extract encrypted KP from sniffed TPM data 31 | 3. Decrypt KP and decode VMK using pin code and decrypted KP 32 | 33 | ### 1 - Capture TPM traffic 34 | 35 | 1. Connect logic analyzer probes to motherboard TPM. 36 | The TPM pinout can be found in its datasheet 37 | 2. Configure decode the two decode: 38 | * SPI 39 | * TPM SPI transaction 40 | 3. Capture TPM traffic at boot time 41 | 4. Export the collected data in CSV format with only the column *SPI TPM: TPM transactions* 42 | 43 | ![SPI export option](docs/spi_capture_export_options.png) 44 | 45 | ### 2 - Extract encrypted KP from sniffed TPM data 46 | 47 | 1. Install script dependancy 48 | * **click** 49 | * **tpmstream** 50 | ```bash 51 | git submodule update --init 52 | pipenv --site-packages install 53 | ``` 54 | 55 | 2. Use the script `extract_kp.py` to extract encrypted KP from CSV data: 56 | 57 | ``` ./extract_kp.py decoder--240531-140324.csv``` 58 | 59 | ![Extracting KP](docs/extract_kp.png) 60 | 61 | 62 | ### 3 - Decrypt KP and decode VMK using pin code and decrypted KP 63 | 64 | 1. Install script dependancy 65 | * **click** 66 | * **pycryptodome** 67 | 68 | ```bash 69 | pipenv --site-packages install 70 | ``` 71 | 72 | 1. Mount the target disk on another machine with linux as OS (Debian, Kali, etc.) 73 | 2. Locate the BitLocker partition (during our test, the partition is the third one). A way to determine, if the partition is a BitLocker volume, is using the script `info.py` available in folder `3_decode_vmk`. This script permits to list the BitLocker metadata. 74 | 75 | ![information about BitLocker volume](docs/bitlocker_info.png) 76 | 77 | 3. With the extracted TPM data and the PIN code, use the script `decode_tpm_data.py` in the folder `3_decode_vmk`. 78 | 79 | ![Decode VMK](docs/bitlocker_vmk.png) 80 | -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/block.py: -------------------------------------------------------------------------------- 1 | """ 2 | typedef struct _FVE_HEADER 3 | { 4 | DWORD size; 5 | DWORD version; 6 | DWORD header_size; 7 | DWORD copy_size; 8 | GUID volume_guid; 9 | DWORD next_counter; 10 | WORD algorithm; 11 | WORD algorithm_unused; 12 | FILETIME timestamp; 13 | } FVE_HEADER, * PFVE_HEADER; 14 | """ 15 | import uuid 16 | from typing import List 17 | from datetime import datetime 18 | from struct import pack,unpack 19 | from binascii import hexlify, unhexlify 20 | 21 | from .entry import FveEntry 22 | from ..utils import reindent 23 | 24 | class FveBlock: 25 | size: int = None 26 | version: int = None 27 | header_size: int = None 28 | copy_size: int = None 29 | volume_guid: uuid.UUID = None 30 | next_counter: int = None 31 | algorithm: int = None 32 | algorithm_unused: int = None 33 | timestamp: datetime = None 34 | entries: List[FveEntry] = None 35 | object_offset: int = 0 36 | 37 | def __init__(self, size, version, header_size, copy_size, volume_guid, next_counter, algorithm, algorithm_unused, timestamp, entries, object_offset=0): 38 | self.size = size 39 | self.version = version 40 | self.header_size = header_size 41 | self.copy_size = copy_size 42 | self.volume_guid = volume_guid 43 | self.next_counter = next_counter 44 | self.algorithm = algorithm 45 | self.algorithm_unused = algorithm_unused 46 | self.timestamp = timestamp 47 | self.object_offset = object_offset 48 | self.entries = entries 49 | 50 | def __repr__(self): 51 | repr = f"""FveBlock(0x{self.object_offset:04X}): 52 | - Size: {self.size} 53 | - Version: {self.version} 54 | - Header size: {self.header_size} 55 | - Copy size: {self.copy_size} 56 | - Volume guid: {self.volume_guid} 57 | - Next counter: 0x{self.next_counter:X} 58 | - Algorithm: 0x{self.algorithm:X} 59 | - Timestamp: {self.timestamp} 60 | - Nb entries: {len(self.entries)}""" 61 | repr += "\n Entries:" 62 | for entry in self.entries: 63 | repr += '\n' 64 | repr += reindent(str(entry), 8) 65 | return repr 66 | 67 | 68 | @classmethod 69 | def load_from_volume(cls, volume, block_offset): 70 | offset = block_offset + 0x40 71 | with open(volume, 'rb') as fp: 72 | fp.seek(offset) 73 | size = unpack(" VolumeMasterKey: 18 | for entry in self.block.entries: 19 | if entry.entry_type is not EntryType.VMK or entry.value_type is not EntryValueType.VOLUME_MASTER_KEY: 20 | continue 21 | vmk: VolumeMasterKey = entry.loaded_data 22 | if vmk.protection_type is KeyProtectionType.TPM_WITH_PIN: 23 | return vmk 24 | raise ValueError("Unable to find VMK for TPM with PIN :/") 25 | 26 | def get_salt_for_key1(self) -> bytes: 27 | result = None 28 | vmk = self.get_vmk_for_TPM_with_PIN() 29 | for property in vmk.properties: 30 | if property.entry_type is EntryType.PROPERTY and property.value_type is EntryValueType.STRETCH_KEY and property.size == 0X6C: 31 | return property.data[4:20] 32 | raise ValueError("Unable to find salt for key1") 33 | 34 | def generate_key1(self, pin: str): 35 | """Generate KEY 1 36 | """ 37 | data = bytearray(b'\x00' * 0x20) 38 | 39 | data += bytearray(sha256(sha256(pin.encode('UTF-16LE')).digest()).digest()) 40 | data += bytearray(self.get_salt_for_key1()) 41 | 42 | data += bytearray(b'\x00' * 0x8) 43 | 44 | assert len(data) == 0x58 45 | 46 | for idx in range(0x100000): 47 | data = bytearray(sha256(bytes(data)).digest()) + data[0x20:] 48 | for idx in range(8): 49 | new = data[0x50 + idx] + 1 50 | if new > 0xFF: 51 | data[0x50 + idx] = 0 52 | else: 53 | data[0x50 + idx] = new 54 | break 55 | return bytes(data[:0x20]) 56 | 57 | def decode_key_protector_container(self, key, encoded_KP_from_tpm): 58 | encoded_data = FveEntry.load_from_data(encoded_KP_from_tpm) 59 | cipher = AES.new(key, AES.MODE_CCM, nonce=encoded_data.loaded_data.nonce) 60 | return cipher.decrypt(encoded_data.loaded_data.encrypted_data) 61 | 62 | def get_encrypted_VMK(self) -> bytes: 63 | encrypted_data : FveEntry = None 64 | vmk = self.get_vmk_for_TPM_with_PIN() 65 | for idx in range(len(vmk.properties)): 66 | property = vmk.properties[idx] 67 | if property.entry_type is EntryType.PROPERTY and property.value_type is EntryValueType.UNK13: 68 | encrypted_data = vmk.properties[idx+1] 69 | return encrypted_data.raw 70 | 71 | def get_encrypted_FVEK(self): 72 | for entry in self.block.entries: 73 | if entry.entry_type is EntryType.FKEV or entry.value_type is EntryValueType.AES_CCM_ENCRYPTED_KEY: 74 | return entry.raw 75 | raise ValueError("Unable to find encrypted FVEK :/") 76 | 77 | def get_encrypted_autounlock(self): 78 | for entry in self.block.entries: 79 | if entry.entry_type is EntryType.AUTO_UNLOCK or entry.value_type is EntryValueType.AES_CCM_ENCRYPTED_KEY: 80 | return entry.raw 81 | raise ValueError("Unable to find encrypted Auto unlock :/") -------------------------------------------------------------------------------- /3_decode_vmk/bitlocker/structure/entry.py: -------------------------------------------------------------------------------- 1 | """ 2 | typedef struct _FVE_ENTRY 3 | { 4 | WORD size; 5 | WORD entry_type; 6 | WORD value_type; 7 | WORD version; 8 | CHAR data[1]; 9 | } FVE_ENTRY, * PFVE_ENTRY; 10 | """ 11 | from struct import unpack 12 | from binascii import hexlify 13 | from ..utils import reindent 14 | from .constant import EntryType,EntryValueType 15 | from . import entries 16 | 17 | class FveEntry: 18 | size: int = None 19 | entry_type: EntryType = None 20 | value_type: EntryValueType = None 21 | version: int = None 22 | data: bytes = None 23 | raw: bytes = None 24 | loaded_data: object = None 25 | 26 | def __init__(self, size, entry_type, value_type, version, data, raw=None): 27 | self.size = size 28 | self.entry_type = EntryType(entry_type) 29 | self.value_type = EntryValueType(value_type) 30 | self.version = version 31 | self.data = data 32 | self.raw = raw 33 | self._load_data() 34 | 35 | def _load_data(self): 36 | if self.entry_type is EntryType.VALIDATION and self.value_type is EntryValueType.VALIDATION: 37 | self.loaded_data = entries.Validation() 38 | elif self.entry_type is EntryType.DRIVE_LABEL and self.value_type is EntryValueType.UNICODE_STRING: 39 | self.loaded_data = entries.UnicodeString(self.data, 'Drive label') 40 | elif self.entry_type is EntryType.PROPERTY and self.value_type is EntryValueType.UNICODE_STRING: 41 | self.loaded_data = entries.UnicodeString(self.data) 42 | elif self.value_type is EntryValueType.AES_CCM_ENCRYPTED_KEY: 43 | self.loaded_data = entries.AesCcmEncryptedKey(self.data) 44 | elif self.value_type is EntryValueType.KEY: 45 | self.loaded_data = entries.Key(self.data) 46 | elif self.entry_type is EntryType.VMK and self.value_type is EntryValueType.VOLUME_MASTER_KEY: 47 | self.loaded_data = entries.VolumeMasterKey(self.data) 48 | current_offset = 28 49 | while current_offset < len(self.data): 50 | entry = self.load_from_data(self.data[current_offset:]) 51 | self.loaded_data.properties.append(entry) 52 | current_offset += entry.size 53 | 54 | def __repr__(self): 55 | repr = f"""FveEntry: 56 | - size: 0x{self.size:08X} 57 | - entry type: 0x{self.entry_type.value:02X} {self.entry_type.name} 58 | - value type: 0x{self.value_type.value:02X} {self.value_type.name} 59 | - version: {self.version}""" 60 | if self.loaded_data is not None: 61 | repr += '\n' 62 | repr += ' ' * 4 63 | repr += '- Loaded data:' 64 | repr += '\n' 65 | repr += reindent(str(self.loaded_data), 8) 66 | else: 67 | repr += '\n' 68 | repr += ' ' * 4 69 | repr += f"- Raw data: {hexlify(self.data).decode('utf-8')}" 70 | return repr 71 | 72 | @classmethod 73 | def load_from_volume(cls, volume, offset): 74 | with open(volume, 'rb') as fp: 75 | fp.seek(offset) 76 | size = unpack(" vmk.bin") 49 | if output is not None: 50 | click.echo("") 51 | with open(output, 'wb') as fp: 52 | fp.write(volume_master_key_container.loaded_data.key_data) 53 | click.echo(f"[+] VMK saved into the file: {output}") 54 | click.echo("") 55 | click.secho("Mount with dislocker:", underline=True) 56 | click.echo(f"""sudo mkdir /mnt/dislocker_tmp 57 | sudo mkdir /mnt/dislocker_tmp2 58 | sudo dislocker -K vmk.bin {volume} /mnt/dislocker_tmp 59 | sudo mount -t ntfs-3g -o remove_hiberfile,recover /mnt/dislocker_tmp/dislocker-file /mnt/dislocker_tmp2 60 | """) 61 | 62 | # encrypted_fvek_container_raw = bde.get_encrypted_FVEK() 63 | # print("[+] Encrypted FVEK container: %s" % hexlify(encrypted_fvek_container_raw).decode('utf-8')) 64 | # print(FveEntry.load_from_data(encrypted_fvek_container_raw)) 65 | 66 | # full_volume_encryption_key_container_raw = bde.decode_key_protector_container(volume_master_key_container.loaded_data.key_data, encrypted_fvek_container_raw) 67 | # print("[+] FVEK container: %s" % hexlify(full_volume_encryption_key_container_raw).decode('utf-8')) 68 | # full_volume_encryption_key_container = FveEntry.load_from_data(full_volume_encryption_key_container_raw) 69 | # print(full_volume_encryption_key_container) 70 | 71 | if __name__ == "__main__": 72 | decode_tpm_data() 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | -------------------------------------------------------------------------------- /2_extract_KP/transaction_tpm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import csv 3 | import binascii 4 | import click 5 | from binascii import hexlify, unhexlify 6 | from typing import List 7 | from tpmstream.io.binary import Binary 8 | from tpmstream.io.pretty import Pretty 9 | from tpmstream.spec.commands import Command, Response 10 | from tpmstream.common.object import events_to_obj,events_to_objs 11 | 12 | class Transaction: 13 | command_raw : bytes = None 14 | command : Command = None 15 | response_raw : bytes = None 16 | response : Response = None 17 | def __init__(self, command_raw : bytes, response_raw : bytes) -> None: 18 | self.command_raw = command_raw 19 | self.response_raw = response_raw 20 | 21 | events = Binary.marshal(tpm_type=Command, buffer=command_raw) 22 | self.command = events_to_obj(events) 23 | 24 | events = Binary.marshal(tpm_type=Response, buffer=response_raw, command_code=self.command.commandCode) 25 | self.response = events_to_obj(events,command_code=self.command.commandCode) 26 | def __repr__(self) -> str: 27 | return f"""Transaction: 28 | CommandCode : {self.command.commandCode} 29 | Command raw : {hexlify(bytes(self.command_raw)).decode('utf-8')} 30 | Response raw : {hexlify(bytes(self.response_raw)).decode('utf-8')} 31 | """ 32 | 33 | class TPMTransactionPulseView: 34 | transactions : List[Transaction] = None 35 | def __init__(self, csv_file): 36 | with open(csv_file) as csvfile: 37 | reader = csv.reader(csvfile) 38 | self.data = list(reader) 39 | self._load_message() 40 | 41 | def get_read_fifo_at_index(self, index): 42 | if index + 2 > len(self.data): 43 | return None 44 | if self.data[index][2] != "Read": 45 | return None 46 | if self.data[index + 1][2] != "Register: TPM_DATA_FIFO_0": 47 | return None 48 | return int(self.data[index + 2][2], 16) 49 | 50 | def get_write_fifo_at_index(self, index): 51 | if index + 2 > len(self.data): 52 | return None 53 | if self.data[index][2] != "Write": 54 | return None 55 | if self.data[index + 1][2] != "Register: TPM_DATA_FIFO_0": 56 | return None 57 | return int(self.data[index + 2][2], 16) 58 | 59 | @staticmethod 60 | def gather_message(raw_extraction_message): 61 | messages = [] 62 | tmp_buffer = [] 63 | for msg in raw_extraction_message: 64 | if msg[0] == 0x80 and msg[1] in [0x01, 0x02]: 65 | if len(tmp_buffer) > 0: 66 | messages.append(tmp_buffer) 67 | tmp_buffer = [] 68 | tmp_buffer.extend(msg) 69 | if tmp_buffer != b'': 70 | messages.append(tmp_buffer) 71 | return messages 72 | 73 | def _load_message(self): 74 | ungrouped_messages_read_fifo = [] 75 | 76 | idx = 0 77 | while idx < len(self.data): 78 | if self.get_read_fifo_at_index(idx) is not None: 79 | buffer = [] 80 | fifo_loop = True 81 | while fifo_loop: 82 | value = self.get_read_fifo_at_index(idx) 83 | if value is None: 84 | fifo_loop = False 85 | #print("Debug Buffer FIFO: %s" % binascii.hexlify(bytes(buffer)).decode('utf-8')) 86 | ungrouped_messages_read_fifo.append(buffer) 87 | else: 88 | buffer.append(value) 89 | idx += 3 90 | idx += 1 91 | 92 | ungrouped_messages_write_fifo = [] 93 | 94 | idx = 0 95 | while idx < len(self.data): 96 | if self.get_write_fifo_at_index(idx) is not None: 97 | buffer = [] 98 | fifo_loop = True 99 | while fifo_loop: 100 | value = self.get_write_fifo_at_index(idx) 101 | if value is None: 102 | fifo_loop = False 103 | #print("Debug Buffer FIFO: %s" % binascii.hexlify(bytes(buffer)).decode('utf-8')) 104 | ungrouped_messages_write_fifo.append(buffer) 105 | else: 106 | buffer.append(value) 107 | idx += 3 108 | idx += 1 109 | 110 | 111 | self.messages_read_fifo = self.gather_message(ungrouped_messages_read_fifo) 112 | self.messages_write_fifo = self.gather_message(ungrouped_messages_write_fifo) 113 | 114 | self.transactions = [] 115 | for idx in range(len(self.messages_read_fifo)): 116 | self.transactions.append(Transaction(self.messages_write_fifo[idx], self.messages_read_fifo[idx])) 117 | 118 | def __repr__(self) -> str: 119 | output = [] 120 | for transac in self.transactions: 121 | output.append(repr(transac)) 122 | return "\n".join(output) 123 | 124 | @click.command() 125 | @click.argument('csv_file', type=click.Path(exists=True, dir_okay=False)) 126 | def main(csv_file): 127 | """Extract TPM transaction data of CSV file that are come from PulseView SPI decoder 128 | """ 129 | transaction = TPMTransactionPulseView(csv_file) 130 | print(transaction) 131 | 132 | if __name__ == "__main__": 133 | main() 134 | --------------------------------------------------------------------------------