Patrick Rudolph | 9b545df | 2019-02-12 11:52:41 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python |
Patrick Rudolph | 9b545df | 2019-02-12 11:52:41 +0100 | [diff] [blame] | 2 | # spdtool - Tool for partial deblobbing of UEFI firmware images |
Patrick Georgi | 1afe286 | 2020-05-10 17:34:15 +0200 | [diff] [blame] | 3 | # SPDX-License-Identifier: GPL-3.0-or-later |
Patrick Rudolph | 9b545df | 2019-02-12 11:52:41 +0100 | [diff] [blame] | 4 | # |
| 5 | # Parse a blob and search for SPD files. |
| 6 | # First it is searched for a possible SPD header. |
| 7 | # |
| 8 | # For each candidate the function verify_match is invoked to check |
| 9 | # additional fields (known bits, reserved bits, CRC, ...) |
| 10 | # |
| 11 | # Dumps the found SPDs into the current folder. |
| 12 | # |
| 13 | # Implemented: |
| 14 | # DDR4 SPDs |
| 15 | # |
| 16 | |
| 17 | import argparse |
| 18 | import crc16 |
| 19 | import struct |
| 20 | |
| 21 | |
| 22 | class Parser(object): |
| 23 | def __init__(self, blob, verbose=False, ignorecrc=False): |
| 24 | self.blob = blob |
| 25 | self.ignorecrc = ignorecrc |
| 26 | self.verbose = verbose |
| 27 | |
| 28 | @staticmethod |
| 29 | def get_matches(): |
| 30 | """Return the first byte to look for""" |
| 31 | raise Exception("Function not implemented") |
| 32 | |
| 33 | def verify_match(self, header, offset): |
| 34 | """Return true if it looks like a SPD""" |
| 35 | raise Exception("Function not implemented") |
| 36 | |
| 37 | def get_len(self, header, offset): |
| 38 | """Return the length of the SPD""" |
| 39 | raise Exception("Function not implemented") |
| 40 | |
| 41 | def get_part_number(self, offset): |
| 42 | """Return the part number in SPD""" |
| 43 | return "" |
| 44 | |
| 45 | def get_manufacturer_id(self, offset): |
| 46 | """Return the manufacturer ID in SPD""" |
| 47 | return 0xffff |
| 48 | |
| 49 | def get_mtransfers(self, offset): |
| 50 | """Return the number of MT/s""" |
| 51 | return 0 |
| 52 | |
| 53 | def get_manufacturer(self, offset): |
| 54 | """Return manufacturer as string""" |
| 55 | id = self.get_manufacturer_id(offset) |
| 56 | if id == 0xffff: |
| 57 | return "Unknown" |
| 58 | ids = { |
| 59 | 0x2c80: "Crucial/Micron", |
| 60 | 0x4304: "Ramaxel", |
| 61 | 0x4f01: "Transcend", |
| 62 | 0x9801: "Kingston", |
| 63 | 0x987f: "Hynix", |
| 64 | 0x9e02: "Corsair", |
| 65 | 0xb004: "OCZ", |
| 66 | 0xad80: "Hynix/Hyundai", |
| 67 | 0xb502: "SuperTalent", |
| 68 | 0xcd04: "GSkill", |
| 69 | 0xce80: "Samsung", |
| 70 | 0xfe02: "Elpida", |
| 71 | 0xff2c: "Micron", |
| 72 | } |
| 73 | if id in ids: |
| 74 | return ids[id] |
| 75 | return "Unknown" |
| 76 | |
| 77 | def blob_as_ord(self, offset): |
| 78 | """Helper for python2/python3 compatibility""" |
| 79 | return self.blob[offset] if type(self.blob[offset]) is int \ |
| 80 | else ord(self.blob[offset]) |
| 81 | |
| 82 | def search(self, start): |
| 83 | """Search for SPD at start. Returns -1 on error or offset |
| 84 | if found. |
| 85 | """ |
| 86 | for i in self.get_matches(): |
| 87 | for offset in range(start, len(self.blob)): |
| 88 | if self.blob_as_ord(offset) == i and \ |
| 89 | self.verify_match(i, offset): |
| 90 | return offset, self.get_len(i, offset) |
| 91 | return -1, 0 |
| 92 | |
| 93 | |
| 94 | class SPD4Parser(Parser): |
| 95 | @staticmethod |
| 96 | def get_matches(): |
| 97 | """Return DDR4 possible header candidates""" |
| 98 | ret = [] |
| 99 | for i in [1, 2, 3, 4]: |
| 100 | for j in [1, 2]: |
| 101 | ret.append(i + j * 16) |
| 102 | return ret |
| 103 | |
| 104 | def verify_match(self, header, offset): |
| 105 | """Verify DDR4 specific bit fields.""" |
| 106 | # offset 0 is a candidate, no need to validate |
| 107 | if self.blob_as_ord(offset + 1) == 0xff: |
| 108 | return False |
| 109 | if self.blob_as_ord(offset + 2) != 0x0c: |
| 110 | return False |
| 111 | if self.blob_as_ord(offset + 5) & 0xc0 > 0: |
| 112 | return False |
| 113 | if self.blob_as_ord(offset + 6) & 0xc > 0: |
| 114 | return False |
| 115 | if self.blob_as_ord(offset + 7) & 0xc0 > 0: |
| 116 | return False |
| 117 | if self.blob_as_ord(offset + 8) != 0: |
| 118 | return False |
| 119 | if self.blob_as_ord(offset + 9) & 0xf > 0: |
| 120 | return False |
| 121 | if self.verbose: |
| 122 | print("%x: Looks like DDR4 SPD" % offset) |
| 123 | |
| 124 | crc = crc16.crc16xmodem(self.blob[offset:offset + 0x7d + 1]) |
| 125 | # Vendors ignore the endianness... |
| 126 | crc_spd1 = self.blob_as_ord(offset + 0x7f) |
| 127 | crc_spd1 |= (self.blob_as_ord(offset + 0x7e) << 8) |
| 128 | crc_spd2 = self.blob_as_ord(offset + 0x7e) |
| 129 | crc_spd2 |= (self.blob_as_ord(offset + 0x7f) << 8) |
| 130 | if crc != crc_spd1 and crc != crc_spd2: |
| 131 | if self.verbose: |
| 132 | print("%x: CRC16 doesn't match" % offset) |
| 133 | if not self.ignorecrc: |
| 134 | return False |
| 135 | |
| 136 | return True |
| 137 | |
| 138 | def get_len(self, header, offset): |
| 139 | """Return the length of the SPD found.""" |
| 140 | if (header >> 4) & 7 == 1: |
| 141 | return 256 |
| 142 | if (header >> 4) & 7 == 2: |
| 143 | return 512 |
| 144 | return 0 |
| 145 | |
| 146 | def get_part_number(self, offset): |
| 147 | """Return part number as string""" |
| 148 | if offset + 0x15c >= len(self.blob): |
| 149 | return "" |
| 150 | tmp = self.blob[offset + 0x149:offset + 0x15c + 1] |
| 151 | return tmp.decode('utf-8').rstrip() |
| 152 | |
| 153 | def get_manufacturer_id(self, offset): |
| 154 | """Return manufacturer ID""" |
| 155 | if offset + 0x141 >= len(self.blob): |
| 156 | return 0xffff |
| 157 | tmp = self.blob[offset + 0x140:offset + 0x141 + 1] |
| 158 | return struct.unpack('H', tmp)[0] |
| 159 | |
| 160 | def get_mtransfers(self, offset): |
| 161 | """Return MT/s as specified by MTB and FTB""" |
| 162 | if offset + 0x7d >= len(self.blob): |
| 163 | return 0 |
| 164 | |
| 165 | if self.blob_as_ord(offset + 0x11) != 0: |
| 166 | return 0 |
| 167 | mtb = 8.0 |
| 168 | ftb = 1000.0 |
| 169 | tmp = self.blob[offset + 0x12:offset + 0x12 + 1] |
| 170 | tckm = struct.unpack('B', tmp)[0] |
| 171 | tmp = self.blob[offset + 0x7d:offset + 0x7d + 1] |
| 172 | tckf = struct.unpack('b', tmp)[0] |
| 173 | return int(2000 / (tckm / mtb + tckf / ftb)) |
| 174 | |
| 175 | |
| 176 | if __name__ == "__main__": |
| 177 | parser = argparse.ArgumentParser(description='SPD rom dumper') |
| 178 | parser.add_argument('--blob', required=True, |
| 179 | help='The ROM to search SPDs in.') |
| 180 | parser.add_argument('--spd4', action='store_true', default=False, |
| 181 | help='Search for DDR4 SPDs.') |
| 182 | parser.add_argument('--hex', action='store_true', default=False, |
| 183 | help='Store SPD in hex format otherwise binary.') |
| 184 | parser.add_argument('-v', '--verbose', help='increase output verbosity', |
| 185 | action='store_true') |
| 186 | parser.add_argument('--ignorecrc', help='Ignore CRC mismatch', |
| 187 | action='store_true', default=False) |
| 188 | args = parser.parse_args() |
| 189 | |
| 190 | blob = open(args.blob, "rb").read() |
| 191 | |
| 192 | if args.spd4: |
| 193 | p = SPD4Parser(blob, args.verbose, args.ignorecrc) |
| 194 | else: |
| 195 | raise Exception("Must specify one of the following arguments:\n--spd4") |
| 196 | |
| 197 | offset = 0 |
| 198 | cnt = 0 |
| 199 | while True: |
| 200 | offset, length = p.search(offset) |
| 201 | if length == 0: |
| 202 | break |
| 203 | print("Found SPD at 0x%x" % offset) |
| 204 | print(" '%s', size %d, manufacturer %s (0x%04x) %d MT/s\n" % |
| 205 | (p.get_part_number(offset), length, p.get_manufacturer(offset), |
| 206 | p.get_manufacturer_id(offset), p.get_mtransfers(offset))) |
| 207 | filename = "spd-%d-%s-%s.bin" % (cnt, p.get_part_number(offset), |
| 208 | p.get_manufacturer(offset)) |
| 209 | filename = filename.replace("/", "_") |
| 210 | filename = "".join([c for c in filename if c.isalpha() or c.isdigit() |
| 211 | or c == '-' or c == '.' or c == '_']).rstrip() |
| 212 | if not args.hex: |
| 213 | open(filename, "wb").write(blob[offset:offset + length]) |
| 214 | else: |
| 215 | filename += ".hex" |
| 216 | with open(filename, "w") as fn: |
| 217 | j = 0 |
| 218 | for i in blob[offset:offset + length]: |
| 219 | fn.write("%02X" % struct.unpack('B', i)[0]) |
| 220 | fn.write(" " if j < 15 else "\n") |
| 221 | j = (j + 1) % 16 |
| 222 | offset += 1 |
| 223 | cnt += 1 |