blob: 8ca549813474ea32129222656bc599e0fd13a073 [file] [log] [blame]
Nicola Corna9bcc0022017-01-23 15:28:24 +01001#!/usr/bin/python
2
3# me_cleaner - Tool for partial deblobbing of Intel ME/TXE firmware images
4# Copyright (C) 2016, 2017 Nicola Corna <nicola@corna.info>
5#
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.
10#
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.
15#
16
17import sys
18import itertools
19import binascii
20import hashlib
Nicola Cornae38f8592017-02-22 10:16:32 +010021import argparse
22import shutil
Nicola Corna9bcc0022017-01-23 15:28:24 +010023from struct import pack, unpack
24
25
Nicola Cornae38f8592017-02-22 10:16:32 +010026min_ftpr_offset = 0x400
27spared_blocks = 4
Nicola Corna9bcc0022017-01-23 15:28:24 +010028unremovable_modules = ("BUP", "ROMP")
29
30
Nicola Cornae38f8592017-02-22 10:16:32 +010031class OutOfRegionException(Exception):
32 pass
33
34
35class regionFile:
36 def __init__(self, f, region_start, region_end):
37 self.f = f
38 self.region_start = region_start
39 self.region_end = region_end
40
41 def read(self, n):
42 return self.f.read(n)
43
44 def readinto(self, b):
45 return self.f.readinto(b)
46
47 def seek(self, offset):
48 return self.f.seek(offset)
49
50 def write_to(self, offset, data):
51 if offset >= self.region_start and \
52 offset + len(data) <= self.region_end:
53 self.f.seek(offset)
54 return self.f.write(data)
55 else:
56 raise OutOfRegionException()
57
58 def fill_range(self, start, end, fill):
59 if start >= self.region_start and end <= self.region_end:
60 if start < end:
61 block = fill * 4096
62 self.f.seek(start)
63 self.f.writelines(itertools.repeat(block,
64 (end - start) // 4096))
65 self.f.write(block[:(end - start) % 4096])
66 else:
67 raise OutOfRegionException()
68
69 def move_range(self, offset_from, size, offset_to, fill):
70 if offset_from >= self.region_start and \
71 offset_from + size <= self.region_end and \
72 offset_to >= self.region_start and \
73 offset_to + size <= self.region_end:
74 for i in range(0, size, 4096):
75 self.f.seek(offset_from + i, 0)
76 block = self.f.read(4096 if size - i >= 4096 else size - i)
77 self.f.seek(offset_from + i, 0)
78 self.f.write(fill * len(block))
79 self.f.seek(offset_to + i, 0)
80 self.f.write(block)
81 else:
82 raise OutOfRegionException()
83
84
Nicola Corna9bcc0022017-01-23 15:28:24 +010085def get_chunks_offsets(llut, me_start):
86 chunk_count = unpack("<I", llut[0x04:0x08])[0]
87 huffman_stream_end = sum(unpack("<II", llut[0x10:0x18])) + me_start
88 nonzero_offsets = [huffman_stream_end]
89 offsets = []
90
91 for i in range(0, chunk_count):
92 chunk = llut[0x40 + i * 4:0x44 + i * 4]
93 offset = 0
94
95 if chunk[3] != 0x80:
96 offset = unpack("<I", chunk[0:3] + b"\x00")[0] + me_start
97
98 offsets.append([offset, 0])
99 if offset != 0:
100 nonzero_offsets.append(offset)
101
102 nonzero_offsets.sort()
103
104 for i in offsets:
105 if i[0] != 0:
106 i[1] = nonzero_offsets[nonzero_offsets.index(i[0]) + 1]
107
108 return offsets
109
110
Nicola Cornae38f8592017-02-22 10:16:32 +0100111def remove_modules(f, mod_headers, ftpr_offset, me_end):
Nicola Corna9bcc0022017-01-23 15:28:24 +0100112 comp_str = ("Uncomp.", "Huffman", "LZMA")
113 unremovable_huff_chunks = []
114 chunks_offsets = []
115 base = 0
116 chunk_size = 0
Nicola Cornae38f8592017-02-22 10:16:32 +0100117 end_addr = 0
Nicola Corna9bcc0022017-01-23 15:28:24 +0100118
119 for mod_header in mod_headers:
120 name = mod_header[0x04:0x14].rstrip(b"\x00").decode("ascii")
121 offset = unpack("<I", mod_header[0x38:0x3C])[0] + ftpr_offset
122 size = unpack("<I", mod_header[0x40:0x44])[0]
123 flags = unpack("<I", mod_header[0x50:0x54])[0]
124 comp_type = (flags >> 4) & 7
125
126 sys.stdout.write(" {:<16} ({:<7}, ".format(name, comp_str[comp_type]))
127
128 if comp_type == 0x00 or comp_type == 0x02:
129 sys.stdout.write("0x{:06x} - 0x{:06x}): "
130 .format(offset, offset + size))
131
132 if name in unremovable_modules:
Nicola Cornae38f8592017-02-22 10:16:32 +0100133 end_addr = max(end_addr, offset + size)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100134 print("NOT removed, essential")
135 else:
Nicola Cornae38f8592017-02-22 10:16:32 +0100136 end = min(offset + size, me_end)
137 f.fill_range(offset, end, b"\xff")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100138 print("removed")
139
140 elif comp_type == 0x01:
141 sys.stdout.write("fragmented data ): ")
142 if not chunks_offsets:
143 f.seek(offset)
144 llut = f.read(4)
145 if llut == b"LLUT":
146 llut += f.read(0x3c)
147
148 chunk_count = unpack("<I", llut[0x4:0x8])[0]
149 base = unpack("<I", llut[0x8:0xc])[0] + 0x10000000
Nicola Corna9bcc0022017-01-23 15:28:24 +0100150 chunk_size = unpack("<I", llut[0x30:0x34])[0]
151
Nicola Cornae38f8592017-02-22 10:16:32 +0100152 llut += f.read(chunk_count * 4)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100153 chunks_offsets = get_chunks_offsets(llut, me_start)
154 else:
155 sys.exit("Huffman modules found, but LLUT is not present")
156
157 if name in unremovable_modules:
158 print("NOT removed, essential")
159 module_base = unpack("<I", mod_header[0x34:0x38])[0]
160 module_size = unpack("<I", mod_header[0x3c:0x40])[0]
161 first_chunk_num = (module_base - base) // chunk_size
162 last_chunk_num = first_chunk_num + module_size // chunk_size
163
164 unremovable_huff_chunks += \
165 [x for x in chunks_offsets[first_chunk_num:
166 last_chunk_num + 1] if x[0] != 0]
167 else:
168 print("removed")
169
170 else:
171 sys.stdout.write("0x{:06x} - 0x{:06x}): unknown compression, "
172 "skipping".format(offset, offset + size))
173
174 if chunks_offsets:
175 removable_huff_chunks = []
176
177 for chunk in chunks_offsets:
178 if all(not(unremovable_chk[0] <= chunk[0] < unremovable_chk[1] or
179 unremovable_chk[0] < chunk[1] <= unremovable_chk[1])
180 for unremovable_chk in unremovable_huff_chunks):
181 removable_huff_chunks.append(chunk)
182
183 for removable_chunk in removable_huff_chunks:
184 if removable_chunk[1] > removable_chunk[0]:
Nicola Cornae38f8592017-02-22 10:16:32 +0100185 end = min(removable_chunk[1], me_end)
186 f.fill_range(removable_chunk[0], end, b"\xff")
187
188 end_addr = max(end_addr,
189 max(unremovable_huff_chunks, key=lambda x: x[1])[1])
190
191 return end_addr
Nicola Corna9bcc0022017-01-23 15:28:24 +0100192
193
194def check_partition_signature(f, offset):
195 f.seek(offset)
196 header = f.read(0x80)
197 modulus = int(binascii.hexlify(f.read(0x100)[::-1]), 16)
Nicola Cornae38f8592017-02-22 10:16:32 +0100198 public_exponent = unpack("<I", f.read(4))[0]
Nicola Corna9bcc0022017-01-23 15:28:24 +0100199 signature = int(binascii.hexlify(f.read(0x100)[::-1]), 16)
200
201 header_len = unpack("<I", header[0x4:0x8])[0] * 4
202 manifest_len = unpack("<I", header[0x18:0x1c])[0] * 4
203 f.seek(offset + header_len)
204
205 sha256 = hashlib.sha256()
206 sha256.update(header)
207 sha256.update(f.read(manifest_len - header_len))
208
209 decrypted_sig = pow(signature, public_exponent, modulus)
210
211 return "{:#x}".format(decrypted_sig).endswith(sha256.hexdigest()) # FIXME
212
213
Nicola Cornae38f8592017-02-22 10:16:32 +0100214def relocate_partition(f, me_start, me_end, partition_header_offset,
215 new_offset, mod_headers):
Nicola Corna9bcc0022017-01-23 15:28:24 +0100216
Nicola Cornae38f8592017-02-22 10:16:32 +0100217 f.seek(partition_header_offset)
218 name = f.read(4).rstrip(b"\x00").decode("ascii")
219 f.seek(partition_header_offset + 0x8)
220 old_offset, partition_size = unpack("<II", f.read(0x8))
221 old_offset += me_start
Nicola Corna9bcc0022017-01-23 15:28:24 +0100222
Nicola Cornae38f8592017-02-22 10:16:32 +0100223 llut_start = 0
224 for mod_header in mod_headers:
225 if (unpack("<I", mod_header[0x50:0x54])[0] >> 4) & 7 == 0x01:
226 llut_start = unpack("<I", mod_header[0x38:0x3C])[0] + old_offset
227 break
Nicola Corna9bcc0022017-01-23 15:28:24 +0100228
Nicola Cornae38f8592017-02-22 10:16:32 +0100229 if llut_start != 0:
230 # Bytes 0x9:0xb of the LLUT (bytes 0x1:0x3 of the AddrBase) are added
231 # to the SpiBase (bytes 0xc:0x10 of the LLUT) to compute the final
232 # start of the LLUT. Since AddrBase is not modifiable, we can act only
233 # on SpiBase and here we compute the minimum allowed new_offset.
234 f.seek(llut_start + 0x9)
235 lut_start_corr = unpack("<H", f.read(2))[0]
236 new_offset = max(new_offset,
237 lut_start_corr + me_start - llut_start - 0x40 +
238 old_offset)
239 new_offset = ((new_offset + 0x1f) // 0x20) * 0x20
Nicola Corna9bcc0022017-01-23 15:28:24 +0100240
Nicola Cornae38f8592017-02-22 10:16:32 +0100241 offset_diff = new_offset - old_offset
242 print("Relocating {} to {:#x} - {:#x}..."
243 .format(name, new_offset, new_offset + partition_size))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100244
Nicola Cornae38f8592017-02-22 10:16:32 +0100245 print(" Adjusting FPT entry...")
246 f.write_to(partition_header_offset + 0x8,
247 pack("<I", new_offset - me_start))
248
249 if llut_start != 0:
250 f.seek(llut_start)
251 if f.read(4) == b"LLUT":
252 print(" Adjusting LUT start offset...")
253 lut_offset = llut_start + offset_diff + 0x40 - \
254 lut_start_corr - me_start
255 f.write_to(llut_start + 0x0c, pack("<I", lut_offset))
256
257 print(" Adjusting Huffman start offset...")
258 f.seek(llut_start + 0x14)
259 old_huff_offset = unpack("<I", f.read(4))[0]
260 f.write_to(llut_start + 0x14,
261 pack("<I", old_huff_offset + offset_diff))
262
263 print(" Adjusting chunks offsets...")
264 f.seek(llut_start + 0x4)
265 chunk_count = unpack("<I", f.read(4))[0]
266 f.seek(llut_start + 0x40)
267 chunks = bytearray(chunk_count * 4)
268 f.readinto(chunks)
269 for i in range(0, chunk_count * 4, 4):
270 if chunks[i + 3] != 0x80:
271 chunks[i:i + 3] = \
272 pack("<I", unpack("<I", chunks[i:i + 3] +
273 b"\x00")[0] + offset_diff)[0:3]
274 f.write_to(llut_start + 0x40, chunks)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100275 else:
Nicola Cornae38f8592017-02-22 10:16:32 +0100276 sys.exit("Huffman modules present but no LLUT found!")
277 else:
278 print(" No Huffman modules found")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100279
Nicola Cornae38f8592017-02-22 10:16:32 +0100280 print(" Moving data...")
281 partition_size = min(partition_size, me_end - old_offset)
282 f.move_range(old_offset, partition_size, new_offset, b"\xff")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100283
Nicola Cornae38f8592017-02-22 10:16:32 +0100284 return new_offset
Nicola Corna9bcc0022017-01-23 15:28:24 +0100285
Nicola Corna9bcc0022017-01-23 15:28:24 +0100286
Nicola Cornae38f8592017-02-22 10:16:32 +0100287if __name__ == "__main__":
288 parser = argparse.ArgumentParser(description="Tool to remove as much code "
289 "as possible from Intel ME/TXE firmwares")
290 parser.add_argument("file", help="ME/TXE image or full dump")
291 parser.add_argument("-O", "--output", help="save the modified image in a "
292 "separate file, instead of modifying the original "
293 "file")
294 parser.add_argument("-r", "--relocate", help="relocate the FTPR partition "
295 "to the top of the ME region", action="store_true")
296 parser.add_argument("-k", "--keep-modules", help="don't remove the FTPR "
297 "modules, even when possible", action="store_true")
298 parser.add_argument("-d", "--descriptor", help="remove the ME/TXE "
299 "Read/Write permissions to the other regions on the "
300 "flash from the Intel Flash Descriptor (requires a "
301 "full dump)", action="store_true")
302 parser.add_argument("-c", "--check", help="verify the integrity of the "
303 "fundamental parts of the firmware and exit",
304 action="store_true")
305 args = parser.parse_args()
Nicola Corna9bcc0022017-01-23 15:28:24 +0100306
Nicola Cornae38f8592017-02-22 10:16:32 +0100307 f = open(args.file, "rb" if args.check or args.output else "r+b")
308 f.seek(0x10)
309 magic = f.read(4)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100310
Nicola Cornae38f8592017-02-22 10:16:32 +0100311 if magic == b"$FPT":
312 print("ME/TXE image detected")
313 me_start = 0
314 f.seek(0, 2)
315 me_end = f.tell()
Nicola Corna9bcc0022017-01-23 15:28:24 +0100316
Nicola Cornae38f8592017-02-22 10:16:32 +0100317 if args.descriptor:
318 sys.exit("-d requires a full dump")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100319
Nicola Cornae38f8592017-02-22 10:16:32 +0100320 elif magic == b"\x5a\xa5\xf0\x0f":
321 print("Full image detected")
322 f.seek(0x14)
323 flmap0, flmap1 = unpack("<II", f.read(8))
324 nr = flmap0 >> 24 & 0x7
325 frba = flmap0 >> 12 & 0xff0
326 fmba = (flmap1 & 0xff) << 4
327 if nr >= 2:
328 f.seek(frba)
329 flreg0, flreg1, flreg2 = unpack("<III", f.read(12))
330 fd_start = (flreg0 & 0x1fff) << 12
331 fd_end = flreg0 >> 4 & 0x1fff000 | 0xfff + 1
332 me_start = (flreg2 & 0x1fff) << 12
333 me_end = flreg2 >> 4 & 0x1fff000 | 0xfff + 1
334
335 if me_start >= me_end:
336 sys.exit("The ME/TXE region in this image has been disabled")
337
338 f.seek(me_start + 0x10)
339 if f.read(4) != b"$FPT":
340 sys.exit("The ME/TXE region is corrupted or missing")
341
342 print("The ME/TXE region goes from {:#x} to {:#x}"
343 .format(me_start, me_end))
344 else:
345 sys.exit("This image does not contains a ME/TXE firmware NR = {})"
346 .format(nr))
347 else:
348 sys.exit("Unknown image")
349
350 print("Found FPT header at {:#x}".format(me_start + 0x10))
351
352 f.seek(me_start + 0x14)
353 entries = unpack("<I", f.read(4))[0]
354 print("Found {} partition(s)".format(entries))
355
356 f.seek(me_start + 0x14)
357 header_len = unpack("B", f.read(1))[0]
358
359 f.seek(me_start + 0x30)
360 partitions = f.read(entries * 0x20)
361
362 ftpr_header = b""
363
364 for i in range(entries):
365 if partitions[i * 0x20:(i * 0x20) + 4] == b"FTPR":
366 ftpr_header = partitions[i * 0x20:(i + 1) * 0x20]
367 break
368
369 if ftpr_header == b"":
370 sys.exit("FTPR header not found, this image doesn't seem to be valid")
371
372 ftpr_offset, ftpr_lenght = unpack("<II", ftpr_header[0x08:0x10])
373 ftpr_offset += me_start
374 print("Found FTPR header: FTPR partition spans from {:#x} to {:#x}"
375 .format(ftpr_offset, ftpr_offset + ftpr_lenght))
376
377 f.seek(ftpr_offset)
378 if f.read(4) == b"$CPD":
379 me11 = True
380 num_entries = unpack("<I", f.read(4))[0]
381 ftpr_mn2_offset = 0x10 + num_entries * 0x18
382 else:
383 me11 = False
384 ftpr_mn2_offset = 0
385
386 f.seek(ftpr_offset + ftpr_mn2_offset + 0x24)
387 version = unpack("<HHHH", f.read(0x08))
388 print("ME/TXE firmware version {}"
389 .format('.'.join(str(i) for i in version)))
390
391 if not args.check:
392 if args.output:
393 f.close()
394 shutil.copy(args.file, args.output)
395 f = open(args.output, "r+b")
396
397 mef = regionFile(f, me_start, me_end)
398
Nicola Corna9bcc0022017-01-23 15:28:24 +0100399 print("Removing extra partitions...")
Nicola Cornae38f8592017-02-22 10:16:32 +0100400 mef.fill_range(me_start + 0x30, ftpr_offset, b"\xff")
401 mef.fill_range(ftpr_offset + ftpr_lenght, me_end, b"\xff")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100402
403 print("Removing extra partition entries in FPT...")
Nicola Cornae38f8592017-02-22 10:16:32 +0100404 mef.write_to(me_start + 0x30, ftpr_header)
405 mef.write_to(me_start + 0x14, pack("<I", 1))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100406
407 print("Removing EFFS presence flag...")
Nicola Cornae38f8592017-02-22 10:16:32 +0100408 mef.seek(me_start + 0x24)
409 flags = unpack("<I", mef.read(4))[0]
Nicola Corna9bcc0022017-01-23 15:28:24 +0100410 flags &= ~(0x00000001)
Nicola Cornae38f8592017-02-22 10:16:32 +0100411 mef.write_to(me_start + 0x24, pack("<I", flags))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100412
Nicola Cornae38f8592017-02-22 10:16:32 +0100413 if args.descriptor:
414 print("Removing ME/TXE R/W access to the other flash regions...")
415 fdf = regionFile(f, fd_start, fd_end)
416 fdf.write_to(fmba + 0x4, pack("<I", 0x04040000))
417
418 if me11:
419 mef.seek(me_start + 0x10)
420 header = bytearray(mef.read(0x20))
421 else:
422 mef.seek(me_start)
423 header = bytearray(mef.read(0x30))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100424 checksum = (0x100 - (sum(header) - header[0x1b]) & 0xff) & 0xff
425
426 print("Correcting checksum (0x{:02x})...".format(checksum))
427 # The checksum is just the two's complement of the sum of the first
Nicola Cornae38f8592017-02-22 10:16:32 +0100428 # 0x30 bytes in ME < 11 or bytes 0x10:0x30 in ME >= 11 (except for
429 # 0x1b, the checksum itself). In other words, the sum of those bytes
430 # must be always 0x00.
431 mef.write_to(me_start + 0x1b, pack("B", checksum))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100432
433 if not me11:
434 print("Reading FTPR modules list...")
Nicola Cornae38f8592017-02-22 10:16:32 +0100435 mef.seek(ftpr_offset + 0x1c)
436 tag = mef.read(4)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100437
438 if tag == b"$MN2":
Nicola Cornae38f8592017-02-22 10:16:32 +0100439 mef.seek(ftpr_offset + 0x20)
440 num_modules = unpack("<I", mef.read(4))[0]
441 mef.seek(ftpr_offset + 0x290)
442 data = mef.read(0x84)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100443
444 module_header_size = 0
445 if data[0x0:0x4] == b"$MME":
Nicola Cornae38f8592017-02-22 10:16:32 +0100446 if data[0x60:0x64] == b"$MME" or num_modules == 1:
Nicola Corna9bcc0022017-01-23 15:28:24 +0100447 module_header_size = 0x60
448 elif data[0x80:0x84] == b"$MME":
449 module_header_size = 0x80
450
451 if module_header_size != 0:
Nicola Cornae38f8592017-02-22 10:16:32 +0100452 mef.seek(ftpr_offset + 0x290)
453 mod_headers = [mef.read(module_header_size)
Nicola Corna9bcc0022017-01-23 15:28:24 +0100454 for i in range(0, num_modules)]
455
Nicola Cornae38f8592017-02-22 10:16:32 +0100456 if all(hdr.startswith(b"$MME") for hdr in mod_headers):
457 if args.keep_modules:
458 end_addr = ftpr_offset + ftpr_lenght
459 else:
460 end_addr = remove_modules(mef, mod_headers,
461 ftpr_offset, me_end)
462
463 if args.relocate:
464 new_ftpr_offset = relocate_partition(mef,
465 me_start, me_end,
466 me_start + 0x30,
467 min_ftpr_offset + me_start,
468 mod_headers)
469 end_addr += new_ftpr_offset - ftpr_offset
470 ftpr_offset = new_ftpr_offset
471
472 end_addr = (end_addr // 0x1000 + 1) * 0x1000
473 end_addr += spared_blocks * 0x1000
474
475 print("The ME minimum size should be {0} bytes "
476 "({0:#x} bytes)".format(end_addr - me_start))
477
478 if me_start > 0:
479 print("The ME region can be reduced up to:\n"
480 " {:08x}:{:08x} me"
481 .format(me_start, end_addr - 1))
Nicola Corna9bcc0022017-01-23 15:28:24 +0100482 else:
483 print("Found less modules than expected in the FTPR "
484 "partition; skipping modules removal")
485 else:
486 print("Can't find the module header size; skipping "
487 "modules removal")
488 else:
489 print("Wrong FTPR partition tag ({}); skipping modules removal"
490 .format(tag))
491 else:
492 print("Modules removal in ME v11 or greater is not yet supported")
493
Nicola Cornae38f8592017-02-22 10:16:32 +0100494 sys.stdout.write("Checking FTPR RSA signature... ")
495 if check_partition_signature(f, ftpr_offset + ftpr_mn2_offset):
496 print("VALID")
497 else:
498 print("INVALID!!")
499 sys.exit("The FTPR partition signature is not valid. Is the input "
500 "ME/TXE image valid?")
Nicola Corna9bcc0022017-01-23 15:28:24 +0100501
Nicola Cornae38f8592017-02-22 10:16:32 +0100502 f.close()
503
504 if not args.check:
Nicola Corna9bcc0022017-01-23 15:28:24 +0100505 print("Done! Good luck!")