3 # spdtool - Tool for partial deblobbing of UEFI firmware images
4 # Copyright (C) 2019 9elements Agency GmbH <patrick.rudolph@9elements.com>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
17 # Parse a blob and search for SPD files.
18 # First it is searched for a possible SPD header.
20 # For each candidate the function verify_match is invoked to check
21 # additional fields (known bits, reserved bits, CRC, ...)
23 # Dumps the found SPDs into the current folder.
35 def __init__(self
, blob
, verbose
=False, ignorecrc
=False):
37 self
.ignorecrc
= ignorecrc
38 self
.verbose
= verbose
42 """Return the first byte to look for"""
43 raise Exception("Function not implemented")
45 def verify_match(self
, header
, offset
):
46 """Return true if it looks like a SPD"""
47 raise Exception("Function not implemented")
49 def get_len(self
, header
, offset
):
50 """Return the length of the SPD"""
51 raise Exception("Function not implemented")
53 def get_part_number(self
, offset
):
54 """Return the part number in SPD"""
57 def get_manufacturer_id(self
, offset
):
58 """Return the manufacturer ID in SPD"""
61 def get_mtransfers(self
, offset
):
62 """Return the number of MT/s"""
65 def get_manufacturer(self
, offset
):
66 """Return manufacturer as string"""
67 id = self
.get_manufacturer_id(offset
)
71 0x2c80: "Crucial/Micron",
78 0xad80: "Hynix/Hyundai",
79 0xb502: "SuperTalent",
89 def blob_as_ord(self
, offset
):
90 """Helper for python2/python3 compatibility"""
91 return self
.blob
[offset
] if type(self
.blob
[offset
]) is int \
92 else ord(self
.blob
[offset
])
94 def search(self
, start
):
95 """Search for SPD at start. Returns -1 on error or offset
98 for i
in self
.get_matches():
99 for offset
in range(start
, len(self
.blob
)):
100 if self
.blob_as_ord(offset
) == i
and \
101 self
.verify_match(i
, offset
):
102 return offset
, self
.get_len(i
, offset
)
106 class SPD4Parser(Parser
):
109 """Return DDR4 possible header candidates"""
111 for i
in [1, 2, 3, 4]:
113 ret
.append(i
+ j
* 16)
116 def verify_match(self
, header
, offset
):
117 """Verify DDR4 specific bit fields."""
118 # offset 0 is a candidate, no need to validate
119 if self
.blob_as_ord(offset
+ 1) == 0xff:
121 if self
.blob_as_ord(offset
+ 2) != 0x0c:
123 if self
.blob_as_ord(offset
+ 5) & 0xc0 > 0:
125 if self
.blob_as_ord(offset
+ 6) & 0xc > 0:
127 if self
.blob_as_ord(offset
+ 7) & 0xc0 > 0:
129 if self
.blob_as_ord(offset
+ 8) != 0:
131 if self
.blob_as_ord(offset
+ 9) & 0xf > 0:
134 print("%x: Looks like DDR4 SPD" % offset
)
136 crc
= crc16
.crc16xmodem(self
.blob
[offset
:offset
+ 0x7d + 1])
137 # Vendors ignore the endianness...
138 crc_spd1
= self
.blob_as_ord(offset
+ 0x7f)
139 crc_spd1 |
= (self
.blob_as_ord(offset
+ 0x7e) << 8)
140 crc_spd2
= self
.blob_as_ord(offset
+ 0x7e)
141 crc_spd2 |
= (self
.blob_as_ord(offset
+ 0x7f) << 8)
142 if crc
!= crc_spd1
and crc
!= crc_spd2
:
144 print("%x: CRC16 doesn't match" % offset
)
145 if not self
.ignorecrc
:
150 def get_len(self
, header
, offset
):
151 """Return the length of the SPD found."""
152 if (header
>> 4) & 7 == 1:
154 if (header
>> 4) & 7 == 2:
158 def get_part_number(self
, offset
):
159 """Return part number as string"""
160 if offset
+ 0x15c >= len(self
.blob
):
162 tmp
= self
.blob
[offset
+ 0x149:offset
+ 0x15c + 1]
163 return tmp
.decode('utf-8').rstrip()
165 def get_manufacturer_id(self
, offset
):
166 """Return manufacturer ID"""
167 if offset
+ 0x141 >= len(self
.blob
):
169 tmp
= self
.blob
[offset
+ 0x140:offset
+ 0x141 + 1]
170 return struct
.unpack('H', tmp
)[0]
172 def get_mtransfers(self
, offset
):
173 """Return MT/s as specified by MTB and FTB"""
174 if offset
+ 0x7d >= len(self
.blob
):
177 if self
.blob_as_ord(offset
+ 0x11) != 0:
181 tmp
= self
.blob
[offset
+ 0x12:offset
+ 0x12 + 1]
182 tckm
= struct
.unpack('B', tmp
)[0]
183 tmp
= self
.blob
[offset
+ 0x7d:offset
+ 0x7d + 1]
184 tckf
= struct
.unpack('b', tmp
)[0]
185 return int(2000 / (tckm
/ mtb
+ tckf
/ ftb
))
188 if __name__
== "__main__":
189 parser
= argparse
.ArgumentParser(description
='SPD rom dumper')
190 parser
.add_argument('--blob', required
=True,
191 help='The ROM to search SPDs in.')
192 parser
.add_argument('--spd4', action
='store_true', default
=False,
193 help='Search for DDR4 SPDs.')
194 parser
.add_argument('--hex', action
='store_true', default
=False,
195 help='Store SPD in hex format otherwise binary.')
196 parser
.add_argument('-v', '--verbose', help='increase output verbosity',
198 parser
.add_argument('--ignorecrc', help='Ignore CRC mismatch',
199 action
='store_true', default
=False)
200 args
= parser
.parse_args()
202 blob
= open(args
.blob
, "rb").read()
205 p
= SPD4Parser(blob
, args
.verbose
, args
.ignorecrc
)
207 raise Exception("Must specify one of the following arguments:\n--spd4")
212 offset
, length
= p
.search(offset
)
215 print("Found SPD at 0x%x" % offset
)
216 print(" '%s', size %d, manufacturer %s (0x%04x) %d MT/s\n" %
217 (p
.get_part_number(offset
), length
, p
.get_manufacturer(offset
),
218 p
.get_manufacturer_id(offset
), p
.get_mtransfers(offset
)))
219 filename
= "spd-%d-%s-%s.bin" % (cnt
, p
.get_part_number(offset
),
220 p
.get_manufacturer(offset
))
221 filename
= filename
.replace("/", "_")
222 filename
= "".join([c
for c
in filename
if c
.isalpha() or c
.isdigit()
223 or c
== '-' or c
== '.' or c
== '_']).rstrip()
225 open(filename
, "wb").write(blob
[offset
:offset
+ length
])
228 with
open(filename
, "w") as fn
:
230 for i
in blob
[offset
:offset
+ length
]:
231 fn
.write("%02X" % struct
.unpack('B', i
)[0])
232 fn
.write(" " if j
< 15 else "\n")