| #!/usr/bin/env python |
| # spdtool - Tool for partial deblobbing of UEFI firmware images |
| # SPDX-License-Identifier: GPL-3.0-or-later |
| # |
| # Parse a blob and search for SPD files. |
| # First it is searched for a possible SPD header. |
| # |
| # For each candidate the function verify_match is invoked to check |
| # additional fields (known bits, reserved bits, CRC, ...) |
| # |
| # Dumps the found SPDs into the current folder. |
| # |
| # Implemented: |
| # DDR4 SPDs |
| # |
| |
| import argparse |
| import crc16 |
| import struct |
| |
| |
| class Parser(object): |
| def __init__(self, blob, verbose=False, ignorecrc=False): |
| self.blob = blob |
| self.ignorecrc = ignorecrc |
| self.verbose = verbose |
| |
| @staticmethod |
| def get_matches(): |
| """Return the first byte to look for""" |
| raise Exception("Function not implemented") |
| |
| def verify_match(self, header, offset): |
| """Return true if it looks like a SPD""" |
| raise Exception("Function not implemented") |
| |
| def get_len(self, header, offset): |
| """Return the length of the SPD""" |
| raise Exception("Function not implemented") |
| |
| def get_part_number(self, offset): |
| """Return the part number in SPD""" |
| return "" |
| |
| def get_manufacturer_id(self, offset): |
| """Return the manufacturer ID in SPD""" |
| return 0xffff |
| |
| def get_mtransfers(self, offset): |
| """Return the number of MT/s""" |
| return 0 |
| |
| def get_manufacturer(self, offset): |
| """Return manufacturer as string""" |
| id = self.get_manufacturer_id(offset) |
| if id == 0xffff: |
| return "Unknown" |
| ids = { |
| 0x2c80: "Crucial/Micron", |
| 0x4304: "Ramaxel", |
| 0x4f01: "Transcend", |
| 0x9801: "Kingston", |
| 0x987f: "Hynix", |
| 0x9e02: "Corsair", |
| 0xb004: "OCZ", |
| 0xad80: "Hynix/Hyundai", |
| 0xb502: "SuperTalent", |
| 0xcd04: "GSkill", |
| 0xce80: "Samsung", |
| 0xfe02: "Elpida", |
| 0xff2c: "Micron", |
| } |
| if id in ids: |
| return ids[id] |
| return "Unknown" |
| |
| def blob_as_ord(self, offset): |
| """Helper for python2/python3 compatibility""" |
| return self.blob[offset] if type(self.blob[offset]) is int \ |
| else ord(self.blob[offset]) |
| |
| def search(self, start): |
| """Search for SPD at start. Returns -1 on error or offset |
| if found. |
| """ |
| for i in self.get_matches(): |
| for offset in range(start, len(self.blob)): |
| if self.blob_as_ord(offset) == i and \ |
| self.verify_match(i, offset): |
| return offset, self.get_len(i, offset) |
| return -1, 0 |
| |
| |
| class SPD4Parser(Parser): |
| @staticmethod |
| def get_matches(): |
| """Return DDR4 possible header candidates""" |
| ret = [] |
| for i in [1, 2, 3, 4]: |
| for j in [1, 2]: |
| ret.append(i + j * 16) |
| return ret |
| |
| def verify_match(self, header, offset): |
| """Verify DDR4 specific bit fields.""" |
| # offset 0 is a candidate, no need to validate |
| if self.blob_as_ord(offset + 1) == 0xff: |
| return False |
| if self.blob_as_ord(offset + 2) != 0x0c: |
| return False |
| if self.blob_as_ord(offset + 5) & 0xc0 > 0: |
| return False |
| if self.blob_as_ord(offset + 6) & 0xc > 0: |
| return False |
| if self.blob_as_ord(offset + 7) & 0xc0 > 0: |
| return False |
| if self.blob_as_ord(offset + 8) != 0: |
| return False |
| if self.blob_as_ord(offset + 9) & 0xf > 0: |
| return False |
| if self.verbose: |
| print("%x: Looks like DDR4 SPD" % offset) |
| |
| crc = crc16.crc16xmodem(self.blob[offset:offset + 0x7d + 1]) |
| # Vendors ignore the endianness... |
| crc_spd1 = self.blob_as_ord(offset + 0x7f) |
| crc_spd1 |= (self.blob_as_ord(offset + 0x7e) << 8) |
| crc_spd2 = self.blob_as_ord(offset + 0x7e) |
| crc_spd2 |= (self.blob_as_ord(offset + 0x7f) << 8) |
| if crc != crc_spd1 and crc != crc_spd2: |
| if self.verbose: |
| print("%x: CRC16 doesn't match" % offset) |
| if not self.ignorecrc: |
| return False |
| |
| return True |
| |
| def get_len(self, header, offset): |
| """Return the length of the SPD found.""" |
| if (header >> 4) & 7 == 1: |
| return 256 |
| if (header >> 4) & 7 == 2: |
| return 512 |
| return 0 |
| |
| def get_part_number(self, offset): |
| """Return part number as string""" |
| if offset + 0x15c >= len(self.blob): |
| return "" |
| tmp = self.blob[offset + 0x149:offset + 0x15c + 1] |
| return tmp.decode('utf-8').rstrip() |
| |
| def get_manufacturer_id(self, offset): |
| """Return manufacturer ID""" |
| if offset + 0x141 >= len(self.blob): |
| return 0xffff |
| tmp = self.blob[offset + 0x140:offset + 0x141 + 1] |
| return struct.unpack('H', tmp)[0] |
| |
| def get_mtransfers(self, offset): |
| """Return MT/s as specified by MTB and FTB""" |
| if offset + 0x7d >= len(self.blob): |
| return 0 |
| |
| if self.blob_as_ord(offset + 0x11) != 0: |
| return 0 |
| mtb = 8.0 |
| ftb = 1000.0 |
| tmp = self.blob[offset + 0x12:offset + 0x12 + 1] |
| tckm = struct.unpack('B', tmp)[0] |
| tmp = self.blob[offset + 0x7d:offset + 0x7d + 1] |
| tckf = struct.unpack('b', tmp)[0] |
| return int(2000 / (tckm / mtb + tckf / ftb)) |
| |
| |
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description='SPD rom dumper') |
| parser.add_argument('--blob', required=True, |
| help='The ROM to search SPDs in.') |
| parser.add_argument('--spd4', action='store_true', default=False, |
| help='Search for DDR4 SPDs.') |
| parser.add_argument('--hex', action='store_true', default=False, |
| help='Store SPD in hex format otherwise binary.') |
| parser.add_argument('-v', '--verbose', help='increase output verbosity', |
| action='store_true') |
| parser.add_argument('--ignorecrc', help='Ignore CRC mismatch', |
| action='store_true', default=False) |
| args = parser.parse_args() |
| |
| blob = open(args.blob, "rb").read() |
| |
| if args.spd4: |
| p = SPD4Parser(blob, args.verbose, args.ignorecrc) |
| else: |
| raise Exception("Must specify one of the following arguments:\n--spd4") |
| |
| offset = 0 |
| cnt = 0 |
| while True: |
| offset, length = p.search(offset) |
| if length == 0: |
| break |
| print("Found SPD at 0x%x" % offset) |
| print(" '%s', size %d, manufacturer %s (0x%04x) %d MT/s\n" % |
| (p.get_part_number(offset), length, p.get_manufacturer(offset), |
| p.get_manufacturer_id(offset), p.get_mtransfers(offset))) |
| filename = "spd-%d-%s-%s.bin" % (cnt, p.get_part_number(offset), |
| p.get_manufacturer(offset)) |
| filename = filename.replace("/", "_") |
| filename = "".join([c for c in filename if c.isalpha() or c.isdigit() |
| or c == '-' or c == '.' or c == '_']).rstrip() |
| if not args.hex: |
| open(filename, "wb").write(blob[offset:offset + length]) |
| else: |
| filename += ".hex" |
| with open(filename, "w") as fn: |
| j = 0 |
| for i in blob[offset:offset + length]: |
| fn.write("%02X" % struct.unpack('B', i)[0]) |
| fn.write(" " if j < 15 else "\n") |
| j = (j + 1) % 16 |
| offset += 1 |
| cnt += 1 |