├── .gitignore ├── README.md ├── amdnvtool ├── __main__.py ├── crypto.py ├── nv_data.py ├── parsed.py └── raw.py ├── example.nvdata ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/.*.swp 3 | **/.DS_Store 4 | **/*.pyc 5 | 6 | **/.idea/ 7 | 8 | *.egg-info/ 9 | **/__pycache__/ 10 | 11 | venv/ 12 | build/ 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amdnvtool 2 | 3 | This tool can parse and decrypt the `NV_DATA` entry. 4 | 5 | ## Prerequisites 6 | 7 | Installing the package in a python venv: 8 | ```bash 9 | $ python3.8 -m venv venv 10 | $ . venv/bin/activate 11 | $ pip install -U pip setuptools_rust 12 | $ pip install -e . 13 | ``` 14 | 15 | ## Example 16 | 17 | Decoding the example nvdata blob: 18 | ```bash 19 | $ amdnvtool example.nvdata 20 | Context 4 21 | [x'84090000', x'fac9aeb5dd2f95b771e75aa7'] 22 | [x'90090000', x'7451d3a7d7f24213d0027f5f'] 23 | [x'e8110000', x'c404bf82d17be872b5a4dc69'] 24 | Context 5 25 | [x'01000000', x'72c649a968d81894c0332cdc'] 26 | ... 27 | ``` 28 | 29 | This example `NV_DATA` blob has been taken from our Ryzen 3600 CPU in an Asrock A520M-HDV motherboard. 30 | The decryption keys are derived from a hardcoded secret in the `amdnvtool/crypto.py` file: 31 | ```python 32 | class SecretKeys: 33 | def __init__(self, secret: bytes): 34 | self.secret = secret 35 | self.wrapping_aes_key = kdf(secret, 'AES Key for wrapping data') 36 | self.wrapping_hmac_key = kdf(secret, 'HMAC Key for wrapping data') 37 | self.signature_hmac_key = kdf(secret, 'HMAC Signature Key for PSP Data saved in DRAM') 38 | 39 | class NvDataKeys(SecretKeys): 40 | def __init__(self, secret, ftpm_key_modulus, ftpm_app_id): 41 | super().__init__(secret) 42 | self.ftpm_key_modulus = ftpm_key_modulus 43 | self.ftpm_key_mod_hash = sha256(ftpm_key_modulus) 44 | self.ftpm_app_id = ftpm_app_id 45 | 46 | self.aes_i_key = hmac_sha256(self.wrapping_aes_key, self.ftpm_key_mod_hash) 47 | self.hmac_i_key = hmac_sha256(self.wrapping_hmac_key, self.ftpm_key_mod_hash) 48 | 49 | self.aes_key = hmac_sha256(self.aes_i_key, self.ftpm_app_id)[:16] 50 | self.hmac_key = hmac_sha256(self.hmac_i_key, self.ftpm_app_id) 51 | 52 | def get_keys(): 53 | 54 | # inputs 55 | secret = ba.a2b_hex('89c209ab1571b23c84b9fef0a1416fbc9482b014cc5fe242a797b72df028556f') 56 | 57 | ftpm_app_id = ba.a2b_hex('00b5a2ab4538ca45bb56f2e5ae71c585') 58 | 59 | ccd7_key_modulus = ba.a2b_hex( 60 | 'e9451471a33663ade48d5d8a4fe587f9' 61 | + 'c6687c89c83a3b8c6d892e610cf5032c' 62 | + '2d9377d5c5639eb820cf1ca5d39aedcb' 63 | + 'aaa3b8313412ecc84699581808090b60' 64 | + '68333d318f56d0271e13696c7ec0d4fe' 65 | + '902e7832125ff1004961a900581c6189' 66 | + '5ac8a52ef05278777ffaec5df49ce88c' 67 | + '7b6bcec897a9ef780d512cd2b490fb55' 68 | + '9cef174e98ad83bb2ad755af371df768' 69 | + '6e058977268d6dbd0f1fbe24d48d057a' 70 | + '9649202ef73eb02005edaa72d267cb99' 71 | + '6a26416e37a70225ddb22593dc7fcb2a' 72 | + '397ae843cb41ec3f7eaebe32fda6fddc' 73 | + 'cf455ca5134b192ef5e03a8ca63b8b66' 74 | + '8a9c87e213654691f5be6ea27f89eae0' 75 | + 'c2871f6c66efc46b979700e1488c39e6' 76 | ) 77 | 78 | return NvDataKeys(secret, ccd7_key_modulus, ftpm_app_id) 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /amdnvtool/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .nv_data import NVData 4 | 5 | 6 | def main(): 7 | if len(sys.argv) < 3: 8 | print(f"Error: more arguments are required") 9 | print(f"usage: amdnvtool romfile [-s | lsb_key_hex]") 10 | sys.exit(1) 11 | 12 | romfile = sys.argv[1] 13 | lsb_key_hex = sys.argv[2] 14 | if lsb_key_hex == '-s': 15 | secret_hex = sys.argv[3] 16 | nv_data = NVData.from_file_and_secret_hex(romfile, secret_hex) 17 | else: 18 | nv_data = NVData.from_file_and_lsb_key_hex(romfile, lsb_key_hex) 19 | 20 | nv_data.raw.assert_all_hmacs_are_valid(nv_data.keys.hmac_key) 21 | 22 | #nv_data.print_parsed() 23 | #nv_data.print_by_context() 24 | nv_data.print_json_by_context() 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /amdnvtool/crypto.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Set 2 | 3 | from cryptography.hazmat.primitives import hashes, hmac, ciphers 4 | from psptool import PSPTool 5 | 6 | import binascii as ba 7 | 8 | T = TypeVar('T') 9 | 10 | 11 | def sole(set_of_one: Set[T], assert_msg="Set does not contain exactly one element") -> T: 12 | assert len(set_of_one) == 1, assert_msg 13 | return list(set_of_one)[0] 14 | 15 | 16 | def byteswap(buffer: bytes) -> bytes: 17 | return buffer[::-1] 18 | 19 | 20 | def hmac_sha256(key: bytes, msg: bytes) -> bytes: 21 | ctx = hmac.HMAC(key, hashes.SHA256()) 22 | ctx.update(msg) 23 | return ctx.finalize() 24 | 25 | 26 | def sha256(msg: bytes) -> bytes: 27 | ctx = hashes.Hash(hashes.SHA256()) 28 | ctx.update(msg) 29 | return ctx.finalize() 30 | 31 | 32 | def aes_ctr(key: bytes, iv: bytes): 33 | return ciphers.Cipher(ciphers.algorithms.AES(key), ciphers.modes.CTR(iv)) 34 | 35 | 36 | def aes_ctr_dec(key: bytes, iv: bytes, txt: bytes) -> bytes: 37 | ctx = aes_ctr(key, iv).decryptor() 38 | return ctx.update(txt) + ctx.finalize() 39 | 40 | 41 | def aes_ctr_enc(key: bytes, iv: bytes, txt: bytes) -> bytes: 42 | ctx = aes_ctr(key, iv).encryptor() 43 | return ctx.update(txt) + ctx.finalize() 44 | 45 | 46 | def kdf(key: bytes, label: str, output_len: int = 32) -> bytes: 47 | output = b'' 48 | suffix = label.encode('ascii') + b'\0' * 5 + (output_len * 8).to_bytes(4, 'little') 49 | 50 | for i in range(1, 1 + ((output_len + 31) >> 5)): 51 | output += hmac_sha256(key, i.to_bytes(4, 'big') + suffix) 52 | 53 | return output[:output_len] 54 | 55 | 56 | class SecretKeys: 57 | def __init__(self, secret: bytes): 58 | self.secret = secret 59 | self.wrapping_aes_key = kdf(secret, 'AES Key for wrapping data') 60 | self.wrapping_hmac_key = kdf(secret, 'HMAC Key for wrapping data') 61 | self.signature_hmac_key = kdf(secret, 'HMAC Signature Key for PSP Data saved in DRAM') 62 | 63 | 64 | def unseal_secret(sealed_secret: bytes, lsb_key: bytes): 65 | ctx = ciphers.Cipher(ciphers.algorithms.AES(lsb_key), ciphers.modes.ECB()).encryptor() 66 | return ctx.update(sealed_secret) + ctx.finalize() 67 | 68 | 69 | class NvDataKeys(SecretKeys): 70 | @staticmethod 71 | def from_file_and_lsb_key_hex(filename: str, lsb_key_hex: str): 72 | pt = PSPTool.from_file(filename) 73 | 74 | # For all portions of the NvDataKeys let's fetch all possible inputs and assert they are the same using sole() 75 | driver_entries = pt.blob.get_entries_by_type(0x28) 76 | 77 | # 1. sealed_secret: 78 | sealed_secrets = set() 79 | for de in driver_entries: 80 | # We suspect the sealed_secret right before this string 81 | sealed_secret_size = 0x20 82 | body = de.get_decrypted_decompressed_body() 83 | offset = body.find(b"HMAC Signature Key for PSP Data saved in DRAM") - sealed_secret_size 84 | sealed_secrets.add(body[offset:offset+sealed_secret_size]) 85 | sealed_secret = sole(sealed_secrets) 86 | lsb_key = ba.unhexlify(lsb_key_hex) 87 | secret = unseal_secret(sealed_secret, lsb_key) 88 | 89 | return NvDataKeys._from_file_and_secret(pt, filename, secret) 90 | 91 | @staticmethod 92 | def from_file_and_secret_hex(filename: str, secret_hex: str): 93 | pt = PSPTool.from_file(filename) 94 | secret = ba.unhexlify(secret_hex) 95 | return NvDataKeys._from_file_and_secret(pt, filename, secret) 96 | 97 | @staticmethod 98 | def _from_file_and_secret(pt, filename: str, secret: bytes): 99 | 100 | psp_boot_time_trustlets = pt.blob.get_entries_by_type(0xc) 101 | 102 | # 2. ftpm_key_modulus 103 | ftpm_key_moduli = set() 104 | for pbtt in psp_boot_time_trustlets: 105 | if pbtt.signed_entity: # Zen 1 PBTTS are legacy_headers that cannot be verified by PSPTol (yet!) 106 | assert len(pbtt.signed_entity.certifying_keys) >= 1 107 | ck = list(pbtt.signed_entity.certifying_keys)[0] 108 | pk = ck.get_public_key() 109 | ftpm_key_moduli.add( 110 | pk.get_crypto_material(pk.signature_size) 111 | ) 112 | ftpm_key_modulus: bytes = sole(ftpm_key_moduli) 113 | 114 | # 3. ftpm_app_id 115 | ftpm_app_ids = set() 116 | for pbtt in psp_boot_time_trustlets: 117 | if pbtt.signed_entity: # Zen 1 PBTTS are legacy_headers that cannot be verified by PSPTol (yet!) 118 | magic = b"gpd.ta.appID" 119 | offset = pbtt.get_decrypted_decompressed_body().find(magic) + len(magic) + 1 120 | ftpm_app_ids.add( 121 | pbtt.get_decrypted_decompressed_body()[offset:offset+0x10] 122 | ) 123 | ftpm_app_id = sole(ftpm_app_ids) 124 | 125 | return NvDataKeys(secret, ftpm_key_modulus, ftpm_app_id) 126 | 127 | def __init__(self, secret: bytes, ftpm_key_modulus: bytes, ftpm_app_id: bytes): 128 | super().__init__(secret) 129 | self.ftpm_key_modulus = ftpm_key_modulus 130 | self.ftpm_key_mod_hash = sha256(ftpm_key_modulus) 131 | self.ftpm_app_id = ftpm_app_id 132 | 133 | self.aes_i_key = hmac_sha256(self.wrapping_aes_key, self.ftpm_key_mod_hash) 134 | self.hmac_i_key = hmac_sha256(self.wrapping_hmac_key, self.ftpm_key_mod_hash) 135 | 136 | self.aes_key = hmac_sha256(self.aes_i_key, self.ftpm_app_id)[:16] 137 | self.hmac_key = hmac_sha256(self.hmac_i_key, self.ftpm_app_id) 138 | 139 | 140 | if __name__ == "__main__": 141 | # controls 142 | wrapping_aes_key_correct = byteswap(ba.a2b_hex('def5ce4e3896777e19e6f09552253bf587bcef53540eb846bc91b69db930c3f7')) 143 | 144 | wrapping_hmac_key_correct = byteswap(ba.a2b_hex('c8dc593867e497dd73b11a4669ed425a377bf7698e33c991a0a0922ff5676f57')) 145 | 146 | signature_hmac_key_correct = byteswap( 147 | ba.a2b_hex('66ef0c1cbf1491a01e6249000dff641407ded27341b1ef3fd203b1b06474cadd')) 148 | 149 | ccd7_key_hash_correct = ba.a2b_hex('5c4aad785603dc702da3a87aee8017de255743671a5b5b0c56a7de10747e7cc2') 150 | 151 | aes_ikey_correct = ba.a2b_hex('8c2a4fbd636ea09acaa1b30c58ed8e3be9b84cab9b6c8146a6510eea096ef691') 152 | aes_key_correct = byteswap(ba.a2b_hex('986b02d27d60f3071aa794343407cc39')) 153 | 154 | hmac_ikey_correct = ba.a2b_hex('1473ffeec807413fc45c9748d9fdee41bb7ebfa86499d31a1a7bd4e492b57623') 155 | hmac_key_correct = ba.a2b_hex('a82c5d6424ad0a70a2f4334a69385539f63cda66ab96881ba1702aaad385a66f') 156 | 157 | # tests 158 | 159 | keys = NvDataKeys.from_file_and_hex( 160 | '/Users/cwerling/Git/psp-emulation/asrock/roms/ASRock_A520M_HVS_1.31.ftpm_with_data', 161 | 'fb2aaa2268624d6b0cfb1f8b69f936e84377b0f8169668dc0453484a33f81544' 162 | ) 163 | assert keys.wrapping_aes_key == wrapping_aes_key_correct 164 | assert keys.wrapping_hmac_key == wrapping_hmac_key_correct 165 | assert keys.signature_hmac_key == signature_hmac_key_correct 166 | 167 | assert keys.ftpm_key_mod_hash == ccd7_key_hash_correct 168 | 169 | assert keys.aes_i_key == aes_ikey_correct 170 | assert keys.aes_key == aes_key_correct 171 | 172 | assert keys.hmac_i_key == hmac_ikey_correct 173 | assert keys.hmac_key == hmac_key_correct 174 | -------------------------------------------------------------------------------- /amdnvtool/nv_data.py: -------------------------------------------------------------------------------- 1 | from psptool import PSPTool 2 | 3 | import json, base64 4 | 5 | from .crypto import NvDataKeys, sole 6 | from . import raw, crypto, parsed 7 | 8 | 9 | # Custom JSON encoder for binary data 10 | class Base64Encoder(json.JSONEncoder): 11 | def default(self, obj): 12 | if isinstance(obj, bytes): 13 | return { 14 | '__base64__': base64.b64encode(obj).decode('utf-8') 15 | } 16 | return json.JSONEncoder.default(self, obj) 17 | 18 | 19 | class HexByteEncoder(json.JSONEncoder): 20 | def default(self, obj): 21 | if isinstance(obj, parsed.HexBytes): 22 | return repr(obj) 23 | return json.JSONEncoder.default(self, obj) 24 | 25 | # Respective decoder for binary data 26 | # usage: json.loads(serialized_bytes, object_hook=as_base64) 27 | def as_base64(dct): 28 | if '__base64__' in dct: 29 | return base64.b64decode(dct['__base64__']) 30 | return dct 31 | 32 | 33 | class NVData: 34 | 35 | def __init__(self, raw, nv_data_keys: NvDataKeys): 36 | self.raw = raw 37 | self.keys = nv_data_keys 38 | self._parsed = None 39 | self._by_context = None 40 | 41 | @staticmethod 42 | def from_file_and_lsb_key_hex(filename: str, lsb_key_hex: str): 43 | pt = PSPTool.from_file(filename) 44 | psp_nv_data_entry = sole(set(pt.blob.get_entries_by_type(0x4))) 45 | 46 | nv_data_keys = NvDataKeys.from_file_and_lsb_key_hex(filename, lsb_key_hex) 47 | return NVData(raw.NVRom(psp_nv_data_entry.get_bytes()), nv_data_keys) 48 | 49 | @staticmethod 50 | def from_file_and_secret_hex(filename: str, secret_hex: str): 51 | pt = PSPTool.from_file(filename) 52 | psp_nv_data_entry = sole(set(pt.blob.get_entries_by_type(0x4))) 53 | 54 | nv_data_keys = NvDataKeys.from_file_and_secret_hex(filename, secret_hex) 55 | return NVData(raw.NVRom(psp_nv_data_entry.get_bytes()), nv_data_keys) 56 | 57 | @property 58 | def parsed(self): 59 | if not self._parsed: 60 | self._parsed = self.raw.to_parsed(self.keys.aes_key) 61 | return self._parsed 62 | 63 | @property 64 | def by_context(self): 65 | if not self._by_context: 66 | self._by_context = parsed.map_by_context_id(self.parsed) 67 | return self._by_context 68 | 69 | @property 70 | def are_hmacs_valid(self): 71 | return self.raw.verify_all_hmacs(self.keys.hmac_key) 72 | 73 | def print_parsed(self): 74 | for (nvdata_num, nvdata) in enumerate(self.parsed): 75 | print(f'NVData {nvdata_num}:') 76 | for (seq_num, entries) in enumerate(nvdata): 77 | print(f' Sequence {seq_num}:') 78 | for (entry_num, entry) in enumerate(entries): 79 | print(f' Entry {entry_num}:\n ' + entry.try_interpret().replace('\n', '\n ')) 80 | 81 | def print_by_context(self): 82 | for (context_id, sequence) in self.by_context.items(): 83 | print(f'Context {context_id:x}') 84 | for content in sequence: 85 | print(f' {content}') 86 | 87 | def print_json_by_context(self): 88 | json_obj = [{ 89 | 'context': context_id, 90 | 'sequence': [content for content in sequence] 91 | } for (context_id, sequence) in self.by_context.items()] 92 | print(json.dumps(json_obj, sort_keys=True, indent=4, cls=HexByteEncoder)) 93 | # print(json.dumps(json_obj, sort_keys=True, indent=4, cls=Base64Encoder)) 94 | -------------------------------------------------------------------------------- /amdnvtool/parsed.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Dict 3 | from binascii import hexlify 4 | 5 | 6 | class HexInt(int): 7 | def __repr__(self): 8 | return hex(self) 9 | 10 | 11 | class HexBytes(bytes): 12 | def __repr__(self): 13 | # return f'len={hex(len(self))}, hexbytes=\'{hexlify(self).decode()}\'' 14 | return hexlify(self).decode() 15 | 16 | def try_interpret(self): 17 | #s = self.decode('ascii', errors='backslashreplace') 18 | #ile = int.from_bytes(self, 'little') 19 | #ibe = int.from_bytes(self, 'big') 20 | res = f'{self.__repr__()}\n' 21 | res += f'{self}' 22 | #res += f'str : {s}\n' 23 | #res += f'i (le): {ile}\n' 24 | #res += f'i (be): {ibe}' 25 | return res 26 | 27 | 28 | @dataclass 29 | class Entry: 30 | context_id : HexInt 31 | sequence_nr : HexInt 32 | fields : List[HexBytes] 33 | 34 | @staticmethod 35 | def build(context_id, sequence_nr, fields): 36 | return Entry( 37 | HexInt(context_id), 38 | HexInt(sequence_nr), 39 | [HexBytes(field) for field in fields], 40 | ) 41 | 42 | 43 | def try_interpret(self): 44 | res = f'{self.context_id} {self.sequence_nr}' 45 | for (num, field) in enumerate(self.fields): 46 | res += f'\nField {num}:\n ' 47 | res += field.try_interpret().replace('\n','\n ') 48 | return res 49 | 50 | def map_by_context_id(entries : List[List[List[Entry]]]) -> Dict[int, List[HexBytes]]: 51 | result = dict() 52 | for nd in entries: 53 | for es in nd: 54 | for e in es: 55 | sequence = list() 56 | if result.get(e.context_id): 57 | sequence = result[e.context_id] 58 | 59 | if len(sequence) + 1 > e.sequence_nr: 60 | #assert sequence[e.sequence_nr-1] == e.fields 61 | pass 62 | else: 63 | for _ in range(len(sequence) + 1, e.sequence_nr): 64 | sequence.append(None) 65 | sequence.append(e.fields) 66 | 67 | result[e.context_id] = sequence 68 | return result 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /amdnvtool/raw.py: -------------------------------------------------------------------------------- 1 | #import sys 2 | from abc import ABC, abstractmethod 3 | from typing import Generator, List 4 | from . import crypto, parsed 5 | 6 | 7 | class RawBuffer(ABC): 8 | ''' 9 | Types like this are part of the parsing tree of the nv data blob. 10 | ''' 11 | 12 | name = None 13 | 14 | @property 15 | @abstractmethod 16 | def bytes(self) -> bytes: 17 | ''' 18 | The bytes of this object. 19 | ''' 20 | pass 21 | 22 | def __len__(self) -> int: 23 | return len(self.bytes) 24 | 25 | @property 26 | def value(self): 27 | ''' 28 | The value of this object. 29 | ''' 30 | return self.bytes 31 | 32 | @abstractmethod 33 | def fields(self) -> Generator: 34 | ''' 35 | The fields of this object. 36 | ''' 37 | pass 38 | 39 | def __str__(self) -> str: 40 | return self.value.__str__() 41 | 42 | def __repr__(self) -> str: 43 | prefix = '' 44 | if self.name: 45 | prefix = f'{self.name} : ' 46 | fields = list(map(lambda f: f.__repr__().replace('\n','\n '), self.fields())) 47 | if fields: 48 | return prefix + '{\n ' + ',\n '.join(fields) + '\n}' 49 | return prefix + self.__str__() 50 | 51 | 52 | class NamedBytes(RawBuffer): 53 | ''' 54 | Basically a field 55 | ''' 56 | 57 | def __init__(self, name: str, buf: bytes): 58 | self.name = name 59 | self._buf = buf 60 | 61 | @property 62 | def bytes(self) -> bytes: 63 | return self._buf 64 | 65 | def fields(self) -> Generator: 66 | yield from () 67 | 68 | class NamedLittleInt(NamedBytes): 69 | @property 70 | def value(self) -> int: 71 | return int.from_bytes(self.bytes, 'little') 72 | 73 | def __str__(self) -> str: 74 | return hex(self.value) 75 | 76 | class NamedBigInt(NamedBytes): 77 | @property 78 | def value(self) -> int: 79 | return int.from_bytes(self.bytes, 'big') 80 | 81 | def __str__(self) -> str: 82 | return hex(self.value) 83 | 84 | class NamedStr(NamedBytes): 85 | @property 86 | def value(self) -> str: 87 | return self.bytes.decode('ascii', errors='backslashreplace') 88 | 89 | class Header(NamedBytes): 90 | ''' 91 | The header of the NVData structure. 92 | ''' 93 | 94 | def __init__(self, buf: bytes): 95 | super().__init__('header', buf) 96 | 97 | assert len(buf) == 0x40, "The header needs to be 0x40 bytes long" 98 | self._buffer = buf 99 | 100 | self.magic = NamedStr("magic", buf[:4]) 101 | self.version = NamedLittleInt("version", buf[4:8]) 102 | self.reserved = NamedBytes("reserved", buf[8:]) 103 | 104 | assert self.magic.value in ['NVx3', 'NVx2'] 105 | #assert self.version.value == 1 106 | assert self.reserved.bytes == b'\xff' * 0x38 107 | 108 | def fields(self) -> Generator: 109 | yield self.magic 110 | yield self.version 111 | yield self.reserved 112 | 113 | class EntryHeader(NamedBytes): 114 | ''' 115 | An NVData entry header 116 | ''' 117 | 118 | def __init__(self, buf: bytes): 119 | super().__init__('header', buf) 120 | 121 | assert len(buf) == 0x20, "The entry header needs to be 0x20 bytes long" 122 | self._buffer = buf 123 | 124 | self.reserved_1 = NamedBytes("reserved_1", buf[:4]) 125 | 126 | self.unknown_1 = NamedLittleInt("unknown_1", buf[4:6]) 127 | 128 | self.total_size = NamedLittleInt("total_size", buf[6:8]) 129 | self.body_size = NamedLittleInt("body_size", buf[8:10]) 130 | 131 | self.has_checksum = NamedLittleInt("has_checksum", buf[10:12]) 132 | self.context = NamedLittleInt("context", buf[12:16]) 133 | self.sequence_nr = NamedLittleInt("sequence_nr", buf[16:20]) 134 | 135 | self.magic = NamedStr("magic", buf[20:24]) 136 | 137 | self.reserved_2 = NamedBytes("reserved_2", buf[24:32]) 138 | 139 | assert self.has_checksum.value in {0,1} 140 | 141 | #assert self.reserved_1.bytes == b'\0'*4 142 | assert self.reserved_2.bytes == b'\0'*8 143 | 144 | assert self.unknown_1.value == 2 145 | 146 | assert self.total_size.value - self.body_size.value == 0x20 147 | assert self.total_size.value & 0xf == 0 148 | 149 | assert self.magic.value == 'NVR_' 150 | 151 | def fields(self) -> Generator: 152 | yield self.unknown_1 153 | yield self.total_size 154 | yield self.body_size 155 | yield self.has_checksum 156 | yield self.context 157 | yield self.sequence_nr 158 | yield self.magic 159 | 160 | 161 | class EntryFieldDefs(NamedBytes): 162 | ''' 163 | An NVData entry field definitions 164 | ''' 165 | 166 | def __init__(self, buf: bytes): 167 | assert len(buf) == 0x20, "The entry field definitions are 0x20 bytes long!" 168 | super().__init__('field_defs', buf) 169 | 170 | self.total_size = NamedLittleInt("total_size", buf[:4]) 171 | # total size includes hmac, iv, and field_defs 172 | expected_total_size = self.total_size.value - 0x30 - 0x20 173 | 174 | assert expected_total_size > 0, "There is no space for any fields {=expected_total_size}!" 175 | 176 | self.field_sizes = list() 177 | field_num = 0 178 | total_field_size = 0 179 | 180 | while field_num < 7: 181 | field_size_buf = buf[4+4*field_num:8+4*field_num] 182 | field_size = NamedLittleInt(f'field_{field_num}_size', field_size_buf) 183 | 184 | # last field def 185 | if field_size.value == 0: 186 | assert buf[4+4*field_num:] == b'\0' * (0x1c - 4*field_num), \ 187 | "There should only be zeroes after the last field def!" 188 | break 189 | 190 | total_field_size += field_size.value 191 | assert total_field_size <= expected_total_size 192 | 193 | self.field_sizes.append(field_size) 194 | 195 | field_num += 1 196 | 197 | assert total_field_size == expected_total_size 198 | 199 | def fields(self) -> Generator: 200 | yield self.total_size 201 | for field_size in self.field_sizes: 202 | yield field_size 203 | 204 | 205 | class EntryBody(NamedBytes): 206 | ''' 207 | An NVData entry's body 208 | ''' 209 | 210 | def __init__(self, buf: bytes): 211 | 212 | super().__init__('entry_body', buf) 213 | 214 | assert len(self) >= 0x30, \ 215 | "There needs to be enough space for the iv and the field definitions!" 216 | 217 | self.iv = NamedBytes('iv', self.bytes[:0x10]) 218 | 219 | self.field_defs = EntryFieldDefs(self.bytes[0x10:0x30]) 220 | assert len(self) + 0x20 == self.field_defs.total_size.value 221 | 222 | self.body_fields = list() 223 | next_field_start = 0x30 224 | for (field_num, field_size) in enumerate(self.field_defs.field_sizes): 225 | field_buffer = self.bytes[next_field_start:next_field_start + field_size.value] 226 | self.body_fields.append(NamedBytes(f'field_{field_num}', field_buffer)) 227 | 228 | next_field_start += field_size.value 229 | assert next_field_start <= len(self) 230 | 231 | def fields(self) -> Generator: 232 | yield self.iv 233 | yield self.field_defs 234 | for body_field in self.body_fields: 235 | yield body_field 236 | 237 | def raw_fields(self, _key): 238 | return [field.bytes for field in self.body_fields] 239 | 240 | def decrypt_fields(self, key): 241 | return [ 242 | crypto.aes_ctr_dec(key, self.iv.bytes, field.bytes) 243 | for field in self.body_fields 244 | ] 245 | 246 | class Entry(NamedBytes): 247 | ''' 248 | An NVData entry 249 | ''' 250 | 251 | def __init__(self, buf: bytes): 252 | 253 | self.header = EntryHeader(buf[:0x20]) 254 | 255 | super().__init__('entry', buf[:self.header.total_size.value]) 256 | 257 | assert len(self) >= 0x40, \ 258 | "There needs to be enough space for header and hmac!" 259 | 260 | self.hmac = NamedBytes('hmac', self.bytes[0x20:0x40]) 261 | self.body = EntryBody(self.bytes[0x40:]) 262 | 263 | def fields(self) -> Generator: 264 | yield self.header 265 | yield self.hmac 266 | yield self.body 267 | 268 | def verify_hmac(self, key) -> bool: 269 | return crypto.hmac_sha256(key, self.body.bytes) == self.hmac.bytes 270 | 271 | def to_parsed(self, key) -> parsed.Entry: 272 | return parsed.Entry.build( 273 | self.header.context.value, 274 | self.header.sequence_nr.value, 275 | #self.body.raw_fields(key), 276 | self.body.decrypt_fields(key), 277 | ) 278 | 279 | 280 | class NVEntrySequence(NamedBytes): 281 | ''' 282 | NV entries ending in an hmac checksum 283 | ''' 284 | 285 | def __init__(self, buf: bytes): 286 | 287 | assert len(buf) >= 0x70, 'There needs to be enough space for an entry and the checksum!' 288 | 289 | self.entries = list() 290 | next_entry_start = 0x0 291 | 292 | while True: 293 | 294 | #sys.stdout.write(f'\rparsing entry at 0x{next_entry_start:x}') 295 | #sys.stdout.flush() 296 | 297 | try: 298 | entry = Entry(buf[next_entry_start:]) 299 | except: 300 | #sys.stdout.write('\n') 301 | raise 302 | next_entry_start += len(entry) 303 | self.entries.append(entry) 304 | 305 | if entry.header.has_checksum.value: 306 | break 307 | 308 | #sys.stdout.write('\r \r') 309 | 310 | self.hmac = NamedBytes('hmac', buf[next_entry_start:next_entry_start+0x20]) 311 | 312 | super().__init__('nv_entry_sequence', buf[:next_entry_start + 0x20]) 313 | 314 | def fields(self) -> Generator: 315 | for entry in self.entries: 316 | yield entry 317 | yield self.hmac 318 | 319 | def verify_all_hmacs(self, key) -> bool: 320 | return all(e.verify_hmac(key) for e in self.entries) 321 | 322 | def assert_hmac_is_valid(self, key): 323 | for e in self.entries: 324 | if not e.verify_hmac(key): 325 | raise Exception(e, "is not valid!") 326 | 327 | def to_parsed(self, key) -> List[parsed.Entry]: 328 | return [entry.to_parsed(key) for entry in self.entries] 329 | 330 | 331 | class NVData(NamedBytes): 332 | ''' 333 | NVData 334 | ''' 335 | 336 | def __init__(self, buf: bytes): 337 | assert len(buf) >= 0x60, 'There needs to be enough space for header and checksum!' 338 | 339 | self.header = Header(buf[:0x40]) 340 | self.entry_seqs = list() 341 | next_entry_seq_start = 0x40 342 | while buf[next_entry_seq_start:next_entry_seq_start+0x10] != b'\xff'*0x10: 343 | entry_seq = NVEntrySequence(buf[next_entry_seq_start:]) 344 | 345 | next_entry_seq_start += len(entry_seq) 346 | self.entry_seqs.append(entry_seq) 347 | 348 | free_bytes_end = next_entry_seq_start 349 | while free_bytes_end < len(buf) and buf[free_bytes_end] == 255: 350 | free_bytes_end += 1 351 | self.free_space = NamedBytes('free_space', buf[next_entry_seq_start:free_bytes_end]) 352 | assert self.free_space.bytes == b'\xff' * len(self.free_space) 353 | 354 | super().__init__('nv_data', buf[:free_bytes_end]) 355 | 356 | def fields(self) -> Generator: 357 | yield self.header 358 | for entry_seq in self.entry_seqs: 359 | yield entry_seq 360 | 361 | def verify_all_hmacs(self, key) -> bool: 362 | buffer_len = len(self.header) 363 | for entry_seq in self.entry_seqs: 364 | buffer_len += len(entry_seq) 365 | checksum = crypto.hmac_sha256(key, self.bytes[:buffer_len-0x20]) 366 | if checksum != entry_seq.hmac.bytes: 367 | return False 368 | 369 | return all(s.verify_all_hmacs(key) for s in self.entry_seqs) 370 | 371 | 372 | def assert_all_hmacs_are_valid(self, key): 373 | for s in self.entry_seqs: 374 | s.assert_hmac_is_valid(key) 375 | 376 | buffer_len = len(self.header) 377 | for entry_seq in self.entry_seqs: 378 | buffer_len += len(entry_seq) 379 | checksum = crypto.hmac_sha256(key, self.bytes[:buffer_len-0x20]) 380 | if checksum != entry_seq.hmac.bytes: 381 | raise Exception("Sequence hmac up until {buffer_len} is invalid!") 382 | 383 | def to_parsed(self, key) -> List[List[parsed.Entry]]: 384 | return [seq.to_parsed(key) for seq in self.entry_seqs] 385 | 386 | 387 | class NVRom(NamedBytes): 388 | ''' 389 | NVRom 390 | ''' 391 | 392 | def __init__(self, buf: bytes): 393 | 394 | super().__init__('nv_rom', buf) 395 | 396 | self.nvdatas = list() 397 | next_nvdata = 0 398 | while next_nvdata < len(buf): 399 | nvdata = NVData(buf[next_nvdata:]) 400 | 401 | next_nvdata += len(nvdata) 402 | self.nvdatas.append(nvdata) 403 | 404 | def fields(self) -> Generator: 405 | for nvdata in self.nvdatas: 406 | yield nvdata 407 | 408 | def verify_all_hmacs(self, key) -> bool: 409 | return all(d.verify_all_hmacs(key) for d in self.nvdatas) 410 | 411 | def assert_all_hmacs_are_valid(self, key) -> bool: 412 | for d in self.nvdatas: 413 | d.assert_all_hmacs_are_valid(key) 414 | 415 | def to_parsed(self, key) -> List[List[List[parsed.Entry]]]: 416 | return [nvdata.to_parsed(key) for nvdata in self.nvdatas] 417 | 418 | 419 | -------------------------------------------------------------------------------- /example.nvdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PSPReverse/amd-nv-tool/57f78e1ffbfb50c22951c2bc7920515a9070bed6/example.nvdata -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = amdnvtool 3 | version = 0.1 4 | author = Christian Werling, Niklas Jacob 5 | author_email = cwerling@posteo.de, hnj@posteo.de 6 | url = https://github.com/PSPReverse/amd-nv-tool 7 | description = amdnvtool can extract and modify information from BIOS images of AMD systems 8 | 9 | classifiers= Development Status :: 4 - Beta 10 | Intended Audience :: Science/Research 11 | Topic :: Security 12 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 13 | Programming Language :: Python :: 3.8 14 | 15 | [options] 16 | packages = amdnvtool 17 | install_requires = cryptography 18 | psptool 19 | 20 | [options.entry_points] 21 | console_scripts = amdnvtool = amdnvtool.__main__:main 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | with open('README.md') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | long_description=long_description, 10 | long_description_content_type='text/markdown', 11 | ) 12 | --------------------------------------------------------------------------------