psptool 2.6__tar.gz → 2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {psptool-2.6 → psptool-2.7}/.github/workflows/python-tests.yml +2 -3
  2. {psptool-2.6 → psptool-2.7}/.gitmodules +1 -1
  3. {psptool-2.6 → psptool-2.7}/PKG-INFO +4 -2
  4. {psptool-2.6 → psptool-2.7}/README.md +3 -1
  5. {psptool-2.6 → psptool-2.7}/psptool/__main__.py +6 -2
  6. {psptool-2.6 → psptool-2.7}/psptool/blob.py +3 -0
  7. {psptool-2.6 → psptool-2.7}/psptool/cert_tree.py +7 -4
  8. {psptool-2.6 → psptool-2.7}/psptool/directory.py +26 -10
  9. {psptool-2.6 → psptool-2.7}/psptool/entry.py +94 -42
  10. {psptool-2.6 → psptool-2.7}/psptool/fet.py +18 -6
  11. {psptool-2.6 → psptool-2.7}/psptool/psptool.py +25 -5
  12. {psptool-2.6 → psptool-2.7}/psptool/utils.py +19 -0
  13. {psptool-2.6 → psptool-2.7}/psptool.egg-info/PKG-INFO +4 -2
  14. {psptool-2.6 → psptool-2.7}/psptool.egg-info/SOURCES.txt +2 -0
  15. psptool-2.7/psptool.egg-info/requires.txt +2 -0
  16. {psptool-2.6 → psptool-2.7}/setup.cfg +2 -2
  17. psptool-2.7/tests/create_metrics.sh +20 -0
  18. {psptool-2.6 → psptool-2.7}/tests/integration/test_rom_files.py +1 -2
  19. psptool-2.7/tests/metrics.txt +449 -0
  20. psptool-2.6/psptool.egg-info/requires.txt +0 -2
  21. {psptool-2.6 → psptool-2.7}/.gitignore +0 -0
  22. {psptool-2.6 → psptool-2.7}/LICENSE +0 -0
  23. {psptool-2.6 → psptool-2.7}/psptool/__init__.py +0 -0
  24. {psptool-2.6 → psptool-2.7}/psptool/crypto.py +0 -0
  25. {psptool-2.6 → psptool-2.7}/psptool/errors.py +0 -0
  26. {psptool-2.6 → psptool-2.7}/psptool/firmware.py +0 -0
  27. {psptool-2.6 → psptool-2.7}/psptool/rom.py +0 -0
  28. {psptool-2.6 → psptool-2.7}/psptool.egg-info/dependency_links.txt +0 -0
  29. {psptool-2.6 → psptool-2.7}/psptool.egg-info/entry_points.txt +0 -0
  30. {psptool-2.6 → psptool-2.7}/psptool.egg-info/top_level.txt +0 -0
  31. {psptool-2.6 → psptool-2.7}/setup.py +0 -0
  32. {psptool-2.6 → psptool-2.7}/tests/__init__.py +0 -0
  33. {psptool-2.6 → psptool-2.7}/tests/integration/__init__.py +0 -0
  34. {psptool-2.6 → psptool-2.7}/tests/integration/test_psptrace.py +0 -0
  35. {psptool-2.6 → psptool-2.7}/tests/unit/__init__.py +0 -0
  36. {psptool-2.6 → psptool-2.7}/tests/unit/test_cli.py +0 -0
@@ -14,8 +14,7 @@ jobs:
14
14
  - uses: actions/checkout@v3
15
15
  with:
16
16
  submodules: recursive
17
- ssh-key: ${{ secrets.PSPTOOL_FIXTURES_REPO_PRIVATE_KEY }}
18
- lfs: true
17
+ ssh-key: ${{ secrets.PSPTOOL_FIXTURES_NEW_PRIVATE_KEY }}
19
18
  - name: Set up Python ${{ matrix.python-version }}
20
19
  uses: actions/setup-python@v3
21
20
  with:
@@ -34,4 +33,4 @@ jobs:
34
33
  flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
35
34
  - name: Test with unittest
36
35
  run: |
37
- python -m unittest discover -s tests -v
36
+ python -m unittest discover -s tests -v
@@ -1,3 +1,3 @@
1
1
  [submodule "tests/integration/fixtures"]
2
2
  path = tests/integration/fixtures
3
- url = git@github.com:PSPReverse/PSPTool-fixtures.git
3
+ url = git@github.com:cwerling/PSPTool-fixtures.git
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: psptool
3
- Version: 2.6
3
+ Version: 2.7
4
4
  Summary: PSPTool is a tool for dealing with AMD binary blobs
5
5
  Home-page: https://github.com/PSPReverse/PSPTool
6
6
  Author: Christian Werling
@@ -23,6 +23,8 @@ It is based on reverse-engineering efforts of AMD's **proprietary filesystem** u
23
23
  PSPTool favourably works with UEFI images as obtained through BIOS updates. If these updates are only available through Windows executables, tools like [innoextract](https://github.com/dscharrer/innoextract) can help.
24
24
 
25
25
  PSPTool was developed at TU Berlin in context of the following research:
26
+ - 2023, talk: [Jailbreaking an Electric Vehicle in 2023 or What It Means to Hotwire Tesla's x86-Based Seat Heater](https://www.blackhat.com/us-23/briefings/schedule/#jailbreaking-an-electric-vehicle-in--or-what-it-means-to-hotwire-teslas-x-based-seat-heater-33049)
27
+ - 2023, paper: [faulTPM: Exposing AMD fTPMs' Deepest Secrets](https://arxiv.org/abs/2304.14717)
26
28
  - 2021, paper: [One Glitch to Rule Them All: Fault Injection Attacks Against AMD's Secure Encrypted Virtualization](https://arxiv.org/abs/2108.04575)
27
29
  - 2020, talk: [All You Ever Wanted to Know about the AMD Platform Security Processor and were Afraid to Emulate](https://youtu.be/KR8bPLj4nKE)
28
30
  - 2019, talk: [Uncover, Understand, Own - Regaining Control Over Your AMD CPU](https://media.ccc.de/v/36c3-10942-uncover_understand_own_-_regaining_control_over_your_amd_cpu)
@@ -66,7 +68,7 @@ $ psptool Lenovo_Thinkpad_T495_r12uj35wd.iso
66
68
 
67
69
  <details>
68
70
  <summary>Click to expand output</summary>
69
-
71
+
70
72
  ```
71
73
  +-----+----------+-----------+----------+--------------------------------+
72
74
  | ROM | Addr | Size | FET | AGESA |
@@ -7,6 +7,8 @@ It is based on reverse-engineering efforts of AMD's **proprietary filesystem** u
7
7
  PSPTool favourably works with UEFI images as obtained through BIOS updates. If these updates are only available through Windows executables, tools like [innoextract](https://github.com/dscharrer/innoextract) can help.
8
8
 
9
9
  PSPTool was developed at TU Berlin in context of the following research:
10
+ - 2023, talk: [Jailbreaking an Electric Vehicle in 2023 or What It Means to Hotwire Tesla's x86-Based Seat Heater](https://www.blackhat.com/us-23/briefings/schedule/#jailbreaking-an-electric-vehicle-in--or-what-it-means-to-hotwire-teslas-x-based-seat-heater-33049)
11
+ - 2023, paper: [faulTPM: Exposing AMD fTPMs' Deepest Secrets](https://arxiv.org/abs/2304.14717)
10
12
  - 2021, paper: [One Glitch to Rule Them All: Fault Injection Attacks Against AMD's Secure Encrypted Virtualization](https://arxiv.org/abs/2108.04575)
11
13
  - 2020, talk: [All You Ever Wanted to Know about the AMD Platform Security Processor and were Afraid to Emulate](https://youtu.be/KR8bPLj4nKE)
12
14
  - 2019, talk: [Uncover, Understand, Own - Regaining Control Over Your AMD CPU](https://media.ccc.de/v/36c3-10942-uncover_understand_own_-_regaining_control_over_your_amd_cpu)
@@ -50,7 +52,7 @@ $ psptool Lenovo_Thinkpad_T495_r12uj35wd.iso
50
52
 
51
53
  <details>
52
54
  <summary>Click to expand output</summary>
53
-
55
+
54
56
  ```
55
57
  +-----+----------+-----------+----------+--------------------------------+
56
58
  | ROM | Addr | Size | FET | AGESA |
@@ -46,6 +46,7 @@ def main():
46
46
  parser.add_argument('-n', '--no-duplicates', help=SUPPRESS, action='store_true')
47
47
  parser.add_argument('-j', '--json', help=SUPPRESS, action='store_true')
48
48
  parser.add_argument('-t', '--key-tree', help=SUPPRESS, action='store_true')
49
+ parser.add_argument('-m', '--metrics', help=SUPPRESS, action='store_true')
49
50
  parser.add_argument('-p', '--privkeystub', help=SUPPRESS)
50
51
  parser.add_argument('-a', '--privkeypass', help=SUPPRESS)
51
52
 
@@ -60,6 +61,7 @@ def main():
60
61
  '-n: list unique entries only ordered by their offset',
61
62
  '-j: output in JSON format instead of tables',
62
63
  '-t: print tree of all signed entities and their certifying keys',
64
+ '-m: print entry parsing metrics for testing',
63
65
  '', '']), action='store_true')
64
66
 
65
67
  action.add_argument('-X', '--extract-entry', help='\n'.join([
@@ -114,7 +116,7 @@ def main():
114
116
  elif args.decrypt:
115
117
  if not entry.encrypted:
116
118
  ph.print_error_and_exit(f'Entry is not encrypted {entry.get_readable_type()}')
117
- output = entry.get_decrypted()
119
+ output = entry.to_decrypted_entry_bytes()
118
120
  elif args.pem_key:
119
121
  output = entry.get_pem_encoded()
120
122
  else:
@@ -187,7 +189,7 @@ def main():
187
189
  if args.privkeystub:
188
190
  privkeys = PrivateKeyDict.read_from_files(args.privkeystub, args.privkeypass)
189
191
 
190
- if entry.signed_entity:
192
+ if hasattr(entry, 'signed_entity') and entry.signed_entity:
191
193
  entry.signed_entity.resign_and_replace(privkeys=privkeys, recursive=True)
192
194
  else:
193
195
  ph.print_warning("Did not resign anything since target entry is not signed")
@@ -203,6 +205,8 @@ def main():
203
205
  psp.ls_json(verbose=args.verbose)
204
206
  elif args.key_tree:
205
207
  psp.cert_tree.print_key_tree()
208
+ elif args.metrics:
209
+ psp.print_metrics()
206
210
  elif args.no_duplicates:
207
211
  psp.ls_entries(verbose=args.verbose)
208
212
  else:
@@ -75,6 +75,9 @@ class Blob(NestedBuffer):
75
75
  if not fet_parsed:
76
76
  self.psptool.ph.print_warning(f"Skipping FET at {hex(fet_location)} due to unknown ROM alignment")
77
77
 
78
+ if len(self.roms) == 0:
79
+ self.psptool.ph.print_warning("Could not find any Firmware Entry Table!")
80
+
78
81
  self._construct_range_dict()
79
82
 
80
83
  def __repr__(self):
@@ -106,7 +106,8 @@ class SignedEntity:
106
106
  print(f' Need to rehash')
107
107
  self.entry.update_sha256()
108
108
  print(f' Done')
109
- assert self.signature.buffer_size == privkey.signature_size
109
+ assert self.signature.buffer_size == privkey.signature_size, \
110
+ f"{self.signature.buffer_size=}, {privkey.signature_size=}"
110
111
  signature = privkey.sign_blob(self.entry.get_signed_bytes())
111
112
  assert len(signature) == self.signature.buffer_size, f'Could not resign {self} with {privkey}: ' \
112
113
  f'The new signature has the wrong length ' \
@@ -116,6 +117,8 @@ class SignedEntity:
116
117
  def resign_and_replace(self, privkeys: PrivateKeyDict = None, recursive: bool = False):
117
118
  # this resigns self (multiple times!)
118
119
  for pk in self.certifying_keys:
120
+ if pk in self.contained_keys:
121
+ continue # TODO hotfix
119
122
  pk.replace_and_resign(privkeys, recursive=recursive)
120
123
 
121
124
 
@@ -227,13 +230,13 @@ class PublicKeyEntity:
227
230
  privkey = privkeys[self.key_type.name]
228
231
  assert self.key_type.signature_size == privkey.signature_size
229
232
 
233
+ # replace self
234
+ self.replace_only(privkey.get_public_key())
235
+
230
236
  # resign children
231
237
  for se in self.certified_entities:
232
238
  se.resign_only(privkey)
233
239
 
234
- # replace self
235
- self.replace_only(privkey.get_public_key())
236
-
237
240
  # check crypto
238
241
  for se in self.certified_entities:
239
242
  assert se.is_verified_by(self), f'Resigning {se} with {self} failed!'
@@ -32,21 +32,23 @@ class Directory(NestedBuffer):
32
32
  'rsv2'
33
33
  ]
34
34
 
35
+ _DEFAULT_HEADER_SIZE = 2 * 4,
35
36
  _HEADER_SIZES = {
36
37
  b'$PSP': 4 * 4,
37
38
  b'$PL2': 4 * 4,
38
39
  b'$BHD': 4 * 4,
39
- b'$BL2': 4 * 4
40
+ b'$BL2': 4 * 4,
40
41
  }
41
42
 
43
+ _DEFAULT_ENTRY_SIZE = 2 * 4,
42
44
  _ENTRY_SIZES = {
43
45
  b'$PSP': 4 * 4,
44
46
  b'$PL2': 4 * 4,
45
47
  b'$BHD': 4 * 6,
46
- b'$BL2': 4 * 6
48
+ b'$BL2': 4 * 6,
47
49
  }
48
50
 
49
- _ENTRY_TYPES_SECONDARY_DIR = [0x40, 0x70]
51
+ _ENTRY_TYPES_SECONDARY_DIR = [0x40, 0x48, 0x49, 0x4a, 0x70]
50
52
  _ENTRY_TYPES_PUBKEY = [0x0, 0x9, 0xa, 0x5, 0xd]
51
53
 
52
54
  def __init__(self, parent_rom, rom_address: int, type_: str, psptool, zen_generation='unknown'):
@@ -69,16 +71,16 @@ class Directory(NestedBuffer):
69
71
  self.type = type_
70
72
  self.entries: List[Entry] = []
71
73
 
72
- self._entry_size = self._ENTRY_SIZES[self.magic]
74
+ self._entry_size = self._ENTRY_SIZES.get(self.magic, self._DEFAULT_ENTRY_SIZE)
73
75
 
74
76
  self._parse_entries()
75
77
 
76
78
  # check entries for a link to a secondary directory (i.e. a continuation of this directory)
77
- self.secondary_directory_address = None
79
+ self.secondary_directory_addresses = []
78
80
  for entry in self.entries:
79
81
  if entry.type in self._ENTRY_TYPES_SECONDARY_DIR:
80
- # print_warning(f"Secondary dir at 0x{entry.buffer_offset:x}")
81
- self.secondary_directory_address = entry.buffer_offset
82
+ # psptool.ph.print_warning(f"Secondary dir at 0x{entry.buffer_offset:x}")
83
+ self.secondary_directory_addresses.append(entry.buffer_offset)
82
84
 
83
85
  self.verify_checksum()
84
86
 
@@ -97,6 +99,11 @@ class Directory(NestedBuffer):
97
99
  self.header[8:12] = struct.pack('<I', self.count)
98
100
  self.update_checksum()
99
101
 
102
+ @property
103
+ def address_mode(self):
104
+ rsvd = struct.unpack('=L', self.reserved)[0]
105
+ return (rsvd & 0x60000000) >> 29 if rsvd != 0 else None
106
+
100
107
  def _parse_header(self):
101
108
  # ugly to do this manually, but we do not know our size yet
102
109
  self._count = int.from_bytes(
@@ -104,15 +111,16 @@ class Directory(NestedBuffer):
104
111
  'little'
105
112
  )
106
113
  self.magic = self.rom.get_bytes(self.buffer_offset, 4)
114
+ self.reserved = self.rom.get_bytes(self.buffer_offset + 12, 4)
107
115
 
108
116
  self.header = NestedBuffer(
109
117
  self,
110
- self._HEADER_SIZES[self.magic]
118
+ self._HEADER_SIZES.get(self.magic, self._DEFAULT_HEADER_SIZE)
111
119
  )
112
120
  self.body = NestedBuffer(
113
121
  self,
114
- self._ENTRY_SIZES[self.magic] * self._count,
115
- buffer_offset=self._HEADER_SIZES[self.magic]
122
+ self._ENTRY_SIZES.get(self.magic, self._DEFAULT_ENTRY_SIZE) * self._count,
123
+ buffer_offset=self._HEADER_SIZES.get(self.magic, self._DEFAULT_HEADER_SIZE)
116
124
  )
117
125
 
118
126
  self.buffer_size = len(self.header) + len(self.body)
@@ -126,15 +134,23 @@ class Directory(NestedBuffer):
126
134
  for key, word in zip(self.ENTRY_FIELDS, chunker(entry_bytes, 4)):
127
135
  entry_fields[key] = struct.unpack('<I', word)[0]
128
136
 
137
+ entry_fields['type_flags'] = entry_fields['type'] >> 8
138
+ entry_fields['type'] = entry_fields['type'] & 0xFFFF
139
+
129
140
  entry_fields['offset'] &= self.rom.addr_mask
130
141
  destination = None
131
142
 
132
143
  if entry_fields['type'] in BIOS_ENTRY_TYPES:
133
144
  destination = struct.unpack('<Q', entry_bytes[0x10:0x18])[0]
134
145
 
146
+ # Seen on the Lenovo X13 for the first time
147
+ if self.address_mode == 2:
148
+ entry_fields['offset'] += self.buffer_offset
149
+
135
150
  try:
136
151
  entry = Entry.from_fields(self, self.parent_buffer,
137
152
  entry_fields['type'],
153
+ entry_fields['type_flags'],
138
154
  entry_fields['size'],
139
155
  entry_fields['offset'],
140
156
  self.rom,
@@ -61,11 +61,16 @@ class Entry(NestedBuffer):
61
61
  0x12: 'SMU_OFF_CHIP_FW_2',
62
62
  0x13: 'DEBUG_UNLOCK',
63
63
  0x1A: 'PSP_S3_NV_DATA',
64
+ 0x20: 'HARDWARE_IP_CONFIG',
64
65
  0x21: 'WRAPPED_IKEK',
65
66
  0x22: 'TOKEN_UNLOCK',
66
67
  0x24: 'SEC_GASKET',
67
68
  0x25: 'MP2_FW',
69
+ 0x26: 'MP2_FW_2',
70
+ 0x27: 'USER_MODE_UNIT_TEST',
68
71
  0x28: 'DRIVER_ENTRIES',
72
+ 0x29: 'KVM_IMAGE',
73
+ 0x2A: 'MP5_FW',
69
74
  0x2D: 'S0I3_DRIVER',
70
75
  0x30: 'ABL0',
71
76
  0x31: 'ABL1',
@@ -75,35 +80,52 @@ class Entry(NestedBuffer):
75
80
  0x35: 'ABL5',
76
81
  0x36: 'ABL6',
77
82
  0x37: 'ABL7',
83
+ 0x38: 'SEV_DATA',
84
+ 0x39: 'SEV_CODE',
78
85
  0x3A: 'FW_PSP_WHITELIST',
86
+ 0x3C: 'VBIOS_PRELOAD',
79
87
  # 0x40: 'FW_L2_PTR',
80
88
  0x41: 'FW_IMC',
81
89
  0x42: 'FW_GEC',
82
90
  # 0x43: 'FW_XHCI',
83
91
  0x44: 'FW_INVALID',
92
+ 0x45: 'TOS_SECURITY_POLICY',
93
+ 0x47: 'DRTM_TA',
94
+ 0x51: 'TOS_PUBLIC_KEY',
95
+ 0x54: 'PSP_NVRAM',
96
+ 0x55: 'BL_ROLLBACK_SPL',
97
+ 0x5a: 'MSMU_BINARY_0',
98
+ 0x5c: 'WMOS',
99
+ 0x71: 'DMCUB_INS',
84
100
  0x46: 'ANOTHER_FET',
85
101
  0x50: 'KEY_DATABASE',
86
102
  0x5f: 'FW_PSP_SMUSCS',
87
- 0x60: 'FW_IMC',
88
- 0x61: 'FW_GEC',
103
+ 0x60: 'APCB',
104
+ 0x61: 'APOB',
89
105
  0x62: 'FW_XHCI',
90
- 0x63: 'FW_INVALID',
106
+ 0x63: 'APOB_NV_COPY',
107
+ 0x64: 'PMU_CODE',
108
+ 0x65: 'PMU_DATA',
109
+ 0x66: 'MICROCODE_PATCH',
110
+ 0x67: 'CORE_MCE_DATA',
111
+ 0x68: 'APCB_COPY',
112
+ 0x69: 'EARLY_VGA_IMAGE',
113
+ 0x6A: 'MP2_FW_CFG',
114
+ 0x73: 'PSP_FW_BOOT_LOADER',
115
+ 0x80: 'OEM_System_Trusted_Application',
116
+ 0x81: 'OEM_System_TA_Signing_key',
91
117
  0x108: 'PSP_SMU_FN_FIRMWARE',
92
118
  0x118: 'PSP_SMU_FN_FIRMWARE2',
93
119
 
94
120
  # Entry types named by us
95
121
  # Custom names are denoted by a leading '!'
96
122
  0x14: '!PSP_MCLF_TRUSTLETS', # very similiar to ~PspTrustlets.bin~ in coreboot blobs
97
- 0x38: '!PSP_ENCRYPTED_NV_DATA',
98
123
  0x40: '!PL2_SECONDARY_DIRECTORY',
99
124
  0x43: '!KEY_UNKNOWN_1',
100
125
  0x4e: '!KEY_UNKNOWN_2',
101
126
  0x70: '!BL2_SECONDARY_DIRECTORY',
102
127
  0x15f: '!FW_PSP_SMUSCS_2', # seems to be a secondary FW_PSP_SMUSCS (see above)
103
128
  0x112: '!SMU_OFF_CHIP_FW_3', # seems to tbe a tertiary SMU image (see above)
104
- 0x39: '!SEV_APP',
105
- 0x10062: '!UEFI-IMAGE',
106
- 0x30062: '!UEFI-IMAGE',
107
129
  0xdead: '!KEY_NOT_IN_DIR'
108
130
 
109
131
  }
@@ -122,19 +144,15 @@ class Entry(NestedBuffer):
122
144
  pass
123
145
 
124
146
  @classmethod
125
- def from_fields(cls, parent_directory, parent_buffer, type_, size, offset, blob, psptool, destination: int = None):
147
+ def from_fields(cls, parent_directory, parent_buffer, type_, type_flags, size, offset, blob, psptool, destination: int = None):
126
148
  # Try to parse these ID's as a key entry
127
149
  # todo: consolidate these constants with Directory._ENTRY_TYPES_PUBKEY
128
150
  PUBKEY_ENTRY_TYPES = [0x0, 0x9, 0xa, 0x5, 0xd, 0x43, 0x4e, 0xdead]
129
151
 
130
152
  # Types known to have no PSP HDR
131
153
  # TODO: Find a better way to identify those entries
132
- NO_HDR_ENTRY_TYPES = [0x4, 0xb, 0x21, 0x40, 0x70, 0x30062, 0x6, 0x61, 0x60,
133
- 0x68, 0x100060, 0x100068, 0x5f, 0x15f, 0x1a, 0x22, 0x63,
134
- 0x67, 0x66, 0x100066, 0x200066, 0x300066, 0x10062,
135
- 0x400066, 0x500066, 0x800068, 0x61, 0x200060, 0x300060,
136
- 0x300068, 0x400068, 0x500068, 0x400060, 0x500060, 0x200068,
137
- 0x7, 0x38, 0x46, 0x54, 0x600060, 0x700060, 0x600068, 0x700068]
154
+ NO_HDR_ENTRY_TYPES = [0x4, 0xb, 0x21, 0x40, 0x70, 0x6, 0x61, 0x60, 0x68, 0x5f, 0x15f, 0x1a, 0x22, 0x63, 0x67,
155
+ 0x66, 0x62, 0x61, 0x7, 0x38, 0x46, 0x54]
138
156
 
139
157
  NO_SIZE_ENTRY_TYPES = [0xb]
140
158
 
@@ -152,6 +170,7 @@ class Entry(NestedBuffer):
152
170
  parent_directory,
153
171
  parent_buffer,
154
172
  type_,
173
+ type_flags,
155
174
  size,
156
175
  offset,
157
176
  blob,
@@ -164,12 +183,13 @@ class Entry(NestedBuffer):
164
183
  elif type_ in PUBKEY_ENTRY_TYPES:
165
184
  # Option 2: it's a PubkeyEntry
166
185
  try:
167
- new_entry = PubkeyEntry(parent_directory, parent_buffer, type_, size, offset, blob, psptool)
186
+ new_entry = PubkeyEntry(parent_directory, parent_buffer, type_, type_flags, size, offset, blob, psptool)
168
187
  except Exception as e:
169
188
  new_entry = Entry(
170
189
  parent_directory,
171
190
  parent_buffer,
172
191
  type_,
192
+ type_flags,
173
193
  size,
174
194
  offset,
175
195
  blob,
@@ -181,12 +201,13 @@ class Entry(NestedBuffer):
181
201
  elif type_ in Entry.KEY_STORE_TYPES:
182
202
  # Option 2: it's a KeyStoreEntry
183
203
  try:
184
- new_entry = KeyStoreEntry(parent_directory, parent_buffer, type_, size, offset, blob, psptool)
204
+ new_entry = KeyStoreEntry(parent_directory, parent_buffer, type_, type_flags, size, offset, blob, psptool)
185
205
  except:
186
206
  new_entry = Entry(
187
207
  parent_directory,
188
208
  parent_buffer,
189
209
  type_,
210
+ type_flags,
190
211
  size,
191
212
  offset,
192
213
  blob,
@@ -200,7 +221,7 @@ class Entry(NestedBuffer):
200
221
  # If the size in the directory is zero, set the size to hdr len
201
222
  size = HeaderEntry.HEADER_LEN
202
223
  try:
203
- new_entry = HeaderEntry(parent_directory, parent_buffer, type_, size, offset, blob, psptool)
224
+ new_entry = HeaderEntry(parent_directory, parent_buffer, type_, type_flags, size, offset, blob, psptool)
204
225
  if size == 0:
205
226
  psptool.ph.print_warning(f"Entry with zero size. Type: {type_}. Dir: 0x{offset:x}")
206
227
  except:
@@ -208,6 +229,7 @@ class Entry(NestedBuffer):
208
229
  parent_directory,
209
230
  parent_buffer,
210
231
  type_,
232
+ type_flags,
211
233
  size,
212
234
  offset,
213
235
  blob,
@@ -269,7 +291,7 @@ class Entry(NestedBuffer):
269
291
  # Set zlib_size
270
292
  blob[0x54:0x58] = zlib_size.to_bytes(4, 'little')
271
293
 
272
- entry = HeaderEntry(None, blob, id_, total_size, 0x0, blob, psptool)
294
+ entry = HeaderEntry(None, blob, id_, None, total_size, 0x0, blob, psptool)
273
295
 
274
296
  if signed:
275
297
  entry.signature[:] = private_key.sign(entry.get_signed_bytes())
@@ -278,7 +300,7 @@ class Entry(NestedBuffer):
278
300
  else:
279
301
  raise Entry.TypeError()
280
302
 
281
- def __init__(self, parent_directory, parent_buffer, type_, buffer_size, buffer_offset: int, blob, psptool,
303
+ def __init__(self, parent_directory, parent_buffer, type_, type_flags, buffer_size, buffer_offset: int, blob, psptool,
282
304
  destination: int = None):
283
305
  super().__init__(parent_buffer, buffer_size, buffer_offset=buffer_offset)
284
306
 
@@ -286,6 +308,7 @@ class Entry(NestedBuffer):
286
308
  self.blob = blob
287
309
  self.psptool = psptool
288
310
  self.type = type_
311
+ self.type_flags = type_flags
289
312
  self.destination = destination
290
313
  # todo: deduplicate Entry objects pointing to the same address (in `from_fields`?)
291
314
  self.references = [parent_directory] if parent_directory is not None else []
@@ -459,12 +482,13 @@ class KeyStoreEntryHeader(NestedBuffer):
459
482
  if self.has_sha256_checksum:
460
483
  self.sha256_checksum = NestedBuffer(self, 0x20, buffer_offset=0xd0)
461
484
 
485
+ # Based on KeyStore entries seen in: Lenovo X13 and Lenovo Ideapad 5 Pro 16ACH6
462
486
  zero_ranges = {
463
- (0x00, 0x10),
464
487
  (0x18, 0x18),
465
488
  (0x48, 0x04),
466
489
  (0x50, 0x08),
467
- (0x5c, 0x10),
490
+ (0x5c, 0x04),
491
+ (0x64, 0x08),
468
492
  (0x70, 0x0c),
469
493
  (0x80, 0x50),
470
494
  (0xf0, 0x10),
@@ -514,7 +538,7 @@ class KeyStoreEntryHeader(NestedBuffer):
514
538
  @property
515
539
  def has_sha256_checksum(self) -> bool:
516
540
  # assert self.sha256_checksum_flag_1 == self.sha256_checksum_flag_2
517
- assert self.sha256_checksum_flag_1 in {0, 1}
541
+ assert self.sha256_checksum_flag_1 in {0, 1, 2}, f"unknown {self.sha256_checksum_flag_1=}"
518
542
  return self.sha256_checksum_flag_1 == 1
519
543
 
520
544
 
@@ -649,7 +673,7 @@ class PubkeyEntry(Entry):
649
673
 
650
674
  # misc info
651
675
  self._version = NestedBuffer(self, 4)
652
- if self.version != 1:
676
+ if self.version not in {1, 2}:
653
677
  raise UnknownPubkeyEntryVersion
654
678
  self._key_usage = NestedBuffer(self, 4, 0x24)
655
679
 
@@ -657,6 +681,9 @@ class PubkeyEntry(Entry):
657
681
  self.key_id = KeyId(self, 0x10, 0x4)
658
682
  self.certifying_id = KeyId(self, 0x10, 0x14)
659
683
 
684
+ # security features
685
+ self._security_features = NestedBuffer(self, 2, 0x2A)
686
+
660
687
  # crypto material
661
688
  self._pubexp_bits = NestedBuffer(self, 4, 0x38)
662
689
  self._modulus_bits = NestedBuffer(self, 4, 0x3c)
@@ -716,6 +743,10 @@ class PubkeyEntry(Entry):
716
743
  def modulus(self) -> int:
717
744
  return int.from_bytes(self._modulus.get_bytes(), 'little')
718
745
 
746
+ @property
747
+ def security_features(self) -> int:
748
+ return int.from_bytes(self._security_features.get_bytes(), 'little')
749
+
719
750
  def get_signed_bytes(self):
720
751
  return self.get_bytes(0, self.buffer_size - self.signature_size)
721
752
 
@@ -729,6 +760,26 @@ class PubkeyEntry(Entry):
729
760
  def get_readable_version(self):
730
761
  return str(self.version)
731
762
 
763
+ def get_readable_key_usage(self):
764
+ if self.key_usage == 0:
765
+ return 'AMD_CODE_SIGN'
766
+ if self.key_usage == 1:
767
+ return 'BIOS_CODE_SIGN'
768
+ if self.key_usage == 2:
769
+ return 'AMD_AND_BIOS_CODE_SIGN'
770
+ if self.key_usage == 8:
771
+ return 'PLATFORM_SECURE_BOOT'
772
+ return f'unknown_key_usage({self.key_usage})'
773
+
774
+ def get_readable_security_features(self):
775
+ features = []
776
+ if self.security_features & 0b001:
777
+ features.append('DISABLE_BIOS_KEY_ANTI_ROLLBACK')
778
+ if self.security_features & 0b010:
779
+ features.append('DISABLE_AMD_BIOS_KEY_USE')
780
+ if self.security_features & 0b100:
781
+ features.append('DISABLE_SECURE_DEBUG_UNLOCK')
782
+ return ', '.join(features)
732
783
 
733
784
  class HeaderEntry(Entry):
734
785
 
@@ -890,14 +941,14 @@ class HeaderEntry(Entry):
890
941
  return self._sha256_checksum_flag_2 == 1
891
942
 
892
943
  def verify_sha256(self, print_warning=True) -> bool:
893
- if self._sha256_checksum.get_bytes() == sha256(self.get_decompressed_body()).digest():
944
+ if self._sha256_checksum.get_bytes() == sha256(self.get_decrypted_decompressed_body()).digest():
894
945
  return True
895
946
  if print_warning:
896
947
  self.psptool.ph.print_warning(f"Could not verify sha256 checksum for {self}")
897
948
  return False
898
949
 
899
950
  def update_sha256(self):
900
- self._sha256_checksum[:] = sha256(self.get_decompressed_body()).digest()
951
+ self._sha256_checksum[:] = sha256(self.get_decrypted_decompressed_body()).digest()
901
952
  self.verify_sha256()
902
953
 
903
954
  def get_readable_version(self):
@@ -929,30 +980,31 @@ class HeaderEntry(Entry):
929
980
  return readable_magic
930
981
 
931
982
  def get_readable_signed_by(self):
932
- return str(self.signature_fingerprint, encoding='ascii').upper()[:4]
983
+ return self.signed_entity.certifying_id.magic
933
984
 
934
985
  def get_signed_bytes(self) -> bytes:
935
- if self.compressed:
936
- full_decompressed = self.header.get_bytes() + self.get_decompressed_body()
937
- # Truncate to actually signed portion
938
- return full_decompressed[:self.header_len + self.size_signed]
939
- elif self.encrypted:
940
- return self.get_decrypted()[:self.size_signed + self.header_len]
941
- else:
942
- return self.get_bytes()[:self.size_signed + self.header_len]
986
+ entry_bytes = self.header.get_bytes() + self.get_decrypted_decompressed_body()
987
+ return entry_bytes[:self.header_len + self.size_signed]
943
988
 
944
- def get_decompressed_body(self) -> bytes:
945
- if not self.compressed:
946
- return self.body.get_bytes()
989
+ def get_decrypted_decompressed_body(self) -> bytes:
990
+ if self.encrypted:
991
+ output = self.get_decrypted_body()
947
992
  else:
993
+ output = self.body.get_bytes()
994
+ if self.compressed:
948
995
  try:
949
- return zlib_decompress(self.body.get_bytes()[:self.zlib_size])
996
+ return zlib_decompress(output[:self.zlib_size])
950
997
  except:
951
998
  self.psptool.ph.print_warning(f"ZLIB decompression failed on entry {self.get_readable_type()}")
952
- return self.body.get_bytes()
953
-
954
- def get_decrypted(self) -> bytes:
955
- return self.header.get_bytes() + self.get_decrypted_body()
999
+ return output
1000
+
1001
+ def to_decrypted_entry_bytes(self) -> bytes:
1002
+ """Returns the bytes of the same entry, just with the encryption removed"""
1003
+ header = bytearray(self.header.get_bytes())
1004
+ header[0x18:0x1c] = bytes(4)
1005
+ header[0x20:0x30] = bytes(0x10)
1006
+ signature = self.signature.get_bytes() if self.signed else b''
1007
+ return bytes(header) + self.get_decrypted_body() + signature
956
1008
 
957
1009
  def get_decrypted_body(self) -> bytes:
958
1010
  if not self.encrypted:
@@ -76,10 +76,23 @@ class Fet(NestedBuffer):
76
76
  return
77
77
  dir_ = Directory(self.rom, addr, type_, self.psptool, zen_generation)
78
78
  self.directories.append(dir_)
79
- if dir_.secondary_directory_address is not None:
80
- self.directories.append(
81
- Directory(self.rom, dir_.secondary_directory_address, 'secondary', self.psptool, zen_generation)
82
- )
79
+ for secondary_directory_address in dir_.secondary_directory_addresses:
80
+ secondary_directory_magic = self.rom.get_bytes(secondary_directory_address, 4)
81
+ if secondary_directory_magic in [b'*\rY/', b'j\x8d+1', b'&\r=/', b'd\x8d\x011']:
82
+ weird_new_directory_body = self.rom.get_bytes(secondary_directory_address+16, 8)
83
+ try:
84
+ secondary_dir = Directory(self.rom, int.from_bytes(weird_new_directory_body[:4], 'little'), 'tertiary', self.psptool)
85
+ except:
86
+ self.psptool.ph.print_warning(f"Couldn't create secondary directory at 0x{secondary_directory_address:x}")
87
+ self.directories.append(secondary_dir)
88
+ else:
89
+ secondary_dir = Directory(self.rom, secondary_directory_address, 'secondary', self.psptool, zen_generation)
90
+ self.directories.append(secondary_dir)
91
+
92
+ # Tertiary directories are a thing, apparently
93
+ for tertiary_directory_address in secondary_dir.secondary_directory_addresses:
94
+ tertiary_dir = Directory(self.rom, tertiary_directory_address, 'secondary', self.psptool, zen_generation)
95
+ self.directories.append(tertiary_dir)
83
96
 
84
97
  def _parse_entry_table(self):
85
98
  entries = self.get_chunks(4, 4)
@@ -109,8 +122,7 @@ class Fet(NestedBuffer):
109
122
 
110
123
  def _parse_combo_dir(self, dir_addr):
111
124
  results = []
112
- no_of_entries = int.from_bytes(self.rom[dir_addr + 8: dir_addr + 0xc],
113
- 'little')
125
+ no_of_entries = int.from_bytes(self.rom[dir_addr + 8: dir_addr + 0xc], 'little')
114
126
  combo_dir = self.rom[dir_addr: dir_addr + 16 * (no_of_entries + 2)]
115
127
 
116
128
  # Combo dir entries seem to begin at offset 0x20, make sure we don't