1#!/usr/bin/env python 2# spdtool - Tool for partial deblobbing of UEFI firmware images 3# SPDX-License-Identifier: GPL-3.0-or-later 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 17import argparse 18import crc16 19import struct 20 21 22class 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 94class 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 176if __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 224