psptool 3.1__tar.gz → 3.2.dev0__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 (45) hide show
  1. psptool-3.2.dev0/.github/workflows/gha-metrics.yml +33 -0
  2. psptool-3.2.dev0/.github/workflows/publish.yml +74 -0
  3. {psptool-3.1 → psptool-3.2.dev0}/PKG-INFO +7 -12
  4. {psptool-3.1 → psptool-3.2.dev0}/psptool/__main__.py +5 -0
  5. {psptool-3.1 → psptool-3.2.dev0}/psptool/blob.py +5 -1
  6. {psptool-3.1 → psptool-3.2.dev0}/psptool/directory.py +18 -16
  7. psptool-3.2.dev0/psptool/entry.py +180 -0
  8. {psptool-3.1 → psptool-3.2.dev0}/psptool/fet.py +16 -3
  9. {psptool-3.1 → psptool-3.2.dev0}/psptool/file.py +162 -42
  10. psptool-3.2.dev0/psptool/microcode_file.py +60 -0
  11. {psptool-3.1 → psptool-3.2.dev0}/psptool/psptool.py +30 -3
  12. psptool-3.2.dev0/pyproject.toml +21 -0
  13. psptool-3.2.dev0/tests/gha_metrics.py +80 -0
  14. {psptool-3.1 → psptool-3.2.dev0}/tests/metrics.txt +70 -70
  15. psptool-3.1/psptool/entry.py +0 -96
  16. psptool-3.1/psptool.egg-info/PKG-INFO +0 -877
  17. psptool-3.1/psptool.egg-info/SOURCES.txt +0 -38
  18. psptool-3.1/psptool.egg-info/dependency_links.txt +0 -1
  19. psptool-3.1/psptool.egg-info/entry_points.txt +0 -2
  20. psptool-3.1/psptool.egg-info/requires.txt +0 -2
  21. psptool-3.1/psptool.egg-info/top_level.txt +0 -1
  22. psptool-3.1/setup.cfg +0 -26
  23. psptool-3.1/setup.py +0 -13
  24. {psptool-3.1 → psptool-3.2.dev0}/.github/workflows/python-tests.yml +0 -0
  25. {psptool-3.1 → psptool-3.2.dev0}/.gitignore +0 -0
  26. {psptool-3.1 → psptool-3.2.dev0}/.gitmodules +0 -0
  27. {psptool-3.1 → psptool-3.2.dev0}/LICENSE +0 -0
  28. {psptool-3.1 → psptool-3.2.dev0}/README.md +0 -0
  29. {psptool-3.1 → psptool-3.2.dev0}/psptool/__init__.py +0 -0
  30. {psptool-3.1 → psptool-3.2.dev0}/psptool/cert_tree.py +0 -0
  31. {psptool-3.1 → psptool-3.2.dev0}/psptool/crypto.py +0 -0
  32. {psptool-3.1 → psptool-3.2.dev0}/psptool/errors.py +0 -0
  33. {psptool-3.1 → psptool-3.2.dev0}/psptool/firmware.py +0 -0
  34. {psptool-3.1 → psptool-3.2.dev0}/psptool/header_file.py +0 -0
  35. {psptool-3.1 → psptool-3.2.dev0}/psptool/key_store_file.py +0 -0
  36. {psptool-3.1 → psptool-3.2.dev0}/psptool/pubkey_file.py +0 -0
  37. {psptool-3.1 → psptool-3.2.dev0}/psptool/rom.py +0 -0
  38. {psptool-3.1 → psptool-3.2.dev0}/psptool/utils.py +0 -0
  39. {psptool-3.1 → psptool-3.2.dev0}/tests/__init__.py +0 -0
  40. {psptool-3.1 → psptool-3.2.dev0}/tests/create_metrics.sh +0 -0
  41. {psptool-3.1 → psptool-3.2.dev0}/tests/integration/__init__.py +0 -0
  42. {psptool-3.1 → psptool-3.2.dev0}/tests/integration/test_psptrace.py +0 -0
  43. {psptool-3.1 → psptool-3.2.dev0}/tests/integration/test_rom_files.py +0 -0
  44. {psptool-3.1 → psptool-3.2.dev0}/tests/unit/__init__.py +0 -0
  45. {psptool-3.1 → psptool-3.2.dev0}/tests/unit/test_cli.py +0 -0
@@ -0,0 +1,33 @@
1
+ name: Post metrics comment
2
+
3
+ on: [pull_request_target]
4
+
5
+ jobs:
6
+ test:
7
+
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python-version: ["3.9"]
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ with:
16
+ submodules: recursive
17
+ ssh-key: ${{ secrets.PSPTOOL_FIXTURES_NEW_PRIVATE_KEY }}
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v3
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install .
26
+ - name: Run metrics and post comment
27
+ run: |
28
+ python tests/gha_metrics.py > metrics.md
29
+ gh pr comment $PRNUM --body-file metrics.md
30
+ env:
31
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32
+ GH_REPO: ${{ github.repository }}
33
+ PRNUM: ${{ github.event.pull_request.number }}
@@ -0,0 +1,74 @@
1
+ name: Publish Python distribution to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ persist-credentials: false
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.11"
18
+ - name: Install pypa/build
19
+ run: >-
20
+ python3 -m
21
+ pip install
22
+ build
23
+ --user
24
+ - name: Build a binary wheel and a source tarball
25
+ run: python3 -m build
26
+ - name: Store the distribution packages
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: python-package-distributions
30
+ path: dist/
31
+
32
+ publish-to-pypi:
33
+ name: >-
34
+ Publish Python distribution to PyPI
35
+ if: startsWith(github.ref, 'refs/tags') # only publish on tag push
36
+ needs:
37
+ - build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/p/psptool/
42
+ permissions:
43
+ id-token: write # important for trusted publishing
44
+ steps:
45
+ - name: Download all the dists
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: python-package-distributions
49
+ path: dist/
50
+ - name: Publish distribution to PyPI
51
+ uses: pypa/gh-action-pypi-publish@release/v1
52
+
53
+ publish-to-testpypi:
54
+ name: >-
55
+ Publish Python distribution to TestPyPI
56
+ needs:
57
+ - build
58
+ runs-on: ubuntu-latest
59
+ environment:
60
+ name: testpypi
61
+ url: https://test.pypi.org/p/psptool/
62
+ permissions:
63
+ id-token: write # important for trusted publishing
64
+ steps:
65
+ - name: Download all the dists
66
+ uses: actions/download-artifact@v4
67
+ with:
68
+ name: python-package-distributions
69
+ path: dist/
70
+ - name: Publish distribution to TestPyPI
71
+ uses: pypa/gh-action-pypi-publish@release/v1
72
+ with:
73
+ repository-url: https://test.pypi.org/legacy/
74
+ verbose: true
@@ -1,18 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: psptool
3
- Version: 3.1
3
+ Version: 3.2.dev0
4
4
  Summary: PSPTool is a tool for dealing with AMD binary blobs
5
- Home-page: https://github.com/PSPReverse/PSPTool
6
- Author: Christian Werling
7
- Author-email: cwerling@posteo.de
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Intended Audience :: Science/Research
10
- Classifier: Topic :: Security
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
- Classifier: Programming Language :: Python :: 3.8
5
+ Author-email: Christian Werling <cwerling@posteo.de>
6
+ License-File: LICENSE
13
7
  Requires-Python: >=3.8
8
+ Requires-Dist: cryptography>=38.0.0
9
+ Requires-Dist: prettytable
14
10
  Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
11
 
17
12
  # PSPTool
18
13
 
@@ -874,4 +869,4 @@ b'\x01\x00\x00\x00\x1b\xb9\x87\xc3YIF\x06\xb1t\x94V\x01\xc9\xea[\x1b\xb9\x87\xc3
874
869
  > psp.blob.roms[0].directories[0].files[1].move_buffer(0x60000, 0x1000)
875
870
  > psp.blob.roms[0].set_bytes(0x60000, 0x1000, my_stuff)
876
871
  > psp.to_file('my_modified_bios.bin')
877
- ```
872
+ ```
@@ -200,6 +200,11 @@ def main():
200
200
  out_bytes = file.get_bytes()
201
201
 
202
202
  outpath = outdir + '/d%.2d_e%.2d_%s' % (dir_index, file_index, file.get_readable_type())
203
+
204
+ # We may have same file types but different subprograms, instances
205
+ # Account for it in the filenames
206
+ if file.entry.subprogram != 0 or file.entry.instance != 0:
207
+ outpath += f'_SUB_{hex(file.entry.subprogram)}_INS_{hex(file.entry.instance)}'
203
208
  if type(file) is HeaderFile:
204
209
  outpath += f'_{file.get_readable_version()}'
205
210
 
@@ -67,7 +67,11 @@ class Blob(NestedBuffer):
67
67
  continue
68
68
  try:
69
69
  rom_offset = fet_location - fet_offset # e.g. 0x20800 - 0x20000 = 0x0800
70
- potential_rom = Rom(self, min(rom_size, self._MAX_PAGE_SIZE), rom_offset, fet_offset, psptool)
70
+ # W/A for images with single ROMs that have entries crossing 16MB boundary
71
+ if rom_page == 0:
72
+ potential_rom = Rom(self, rom_size, rom_offset, fet_offset, psptool)
73
+ else:
74
+ potential_rom = Rom(self, min(rom_size, self._MAX_PAGE_SIZE), rom_offset, fet_offset, psptool)
71
75
  self.roms.append(potential_rom)
72
76
  fet_parsed = True
73
77
  break # found correct fet_offset!
@@ -31,10 +31,6 @@ class Directory(NestedBuffer):
31
31
  ENTRY_SIZE = DirectoryEntry.ENTRY_SIZE
32
32
  FILE_CLASS = File
33
33
 
34
- # all directories by offset in rom
35
- # todo: what if we have multi-ROM? then two ROMs share this singleton!
36
- directories_by_offset = {}
37
-
38
34
  class ParseError(Exception):
39
35
  pass
40
36
 
@@ -45,14 +41,15 @@ class Directory(NestedBuffer):
45
41
  @classmethod
46
42
  def create_directories_if_not_exist(cls, offset, fet, zen_generation=None) -> List['Directory']:
47
43
  # Recursively return or create and return found directories
48
- if offset in cls.directories_by_offset:
49
- return [cls.directories_by_offset[offset]]
44
+
45
+ if offset in fet.psptool.directories_by_offset:
46
+ return [fet.psptool.directories_by_offset[offset]]
50
47
  else:
51
48
  # 1. Create the immediate directory in front of us
52
49
  created_directories = []
53
50
  try:
54
51
  directory = cls.from_offset(fet, offset, zen_generation)
55
- cls.directories_by_offset[offset] = directory
52
+ fet.psptool.directories_by_offset[offset] = directory
56
53
  created_directories.append(directory)
57
54
  except Directory.ParseError as e:
58
55
  # Handle empty entries gracefully (like master branch)
@@ -70,8 +67,8 @@ class Directory(NestedBuffer):
70
67
 
71
68
  # 3. Recursively add tertiary directories (double references introduced in Zen 4), if applicable
72
69
  for tertiary_directory_offset in directory.tertiary_directory_offsets:
73
- directory_body = fet.rom.get_bytes(tertiary_directory_offset + 16, 8)
74
- actual_tertiary_offset = int.from_bytes(directory_body[:4], 'little')
70
+ directory_body = fet.rom.get_bytes(tertiary_directory_offset, 32)
71
+ actual_tertiary_offset = int.from_bytes(directory_body[16:20], 'little')
75
72
  # Resolve one more indirection
76
73
  tertiary_directories = cls.create_directories_if_not_exist(actual_tertiary_offset, fet, zen_generation)
77
74
  created_directories += tertiary_directories
@@ -83,7 +80,7 @@ class Directory(NestedBuffer):
83
80
  rom_offset &= fet.rom.addr_mask
84
81
  magic = fet.rom.get_bytes(rom_offset, 4)
85
82
 
86
- if magic == b'\xff\xff\xff\xff':
83
+ if magic == b'\xff\xff\xff\xff' or magic == b'\x00\x00\x00\x00':
87
84
  fet.psptool.ph.print_warning(f"Empty FET entry at ROM address 0x{rom_offset:x}")
88
85
  raise Directory.ParseError("Empty entry")
89
86
  if magic in cls.DIRECTORY_MAGICS:
@@ -106,7 +103,7 @@ class Directory(NestedBuffer):
106
103
  self._count = int.from_bytes(self.rom[self.buffer_offset + 8: self.buffer_offset + 12], 'little')
107
104
  self.magic = self.rom.get_bytes(self.buffer_offset, 4)
108
105
  assert self.magic in self.DIRECTORY_MAGICS
109
- self.reserved = self.rom.get_bytes(self.buffer_offset + 12, 4)
106
+ self.additional_info = self.rom.get_bytes(self.buffer_offset + 12, 4)
110
107
  self.header = NestedBuffer(self, self.HEADER_SIZE)
111
108
  self.body = NestedBuffer(self, self.ENTRY_SIZE * self.count, buffer_offset=self.HEADER_SIZE)
112
109
  self.buffer_size = len(self.header) + len(self.body)
@@ -157,13 +154,18 @@ class Directory(NestedBuffer):
157
154
  self.header[8:12] = struct.pack('<I', self.count)
158
155
  self.update_checksum()
159
156
 
157
+ # 00b: x86 Physical address
158
+ # 01b: Offset from start of the BIOS (flash offset)
159
+ # 10b: Offset from start of directory header
160
+ # 11b: Offset from start of partition
160
161
  @property
161
162
  def address_mode(self):
162
- rsvd = struct.unpack('=L', self.reserved)[0]
163
- return (rsvd & 0x60000000) >> 29 if rsvd != 0 else None
164
- # todo: do we need to still consider the address_mode somewhere?
165
- # at the moment we seem to be running fine by checking entry.rsv0 & (1 << 30) to figure out if we need
166
- # directory-relative or absolute addressing
163
+ info = struct.unpack('<L', self.additional_info)[0]
164
+ version = (info >> 31) & 1
165
+ if version == 1:
166
+ return (info >> 24) & 3
167
+ else:
168
+ return (info >> 29) & 3
167
169
 
168
170
  def verify_checksum(self):
169
171
  data = self[0x8:]
@@ -0,0 +1,180 @@
1
+ # PSPTool - Display, extract and manipulate PSP firmware inside UEFI images
2
+ # Copyright (C) 2021 Christian Werling, Robert Buhren, Hans Niklas Jacob
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ import struct
18
+
19
+ from typing import TYPE_CHECKING
20
+ if TYPE_CHECKING:
21
+ from directory import Directory
22
+
23
+ from .utils import NestedBuffer
24
+
25
+
26
+ class DirectoryEntry(NestedBuffer):
27
+ ENTRY_SIZE = 4 * 4
28
+
29
+ def __init__(self, parent_directory: 'Directory', entry_offset):
30
+ super().__init__(parent_directory.body, self.ENTRY_SIZE, entry_offset)
31
+ self.parent_directory = parent_directory
32
+ self.entry_offset = entry_offset
33
+
34
+ def __repr__(self):
35
+ return f'{self.__class__.__name__}({self.type=:#x}, {self.flags=:#x}, {self.size=:#x}, ' \
36
+ f'{self.offset=:#x}, {self.entry_offset=:#x}, {self.rsv0=:#x})'
37
+
38
+ def file_offset(self):
39
+ # Special case soft fuse chain, no offset or size
40
+ if self.type == 0xb:
41
+ dir_start = self.parent_directory.buffer_offset + self.parent_directory.HEADER_SIZE
42
+ return dir_start + self.entry_offset
43
+
44
+ addr_mode = self.parent_directory.address_mode
45
+ # If directory address mode is 2 or 3 (relative to dir or slot), the
46
+ # entry address mode must be taken into account, otherwise ignored.
47
+ if addr_mode == 2 or addr_mode == 3:
48
+ addr_mode = self.address_mode
49
+
50
+ if addr_mode == 0:
51
+ # x86 physical address, should be in range 0xff000000 - 0xffffffff
52
+ # But some images use flash offset in x86 physical address mode.
53
+ # If ROM is bigger than 16MB and the entry address is in the
54
+ # 0xff000000 - 0xffffffff range, we have to override the mask for
55
+ # physical address.
56
+ rom_size = self.parent_directory.rom.addr_mask + 1
57
+ if self.offset > 0xFF000000 and rom_size > 16 * 1024 * 1024:
58
+ return self.offset & 0x00FFFFFF
59
+ else:
60
+ return self.offset & self.parent_directory.rom.addr_mask
61
+ elif addr_mode == 1:
62
+ # Flash offset from start of BIOS, most common on modern systems
63
+ return self.offset
64
+ elif addr_mode == 2:
65
+ # Flash offset from start of directory header
66
+ return self.parent_directory.buffer_offset + self.offset
67
+ elif addr_mode == 3:
68
+ # Flash offset from start of the slot
69
+ # TODO: How to calculate the offset from slot? Is this correct?
70
+ return self.parent_directory.buffer_offset + self.offset
71
+
72
+ @property
73
+ def type(self):
74
+ return struct.unpack('<B', self[0:1])[0]
75
+
76
+ @type.setter
77
+ def type(self, value):
78
+ self.set_bytes(0, 1, struct.pack('<B', value))
79
+
80
+ @property
81
+ def subprogram(self):
82
+ return struct.unpack('<B', self[1:2])[0]
83
+
84
+ @subprogram.setter
85
+ def subprogram(self, value):
86
+ self.set_bytes(1, 1, struct.pack('<B', value))
87
+
88
+ @property
89
+ def flags(self):
90
+ return struct.unpack('<H', self[2:4])[0]
91
+
92
+ @flags.setter
93
+ def flags(self, value):
94
+ self.set_bytes(2, 2, struct.pack('<H', value))
95
+
96
+ @property
97
+ def instance(self):
98
+ return (self.flags >> 3) & 0xF
99
+
100
+ @instance.setter
101
+ def instance(self, value):
102
+ self.flags = self.flags | ((value & 0xF) << 3)
103
+
104
+ @property
105
+ def size(self):
106
+ return struct.unpack('<I', self[4:8])[0]
107
+
108
+ @size.setter
109
+ def size(self, value):
110
+ self.set_bytes(4, 4, struct.pack('<I', value))
111
+
112
+ @property
113
+ def offset(self):
114
+ return struct.unpack('<I', self[8:12])[0]
115
+
116
+ @offset.setter
117
+ def offset(self, value):
118
+ self.set_bytes(8, 4, struct.pack('<I', value))
119
+
120
+ @property
121
+ def rsv0(self):
122
+ return struct.unpack('<I', self[12:16])[0]
123
+
124
+ @rsv0.setter
125
+ def rsv0(self, value):
126
+ self.set_bytes(12, 4, struct.pack('<I', value))
127
+
128
+ # 00b: x86 Physical address
129
+ # 01b: Offset from start of the BIOS (flash offset)
130
+ # 10b: Offset from start of directory header
131
+ # 11b: Offset from start of partition
132
+ @property
133
+ def address_mode(self):
134
+ return (self.rsv0 >> 30) & 3
135
+
136
+ class BiosDirectoryEntry(DirectoryEntry):
137
+ ENTRY_SIZE = 4 * 6
138
+
139
+ def __repr__(self):
140
+ return super().__repr__()[:-1] + f', {self.destination=:#x})'
141
+
142
+ @property
143
+ def destination(self):
144
+ return struct.unpack('<Q', self[16:24])[0]
145
+
146
+ @destination.setter
147
+ def destination(self, value):
148
+ self.set_bytes(16, 8, struct.pack('<Q', value))
149
+
150
+ @property
151
+ def region_type(self):
152
+ return struct.unpack('<B', self[1:2])[0]
153
+
154
+ @region_type.setter
155
+ def region_type(self, value):
156
+ self.set_bytes(1, 1, struct.pack('<B', value))
157
+
158
+ @property
159
+ def flags(self):
160
+ return struct.unpack('<H', self[2:4])[0]
161
+
162
+ @flags.setter
163
+ def flags(self, value):
164
+ self.set_bytes(2, 2, struct.pack('<H', value))
165
+
166
+ @property
167
+ def subprogram(self):
168
+ return (self.flags >> 8) & 0x7
169
+
170
+ @subprogram.setter
171
+ def subprogram(self, value):
172
+ self.flags = self.flags | ((value & 0x7) << 8)
173
+
174
+ @property
175
+ def instance(self):
176
+ return (self.flags >> 4) & 0xF
177
+
178
+ @instance.setter
179
+ def instance(self, value):
180
+ self.flags = self.flags | ((value & 0xF) << 4)
@@ -69,10 +69,16 @@ class Fet(NestedBuffer):
69
69
  # TODO: Why is 0xFFFFFFFe a possible value here?
70
70
  if rom_addr in [0x0, 0xFFFFFFFF, 0xFFFFFFFe]:
71
71
  continue
72
- rom_addr &= self.rom.addr_mask
72
+ # if ROM is bigger than 16MB, we have to override the mask for
73
+ # physical address
74
+ if rom_addr > 0xFF000000 and self.rom.addr_mask + 1 > 16 * 1024 * 1024:
75
+ rom_addr &= 0x00FFFFFF
76
+ else:
77
+ rom_addr &= self.rom.addr_mask
78
+
73
79
  try:
74
80
  dir_magic = self.rom[rom_addr:rom_addr + 4]
75
- except:
81
+ except AssertionError as e:
76
82
  self.psptool.ph.print_warning(f"FET entry 0x{rom_addr:x} not found or invalid, skipping ...")
77
83
  continue
78
84
  if dir_magic == b'2PSP' or dir_magic == b'2BHD':
@@ -105,7 +111,14 @@ class Fet(NestedBuffer):
105
111
  entry_addr = int.from_bytes(entry, 'little')
106
112
  if entry_addr in [0, 0xFFFFFFFF]:
107
113
  continue
108
- entry_addr &= self.rom.addr_mask
114
+ # if ROM is bigger than 16MB, we have to override the mask for
115
+ # physical address
116
+ rom_size = self.rom.addr_mask + 1
117
+ if entry_addr > 0xFF000000 and rom_size > 16 * 1024 * 1024:
118
+ entry_addr &= 0x00FFFFFF
119
+ else:
120
+ entry_addr &= self.rom.addr_mask
121
+
109
122
  # entry_addr += self.blob_offset
110
123
  zen_generation_id = combo_dir[i*16+5:i*16+8]
111
124
  zen_generation = 'unknown'