diff options
Diffstat (limited to 'util/me_cleaner')
-rw-r--r-- | util/me_cleaner/README.md | 103 | ||||
-rw-r--r-- | util/me_cleaner/man/me_cleaner.1 | 159 | ||||
-rwxr-xr-x | util/me_cleaner/me_cleaner.py | 568 | ||||
-rwxr-xr-x | util/me_cleaner/setup.py | 22 |
4 files changed, 630 insertions, 222 deletions
diff --git a/util/me_cleaner/README.md b/util/me_cleaner/README.md index 72c15986df..d4c20598d6 100644 --- a/util/me_cleaner/README.md +++ b/util/me_cleaner/README.md @@ -1,44 +1,85 @@ -# me_cleaner - -Intel ME is a coprocessor integrated in all post-2006 Intel boards, for which -this [Libreboot page](https://libreboot.org/faq.html#intelme) has an excellent -description. The main component of Intel ME is Intel AMT, and I suggest you to -read [this Wikipedia page](https://en.wikipedia.org/wiki/Intel_Active_Management_Technology) -for more information about it. In short, Intel ME is an irremovable environment -with an obscure signed proprietary firmware, with full network and memory -access, which poses a serious security threat. -Even when disabled from the BIOS settings, Intel ME is active: the only way to -be sure it is disabled is to remove its firmware from the flash chip. +# me\_cleaner + +_me\_cleaner_ is a Python script able to modify an Intel ME firmware image with +the final purpose of reducing its ability to interact with the system. + +## Intel ME + +Intel ME is a co-processor integrated in all post-2006 Intel boards, which is +the base hardware for many Intel features like Intel AMT, Intel Boot Guard, +Intel PAVP and many others. To provide such features, it requires full access to +the system, including memory (through DMA) and network access (transparent to +the user). + +Unlike many other firmware components, the Intel ME firmware can't be neither +disabled nor reimplemented, as it is tightly integrated in the boot process and +it is signed. + +This poses an issue both to the free firmware implementations like [coreboot]( +https://www.coreboot.org/), which are forced to rely on a proprietary, obscure +and always-on blob, and to the privacy-aware users, who are reasonably worried +about such firmware, running on the lowest privilege ring on x86. + +## What can be done Before Nehalem (ME version 6, 2008/2009) the ME firmware could be removed completely from the flash chip by setting a couple of bits inside the flash -descriptor, without the need to reverse-engineer the ME firmware. +descriptor, effectively disabling it. Starting from Nehalem the Intel ME firmware can't be removed anymore: without a -valid firmware the PC shuts off forcefully after 30 minutes. This project is an -attempt to remove as much code as possible from such firmware without falling -into the 30 minutes recovery mode. +valid firmware the PC shuts off forcefully after 30 minutes, probably as an +attempt to enforce the Intel Anti-Theft policies. + +However, while Intel ME can't be turned off completely, it is still possible to +modify its firmware up to a point where Intel ME is active only during the boot +process, effectively disabling it during the normal operation, which is what +_me\_cleaner_ tries to accomplish. + +## Platform support -me_cleaner currently works on most architectures, see [me_cleaner status](https://github.com/corna/me_cleaner/wiki/me_cleaner-status) (or [its discussion](https://github.com/corna/me_cleaner/issues/3)) -for more info about them. me_cleaner works also on the TXE and SPS firmware. +_me\_cleaner_ currently works on [most of the Intel platforms]( +https://github.com/corna/me_cleaner/wiki/me_cleaner-status); while this doesn't +mean it works on all the boards (due to the different firmware implementations), +it has been proven quite reliable on a great number of them. -If you want to understand how me_cleaner works, you can read the ["How does it work?" page](https://github.com/corna/me_cleaner/wiki/How-does-it-work%3F). +## Usage -If you want to apply me_cleaner on your platform I suggest you to read the -["How does it work?" page](https://github.com/corna/me_cleaner/wiki/How-does-it-work%3F) -and then follow the guide ["How to apply me_cleaner"](https://github.com/corna/me_cleaner/wiki/How-to-apply-me_cleaner). +_me\_cleaner_ should handle all the steps necessary to the modification of an +Intel ME firmware with the command: + + $ python me_cleaner.py -S -O modified_image.bin original_dump.bin + +However, obtaining the original firmware and flashing back the modified one is +usually not trivial, as the Intel ME firmware region is often non-writable from +the OS (and it's not a safe option anyways), requiring the use of an external +SPI programmer. + +## Results For pre-Skylake firmware (ME version < 11) this tool removes almost everything, -leaving only the two fundamental modules needed for the correct boot, ROMP and -BUP. The code size is reduced from 1.5 MB (non-AMT firmware) or 5 MB (AMT +leaving only the two fundamental modules needed for the correct boot, `ROMP` and +`BUP`. The code size is reduced from 1.5 MB (non-AMT firmware) or 5 MB (AMT firmware) to ~90 kB of compressed code. Starting from Skylake (ME version >= 11) the ME subsystem and the firmware -structure have changed, requiring substantial changes in me_cleaner. -The fundamental modules required for the correct boot are now four (rbe, kernel, -syslib and bup) and the minimum code size is ~300 kB of compressed code (from -the 2 MB of the non-AMT firmware and the 7 MB of the AMT one). - -This project is based on the work of the community; in particular I thank Igor -Skochinsky, for the core information about Intel ME and its firmware structure, -and Federico Amedeo Izzo, for its help during the study of Intel ME. +structure have changed, requiring substantial changes in _me\_cleaner_. +The fundamental modules required for the correct boot are now four (`rbe`, +`kernel`, `syslib` and `bup`) and the minimum code size is ~300 kB of compressed +code (from the 2 MB of the non-AMT firmware and the 7 MB of the AMT one). + +On some boards the OEM firmware fails to boot without a valid Intel ME firmware; +in the other cases the system should work with minor inconveniences (like longer +boot times or warning messages) or without issues at all. + +Obviously, the features provided by Intel ME won't be functional anymore after +the modifications. + +## Documentation + +The detailed documentation about the working of _me\_cleaner_ can be found on +the page ["How does it work?" page]( +https://github.com/corna/me_cleaner/wiki/How-does-it-work%3F). + +Various guides and tutorials are available on the Internet, however a good +starting point is the ["How to apply me_cleaner" guide]( +https://github.com/corna/me_cleaner/wiki/How-to-apply-me_cleaner). diff --git a/util/me_cleaner/man/me_cleaner.1 b/util/me_cleaner/man/me_cleaner.1 new file mode 100644 index 0000000000..2a219bcb8e --- /dev/null +++ b/util/me_cleaner/man/me_cleaner.1 @@ -0,0 +1,159 @@ +.TH me_cleaner 1 "MARCH 2018" +.SH me_cleaner +.PP +me_cleaner \- Tool for partial deblobbing of Intel ME/TXE firmware images +.SH SYNOPSIS +.PP +\fB\fCme_cleaner.py\fR [\-h] [\-v] [\-O output_file] [\-S | \-s] [\-r] [\-k] +[\-w whitelist | \-b blacklist] [\-d] [\-t] [\-c] [\-D output_descriptor] +[\-M output_me_image] \fIfile\fP +.SH DESCRIPTION +.PP +\fB\fCme_cleaner\fR is a tool able to disable parts of Intel ME/TXE by: +.RS +.IP \(bu 2 +removing most of the code from its firmware +.IP \(bu 2 +setting a special bit to force it to disable itself after the hardware +initialization +.RE +.PP +Using both the modes seems to be the most reliable way on many platforms. +.PP +The resulting modified firmware needs to be flashed (in most of the cases) with +an external programmer, often a dedicated SPI programmer or a Linux board with +a SPI master interface. +.PP +\fB\fCme_cleaner\fR works at least from Nehalem to Coffee Lake (for Intel ME) and on +Braswell/Cherry Trail (for Intel TXE), but may work as well on newer or +different architectures. +.PP +While \fB\fCme_cleaner\fR have been tested on a great number of platforms, fiddling +with the Intel ME/TXE firmware is \fIvery dangerous\fP and can easily lead to a +dead PC. +.PP +\fIYOU HAVE BEEN WARNED.\fP +.SH POSITIONAL ARGUMENTS +.TP +\fB\fCfile\fR +ME/TXE image or full dump. +.SH OPTIONAL ARGUMENTS +.TP +\fB\fC\-h\fR, \fB\fC\-\-help\fR +Show the help message and exit. +.TP +\fB\fC\-v\fR, \fB\fC\-\-version\fR +Show program's version number and exit. +.TP +\fB\fC\-O\fR, \fB\fC\-\-output\fR +Save the modified image in a separate file, instead of modifying the +original file. +.TP +\fB\fC\-S\fR, \fB\fC\-\-soft\-disable\fR +In addition to the usual operations on the ME/TXE firmware, set the +MeAltDisable bit or the HAP bit to ask Intel ME/TXE to disable itself after +the hardware initialization (requires a full dump). +.TP +\fB\fC\-s\fR, \fB\fC\-\-soft\-disable\-only\fR +Instead of the usual operations on the ME/TXE firmware, just set the +MeAltDisable bit or the HAP bit to ask Intel ME/TXE to disable itself after +the hardware initialization (requires a full dump). +.TP +\fB\fC\-r\fR, \fB\fC\-\-relocate\fR +Relocate the FTPR partition to the top of the ME region to save even more +space. +.TP +\fB\fC\-t\fR, \fB\fC\-\-truncate\fR +Truncate the empty part of the firmware (requires a separated ME/TXE image or +\fB\fC\-\-extract\-me\fR). +.TP +\fB\fC\-k\fR, \fB\fC\-\-keep\-modules\fR +Don't remove the FTPR modules, even when possible. +.TP +\fB\fC\-w\fR, \fB\fC\-\-whitelist\fR +Comma separated list of additional partitions to keep in the final image. +This can be used to specify the MFS partition for example, which stores PCIe +and clock settings. +.TP +\fB\fC\-b\fR, \fB\fC\-\-blacklist\fR +Comma separated list of partitions to remove from the image. This option +overrides the default removal list. +.TP +\fB\fC\-d\fR, \fB\fC\-\-descriptor\fR +Remove the ME/TXE Read/Write permissions to the other regions on the flash +from the Intel Flash Descriptor (requires a full dump). +.TP +\fB\fC\-D\fR, \fB\fC\-\-extract\-descriptor\fR +Extract the flash descriptor from a full dump; when used with \fB\fC\-\-truncate\fR +save a descriptor with adjusted regions start and end. +.TP +\fB\fC\-M\fR, \fB\fC\-\-extract\-me\fR +Extract the ME firmware from a full dump; when used with \fB\fC\-\-truncate\fR save a +truncated ME/TXE image. +.TP +\fB\fC\-c\fR, \fB\fC\-\-check\fR +Verify the integrity of the fundamental parts of the firmware and exit. +.SH SUPPORTED PLATFORMS +.PP +Currently \fB\fCme_cleaner\fR has been tested on the following platforms: +.TS +allbox; +cb cb cb cb +c c c c +c c c c +c c c c +c c c c +c c c c +c c c c +c c c c +c c c c +. +PCH CPU ME SKU +Ibex Peak * Nehalem/Westmere 6.0 Ignition +Ibex Peak * Nehalem/Westmere 6.x 1.5/5 MB +Cougar Point Sandy Bridge 7.x 1.5/5 MB +Panther Point Ivy Bridge 8.x 1.5/5 MB +Lynx/Wildcat Point Haswell/Broadwell 9.x 1.5/5 MB +Wildcat Point LP Broadwell Mobile 10.0 1.5/5 MB +Sunrise Point Skylake/Kabylake 11.x CON/COR +Union Point Kabylake 11.x CON/COR +.TE +.TS +allbox; +cb cb cb +c c c +. +SoC TXE SKU +Braswell/Cherry Trail 2.x 1.375 MB +.TE +.PP +* Not working on coreboot +.PP +All the reports are available on the project's GitHub page \[la]https://github.com/corna/me_cleaner/issues/3\[ra]\&. +.SH EXAMPLES +.PP +Check whether the provided image has a valid structure and signature: +.IP +\fB\fCme_cleaner.py \-c dumped_firmware.bin\fR +.PP +Remove most of the Intel ME firmware modules but don't set the HAP/AltMeDisable +bit: +.IP +\fB\fCme_cleaner.py \-S \-O modified_me_firmware.bin dumped_firmware.bin\fR +.PP +Remove most of the Intel ME firmware modules and set the HAP/AltMeDisable bit, +disable the Read/Write access of Intel ME to the other flash region, then +relocate the code to the top of the image and truncate it, extracting a modified +descriptor and ME image: +.IP +\fB\fCme_cleaner.py \-S \-r \-t \-d \-D ifd_shrinked.bin \-M me_shrinked.bin \-O modified_firmware.bin full_dumped_firmware.bin\fR +.SH BUGS +.PP +Bugs should be reported on the project's GitHub page \[la]https://github.com/corna/me_cleaner\[ra]\&. +.SH AUTHOR +.PP +Nicola Corna \[la]nicola@corna.info\[ra] +.SH SEE ALSO +.PP +.BR flashrom (8), +me_cleaner's Wiki \[la]https://github.com/corna/me_cleaner/wiki\[ra] diff --git a/util/me_cleaner/me_cleaner.py b/util/me_cleaner/me_cleaner.py index 11f077d207..03fbbcb56b 100755 --- a/util/me_cleaner/me_cleaner.py +++ b/util/me_cleaner/me_cleaner.py @@ -1,7 +1,7 @@ #!/usr/bin/python -# me_cleaner - Tool for partial deblobbing of Intel ME/TXE firmware images -# Copyright (C) 2016, 2017 Nicola Corna <nicola@corna.info> +# me_cleaner - Tool for partial deblobbing of Intel ME/TXE firmware images +# Copyright (C) 2016-2018 Nicola Corna <nicola@corna.info> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,12 +14,14 @@ # GNU General Public License for more details. # -import sys -import itertools +from __future__ import division, print_function + +import argparse import binascii import hashlib -import argparse +import itertools import shutil +import sys from struct import pack, unpack @@ -27,6 +29,23 @@ min_ftpr_offset = 0x400 spared_blocks = 4 unremovable_modules = ("ROMP", "BUP") unremovable_modules_me11 = ("rbe", "kernel", "syslib", "bup") +unremovable_partitions = ("FTPR",) + +pubkeys_md5 = { + "763e59ebe235e45a197a5b1a378dfa04": ("ME", ("6.x.x.x",)), + "3a98c847d609c253e145bd36512629cb": ("ME", ("6.0.50.x",)), + "0903fc25b0f6bed8c4ed724aca02124c": ("ME", ("7.x.x.x", "8.x.x.x")), + "2011ae6df87c40fba09e3f20459b1ce0": ("ME", ("9.0.x.x", "9.1.x.x")), + "e8427c5691cf8b56bc5cdd82746957ed": ("ME", ("9.5.x.x", "10.x.x.x")), + "986a78e481f185f7d54e4af06eb413f6": ("ME", ("11.x.x.x",)), + "bda0b6bb8ca0bf0cac55ac4c4d55e0f2": ("TXE", ("1.x.x.x",)), + "b726a2ab9cd59d4e62fe2bead7cf6997": ("TXE", ("1.x.x.x",)), + "0633d7f951a3e7968ae7460861be9cfb": ("TXE", ("2.x.x.x",)), + "1d0a36e9f5881540d8e4b382c6612ed8": ("TXE", ("3.x.x.x",)), + "be900fef868f770d266b1fc67e887e69": ("SPS", ("2.x.x.x",)), + "4622e3f2cb212a89c90a4de3336d88d2": ("SPS", ("3.x.x.x",)), + "31ef3d950eac99d18e187375c0764ca4": ("SPS", ("4.x.x.x",)) +} class OutOfRegionException(Exception): @@ -40,59 +59,71 @@ class RegionFile: self.region_end = region_end def read(self, n): - return self.f.read(n) + if f.tell() + n <= self.region_end: + return self.f.read(n) + else: + raise OutOfRegionException() def readinto(self, b): - return self.f.readinto(b) + if f.tell() + len(b) <= self.region_end: + return self.f.readinto(b) + else: + raise OutOfRegionException() def seek(self, offset): - return self.f.seek(offset) + if self.region_start + offset <= self.region_end: + return self.f.seek(self.region_start + offset) + else: + raise OutOfRegionException() def write_to(self, offset, data): - if offset >= self.region_start and \ - offset + len(data) <= self.region_end: - self.f.seek(offset) + if self.region_start + offset + len(data) <= self.region_end: + self.f.seek(self.region_start + offset) return self.f.write(data) else: raise OutOfRegionException() def fill_range(self, start, end, fill): - if start >= self.region_start and end <= self.region_end: + if self.region_start + end <= self.region_end: if start < end: block = fill * 4096 - self.f.seek(start) + self.f.seek(self.region_start + start) self.f.writelines(itertools.repeat(block, (end - start) // 4096)) self.f.write(block[:(end - start) % 4096]) else: raise OutOfRegionException() + def fill_all(self, fill): + self.fill_range(0, self.region_end - self.region_start, fill) + def move_range(self, offset_from, size, offset_to, fill): - if offset_from >= self.region_start and \ - offset_from + size <= self.region_end and \ - offset_to >= self.region_start and \ - offset_to + size <= self.region_end: + if self.region_start + offset_from + size <= self.region_end and \ + self.region_start + offset_to + size <= self.region_end: for i in range(0, size, 4096): - self.f.seek(offset_from + i, 0) - block = self.f.read(4096 if size - i >= 4096 else size - i) - self.f.seek(offset_from + i, 0) + self.f.seek(self.region_start + offset_from + i, 0) + block = self.f.read(min(size - i, 4096)) + self.f.seek(self.region_start + offset_from + i, 0) self.f.write(fill * len(block)) - self.f.seek(offset_to + i, 0) + self.f.seek(self.region_start + offset_to + i, 0) self.f.write(block) else: raise OutOfRegionException() def save(self, filename, size): - self.f.seek(self.region_start) - copyf = open(filename, "w+b") - for i in range(0, size, 4096): - copyf.write(self.f.read(4096 if size - i >= 4096 else size - i)) - return copyf + if self.region_start + size <= self.region_end: + self.f.seek(self.region_start) + copyf = open(filename, "w+b") + for i in range(0, size, 4096): + copyf.write(self.f.read(min(size - i, 4096))) + return copyf + else: + raise OutOfRegionException() -def get_chunks_offsets(llut, me_start): +def get_chunks_offsets(llut): chunk_count = unpack("<I", llut[0x04:0x08])[0] - huffman_stream_end = sum(unpack("<II", llut[0x10:0x18])) + me_start + huffman_stream_end = sum(unpack("<II", llut[0x10:0x18])) nonzero_offsets = [huffman_stream_end] offsets = [] @@ -101,7 +132,7 @@ def get_chunks_offsets(llut, me_start): offset = 0 if chunk[3] != 0x80: - offset = unpack("<I", chunk[0:3] + b"\x00")[0] + me_start + offset = unpack("<I", chunk[0:3] + b"\x00")[0] offsets.append([offset, 0]) if offset != 0: @@ -131,11 +162,11 @@ def remove_modules(f, mod_headers, ftpr_offset, me_end): flags = unpack("<I", mod_header[0x50:0x54])[0] comp_type = (flags >> 4) & 7 - sys.stdout.write(" {:<16} ({:<7}, ".format(name, comp_str[comp_type])) + print(" {:<16} ({:<7}, ".format(name, comp_str[comp_type]), end="") if comp_type == 0x00 or comp_type == 0x02: - sys.stdout.write("0x{:06x} - 0x{:06x}): " - .format(offset, offset + size)) + print("0x{:06x} - 0x{:06x} ): " + .format(offset, offset + size), end="") if name in unremovable_modules: end_addr = max(end_addr, offset + size) @@ -146,7 +177,6 @@ def remove_modules(f, mod_headers, ftpr_offset, me_end): print("removed") elif comp_type == 0x01: - sys.stdout.write("fragmented data ): ") if not chunks_offsets: f.seek(offset) llut = f.read(4) @@ -158,16 +188,25 @@ def remove_modules(f, mod_headers, ftpr_offset, me_end): chunk_size = unpack("<I", llut[0x30:0x34])[0] llut += f.read(chunk_count * 4) - chunks_offsets = get_chunks_offsets(llut, me_start) + chunks_offsets = get_chunks_offsets(llut) else: sys.exit("Huffman modules found, but LLUT is not present") + module_base = unpack("<I", mod_header[0x34:0x38])[0] + module_size = unpack("<I", mod_header[0x3c:0x40])[0] + first_chunk_num = (module_base - base) // chunk_size + last_chunk_num = first_chunk_num + module_size // chunk_size + huff_size = 0 + + for chunk in chunks_offsets[first_chunk_num:last_chunk_num + 1]: + huff_size += chunk[1] - chunk[0] + + print("fragmented data, {:<9}): " + .format("~" + str(int(round(huff_size / 1024))) + " KiB"), + end="") + if name in unremovable_modules: print("NOT removed, essential") - module_base = unpack("<I", mod_header[0x34:0x38])[0] - module_size = unpack("<I", mod_header[0x3c:0x40])[0] - first_chunk_num = (module_base - base) // chunk_size - last_chunk_num = first_chunk_num + module_size // chunk_size unremovable_huff_chunks += \ [x for x in chunks_offsets[first_chunk_num: @@ -176,8 +215,8 @@ def remove_modules(f, mod_headers, ftpr_offset, me_end): print("removed") else: - sys.stdout.write("0x{:06x} - 0x{:06x}): unknown compression, " - "skipping".format(offset, offset + size)) + print("0x{:06x} - 0x{:06x}): unknown compression, skipping" + .format(offset, offset + size), end="") if chunks_offsets: removable_huff_chunks = [] @@ -228,14 +267,13 @@ def print_check_partition_signature(f, offset): "ME/TXE image valid?") -def relocate_partition(f, me_start, me_end, partition_header_offset, +def relocate_partition(f, me_end, partition_header_offset, new_offset, mod_headers): f.seek(partition_header_offset) name = f.read(4).rstrip(b"\x00").decode("ascii") f.seek(partition_header_offset + 0x8) old_offset, partition_size = unpack("<II", f.read(0x8)) - old_offset += me_start llut_start = 0 for mod_header in mod_headers: @@ -251,8 +289,7 @@ def relocate_partition(f, me_start, me_end, partition_header_offset, f.seek(llut_start + 0x9) lut_start_corr = unpack("<H", f.read(2))[0] new_offset = max(new_offset, - lut_start_corr + me_start - llut_start - 0x40 + - old_offset) + lut_start_corr - llut_start - 0x40 + old_offset) new_offset = ((new_offset + 0x1f) // 0x20) * 0x20 offset_diff = new_offset - old_offset @@ -262,15 +299,14 @@ def relocate_partition(f, me_start, me_end, partition_header_offset, print(" Adjusting FPT entry...") f.write_to(partition_header_offset + 0x8, - pack("<I", new_offset - me_start)) + pack("<I", new_offset)) if mod_headers: if llut_start != 0: f.seek(llut_start) if f.read(4) == b"LLUT": print(" Adjusting LUT start offset...") - lut_offset = llut_start + offset_diff + 0x40 - \ - lut_start_corr - me_start + lut_offset = llut_start + offset_diff + 0x40 - lut_start_corr f.write_to(llut_start + 0x0c, pack("<I", lut_offset)) print(" Adjusting Huffman start offset...") @@ -303,7 +339,7 @@ def relocate_partition(f, me_start, me_end, partition_header_offset, return new_offset -def check_and_remove_modules(f, me_start, me_end, offset, min_offset, +def check_and_remove_modules(f, me_end, offset, min_offset, relocate, keep_modules): f.seek(offset + 0x20) @@ -311,29 +347,27 @@ def check_and_remove_modules(f, me_start, me_end, offset, min_offset, f.seek(offset + 0x290) data = f.read(0x84) - module_header_size = 0 + mod_header_size = 0 if data[0x0:0x4] == b"$MME": if data[0x60:0x64] == b"$MME" or num_modules == 1: - module_header_size = 0x60 + mod_header_size = 0x60 elif data[0x80:0x84] == b"$MME": - module_header_size = 0x80 + mod_header_size = 0x80 - if module_header_size != 0: + if mod_header_size != 0: f.seek(offset + 0x290) - mod_headers = [f.read(module_header_size) + data = f.read(mod_header_size * num_modules) + mod_headers = [data[i * mod_header_size:(i + 1) * mod_header_size] for i in range(0, num_modules)] if all(hdr.startswith(b"$MME") for hdr in mod_headers): if args.keep_modules: - end_addr = offset + ftpr_lenght + end_addr = offset + ftpr_length else: - end_addr = remove_modules(f, mod_headers, - offset, me_end) + end_addr = remove_modules(f, mod_headers, offset, me_end) if args.relocate: - new_offset = relocate_partition(f, me_start, me_end, - me_start + 0x30, - min_offset + me_start, + new_offset = relocate_partition(f, me_end, 0x30, min_offset, mod_headers) end_addr += new_offset - offset offset = new_offset @@ -350,14 +384,14 @@ def check_and_remove_modules(f, me_start, me_end, offset, min_offset, return -1, offset -def check_and_remove_modules_me11(f, me_start, me_end, partition_offset, - partition_lenght, min_offset, relocate, +def check_and_remove_modules_me11(f, me_end, partition_offset, + partition_length, min_offset, relocate, keep_modules): comp_str = ("LZMA/uncomp.", "Huffman") if keep_modules: - end_data = partition_offset + partition_lenght + end_data = partition_offset + partition_length else: end_data = 0 @@ -365,7 +399,7 @@ def check_and_remove_modules_me11(f, me_start, me_end, partition_offset, module_count = unpack("<I", f.read(4))[0] modules = [] - modules.append(("end", partition_lenght, 0)) + modules.append(("end", partition_length, 0)) f.seek(partition_offset + 0x10) for i in range(0, module_count): @@ -390,8 +424,8 @@ def check_and_remove_modules_me11(f, me_start, me_end, partition_offset, else: compression = comp_str[modules[i][2]] - sys.stdout.write(" {:<12} ({:<12}, 0x{:06x} - 0x{:06x}): " - .format(name, compression, offset, end)) + print(" {:<12} ({:<12}, 0x{:06x} - 0x{:06x}): " + .format(name, compression, offset, end), end="") if name.endswith(".man"): print("NOT removed, partition manif.") @@ -408,8 +442,7 @@ def check_and_remove_modules_me11(f, me_start, me_end, partition_offset, end_data = max(end_data, end) if relocate: - new_offset = relocate_partition(f, me_start, me_end, me_start + 0x30, - min_offset + me_start, []) + new_offset = relocate_partition(f, me_end, 0x30, min_offset, []) end_data += new_offset - partition_offset partition_offset = new_offset @@ -436,40 +469,72 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="Tool to remove as much code " "as possible from Intel ME/TXE firmware " "images") + softdis = parser.add_mutually_exclusive_group() + bw_list = parser.add_mutually_exclusive_group() + + parser.add_argument("-v", "--version", action="version", + version="%(prog)s 1.2") + parser.add_argument("file", help="ME/TXE image or full dump") - parser.add_argument("-O", "--output", help="save the modified image in a " - "separate file, instead of modifying the original " - "file") + parser.add_argument("-O", "--output", metavar='output_file', help="save " + "the modified image in a separate file, instead of " + "modifying the original file") + softdis.add_argument("-S", "--soft-disable", help="in addition to the " + "usual operations on the ME/TXE firmware, set the " + "MeAltDisable bit or the HAP bit to ask Intel ME/TXE " + "to disable itself after the hardware initialization " + "(requires a full dump)", action="store_true") + softdis.add_argument("-s", "--soft-disable-only", help="instead of the " + "usual operations on the ME/TXE firmware, just set " + "the MeAltDisable bit or the HAP bit to ask Intel " + "ME/TXE to disable itself after the hardware " + "initialization (requires a full dump)", + action="store_true") parser.add_argument("-r", "--relocate", help="relocate the FTPR partition " "to the top of the ME region to save even more space", action="store_true") + parser.add_argument("-t", "--truncate", help="truncate the empty part of " + "the firmware (requires a separated ME/TXE image or " + "--extract-me)", action="store_true") parser.add_argument("-k", "--keep-modules", help="don't remove the FTPR " "modules, even when possible", action="store_true") + bw_list.add_argument("-w", "--whitelist", metavar="whitelist", + help="Comma separated list of additional partitions " + "to keep in the final image. This can be used to " + "specify the MFS partition for example, which stores " + "PCIe and clock settings.") + bw_list.add_argument("-b", "--blacklist", metavar="blacklist", + help="Comma separated list of partitions to remove " + "from the image. This option overrides the default " + "removal list.") parser.add_argument("-d", "--descriptor", help="remove the ME/TXE " "Read/Write permissions to the other regions on the " "flash from the Intel Flash Descriptor (requires a " "full dump)", action="store_true") - parser.add_argument("-t", "--truncate", help="truncate the empty part of " - "the firmware (requires a separated ME/TXE image or " - "--extract-me)", action="store_true") + parser.add_argument("-D", "--extract-descriptor", + metavar='output_descriptor', help="extract the flash " + "descriptor from a full dump; when used with " + "--truncate save a descriptor with adjusted regions " + "start and end") + parser.add_argument("-M", "--extract-me", metavar='output_me_image', + help="extract the ME firmware from a full dump; when " + "used with --truncate save a truncated ME/TXE image") parser.add_argument("-c", "--check", help="verify the integrity of the " "fundamental parts of the firmware and exit", action="store_true") - parser.add_argument("-D", "--extract-descriptor", help="extract the " - "flash descriptor from a full dump; when used with " - "--truncate save a descriptor with adjusted regions " - "start and end") - parser.add_argument("-M", "--extract-me", help="extract the ME firmware " - "from a full dump; when used with --truncate save a " - "truncated ME/TXE image") args = parser.parse_args() - if args.check: - if args.relocate: - sys.exit("-c and -r can't be used together") - elif args.truncate: - sys.exit("-c and -t can't be used together") + if args.check and (args.soft_disable_only or args.soft_disable or + args.relocate or args.descriptor or args.truncate or args.output): + sys.exit("-c can't be used with -S, -s, -r, -d, -t or -O") + + if args.soft_disable_only and (args.relocate or args.truncate): + sys.exit("-s can't be used with -r or -t") + + if (args.whitelist or args.blacklist) and args.relocate: + sys.exit("Relocation is not yet supported with custom whitelist or " + "blacklist") f = open(args.file, "rb" if args.check or args.output else "r+b") f.seek(0x10) @@ -478,18 +543,14 @@ if __name__ == "__main__": if magic == b"$FPT": print("ME/TXE image detected") - if args.descriptor: - sys.exit("-d requires a full dump") - - if args.extract_descriptor: - sys.exit("-D requires a full dump") - - if args.extract_me: - sys.exit("-M requires a full dump") + if args.descriptor or args.extract_descriptor or args.extract_me or \ + args.soft_disable or args.soft_disable_only: + sys.exit("-d, -D, -M, -S and -s require a full dump") - me_start = 0 f.seek(0, 2) + me_start = 0 me_end = f.tell() + mef = RegionFile(f, me_start, me_end) elif magic == b"\x5a\xa5\xf0\x0f": print("Full image detected") @@ -501,6 +562,7 @@ if __name__ == "__main__": flmap0, flmap1 = unpack("<II", f.read(8)) frba = flmap0 >> 12 & 0xff0 fmba = (flmap1 & 0xff) << 4 + fpsba = flmap1 >> 12 & 0xff0 f.seek(frba) flreg = unpack("<III", f.read(12)) @@ -512,8 +574,10 @@ if __name__ == "__main__": if me_start >= me_end: sys.exit("The ME/TXE region in this image has been disabled") - f.seek(me_start + 0x10) - if f.read(4) != b"$FPT": + mef = RegionFile(f, me_start, me_end) + + mef.seek(0x10) + if mef.read(4) != b"$FPT": sys.exit("The ME/TXE region is corrupted or missing") print("The ME/TXE region goes from {:#x} to {:#x}" @@ -521,17 +585,16 @@ if __name__ == "__main__": else: sys.exit("Unknown image") - print("Found FPT header at {:#x}".format(me_start + 0x10)) + end_addr = me_end - f.seek(me_start + 0x14) - entries = unpack("<I", f.read(4))[0] - print("Found {} partition(s)".format(entries)) + print("Found FPT header at {:#x}".format(mef.region_start + 0x10)) - f.seek(me_start + 0x14) - header_len = unpack("B", f.read(1))[0] + mef.seek(0x14) + entries = unpack("<I", mef.read(4))[0] + print("Found {} partition(s)".format(entries)) - f.seek(me_start + 0x30) - partitions = f.read(entries * 0x20) + mef.seek(0x30) + partitions = mef.read(entries * 0x20) ftpr_header = b"" @@ -543,21 +606,20 @@ if __name__ == "__main__": if ftpr_header == b"": sys.exit("FTPR header not found, this image doesn't seem to be valid") - ftpr_offset, ftpr_lenght = unpack("<II", ftpr_header[0x08:0x10]) - ftpr_offset += me_start + ftpr_offset, ftpr_length = unpack("<II", ftpr_header[0x08:0x10]) print("Found FTPR header: FTPR partition spans from {:#x} to {:#x}" - .format(ftpr_offset, ftpr_offset + ftpr_lenght)) + .format(ftpr_offset, ftpr_offset + ftpr_length)) - f.seek(ftpr_offset) - if f.read(4) == b"$CPD": + mef.seek(ftpr_offset) + if mef.read(4) == b"$CPD": me11 = True - num_entries = unpack("<I", f.read(4))[0] + num_entries = unpack("<I", mef.read(4))[0] - f.seek(ftpr_offset + 0x10) + mef.seek(ftpr_offset + 0x10) ftpr_mn2_offset = -1 for i in range(0, num_entries): - data = f.read(0x18) + data = mef.read(0x18) name = data[0x0:0xc].rstrip(b"\x00").decode("ascii") offset = unpack("<I", data[0xc:0xf] + b"\x00")[0] @@ -566,94 +628,214 @@ if __name__ == "__main__": break if ftpr_mn2_offset >= 0: - check_mn2_tag(f, ftpr_offset + ftpr_mn2_offset) + check_mn2_tag(mef, ftpr_offset + ftpr_mn2_offset) print("Found FTPR manifest at {:#x}" .format(ftpr_offset + ftpr_mn2_offset)) else: sys.exit("Can't find the manifest of the FTPR partition") else: - check_mn2_tag(f, ftpr_offset) + check_mn2_tag(mef, ftpr_offset) me11 = False ftpr_mn2_offset = 0 - f.seek(ftpr_offset + ftpr_mn2_offset + 0x24) - version = unpack("<HHHH", f.read(0x08)) + mef.seek(ftpr_offset + ftpr_mn2_offset + 0x24) + version = unpack("<HHHH", mef.read(0x08)) print("ME/TXE firmware version {}" .format('.'.join(str(i) for i in version))) - if not args.check: - if args.output: - f.close() - shutil.copy(args.file, args.output) - f = open(args.output, "r+b") - - mef = RegionFile(f, me_start, me_end) + mef.seek(ftpr_offset + ftpr_mn2_offset + 0x80) + pubkey_md5 = hashlib.md5(mef.read(0x104)).hexdigest() - if args.descriptor or args.extract_descriptor: - fdf = RegionFile(f, fd_start, fd_end) + if pubkey_md5 in pubkeys_md5: + variant, pubkey_versions = pubkeys_md5[pubkey_md5] + print("Public key match: Intel {}, firmware versions {}" + .format(variant, ", ".join(pubkey_versions))) + else: + if version[0] >= 6: + variant = "ME" + else: + variant = "TXE" + print("WARNING Unknown public key {}\n" + " Assuming Intel {}\n" + " Please report this warning to the project's maintainer!" + .format(pubkey_md5, variant)) - print("Removing extra partitions...") - mef.fill_range(me_start + 0x30, ftpr_offset, b"\xff") - mef.fill_range(ftpr_offset + ftpr_lenght, me_end, b"\xff") + if not args.check and args.output: + f.close() + shutil.copy(args.file, args.output) + f = open(args.output, "r+b") - print("Removing extra partition entries in FPT...") - mef.write_to(me_start + 0x30, ftpr_header) - mef.write_to(me_start + 0x14, pack("<I", 1)) + mef = RegionFile(f, me_start, me_end) - print("Removing EFFS presence flag...") - mef.seek(me_start + 0x24) - flags = unpack("<I", mef.read(4))[0] - flags &= ~(0x00000001) - mef.write_to(me_start + 0x24, pack("<I", flags)) + if me_start > 0: + fdf = RegionFile(f, fd_start, fd_end) if me11: - mef.seek(me_start + 0x10) - header = bytearray(mef.read(0x20)) - header[0x0b] = 0x00 - else: - mef.seek(me_start) - header = bytearray(mef.read(0x30)) - header[0x1b] = 0x00 - checksum = (0x100 - sum(header) & 0xff) & 0xff - - print("Correcting checksum (0x{:02x})...".format(checksum)) - # The checksum is just the two's complement of the sum of the first - # 0x30 bytes in ME < 11 or bytes 0x10:0x30 in ME >= 11 (except for - # 0x1b, the checksum itself). In other words, the sum of those bytes - # must be always 0x00. - mef.write_to(me_start + 0x1b, pack("B", checksum)) - - print("Reading FTPR modules list...") - if me11: - end_addr, ftpr_offset = \ - check_and_remove_modules_me11(mef, me_start, me_end, - ftpr_offset, ftpr_lenght, - min_ftpr_offset, args.relocate, - args.keep_modules) + fdf.seek(fpsba) + pchstrp0 = unpack("<I", fdf.read(4))[0] + print("The HAP bit is " + + ("SET" if pchstrp0 & 1 << 16 else "NOT SET")) else: - end_addr, ftpr_offset = \ - check_and_remove_modules(mef, me_start, me_end, ftpr_offset, - min_ftpr_offset, args.relocate, - args.keep_modules) - - if end_addr > 0: - end_addr = (end_addr // 0x1000 + 1) * 0x1000 - end_addr += spared_blocks * 0x1000 + fdf.seek(fpsba + 0x28) + pchstrp10 = unpack("<I", fdf.read(4))[0] + print("The AltMeDisable bit is " + + ("SET" if pchstrp10 & 1 << 7 else "NOT SET")) + + # ME 6 Ignition: wipe everything + me6_ignition = False + if not args.check and not args.soft_disable_only and \ + variant == "ME" and version[0] == 6: + mef.seek(ftpr_offset + 0x20) + num_modules = unpack("<I", mef.read(4))[0] + mef.seek(ftpr_offset + 0x290 + (num_modules + 1) * 0x60) + data = mef.read(0xc) + + if data[0x0:0x4] == b"$SKU" and data[0x8:0xc] == b"\x00\x00\x00\x00": + print("ME 6 Ignition firmware detected, removing everything...") + mef.fill_all(b"\xff") + me6_ignition = True - print("The ME minimum size should be {0} bytes " - "({0:#x} bytes)".format(end_addr - me_start)) - - if me_start > 0: - print("The ME region can be reduced up to:\n" - " {:08x}:{:08x} me".format(me_start, end_addr - 1)) - elif args.truncate: - print("Truncating file at {:#x}...".format(end_addr)) - f.truncate(end_addr) + if not args.check: + if not args.soft_disable_only and not me6_ignition: + print("Reading partitions list...") + unremovable_part_fpt = b"" + extra_part_end = 0 + whitelist = [] + blacklist = [] + + whitelist += unremovable_partitions + + if args.blacklist: + blacklist = args.blacklist.split(",") + elif args.whitelist: + whitelist += args.whitelist.split(",") + + for i in range(entries): + partition = partitions[i * 0x20:(i + 1) * 0x20] + flags = unpack("<I", partition[0x1c:0x20])[0] + + try: + part_name = \ + partition[0x0:0x4].rstrip(b"\x00").decode("ascii") + except UnicodeDecodeError: + part_name = "????" + + part_start, part_length = unpack("<II", partition[0x08:0x10]) + + # ME 6: the last partition has 0xffffffff as size + if variant == "ME" and version[0] == 6 and \ + i == entries - 1 and part_length == 0xffffffff: + part_length = me_end - me_start - part_start + + part_end = part_start + part_length + + if flags & 0x7f == 2: + print(" {:<4} ({:^24}, 0x{:08x} total bytes): nothing to " + "remove" + .format(part_name, "NVRAM partition, no data", + part_length)) + elif part_start == 0 or part_length == 0 or part_end > me_end: + print(" {:<4} ({:^24}, 0x{:08x} total bytes): nothing to " + "remove" + .format(part_name, "no data here", part_length)) + else: + print(" {:<4} (0x{:08x} - 0x{:09x}, 0x{:08x} total bytes): " + .format(part_name, part_start, part_end, part_length), + end="") + if part_name in whitelist or (blacklist and + part_name not in blacklist): + unremovable_part_fpt += partition + if part_name != "FTPR": + extra_part_end = max(extra_part_end, part_end) + print("NOT removed") + else: + mef.fill_range(part_start, part_end, b"\xff") + print("removed") + + print("Removing partition entries in FPT...") + mef.write_to(0x30, unremovable_part_fpt) + mef.write_to(0x14, + pack("<I", len(unremovable_part_fpt) // 0x20)) + + mef.fill_range(0x30 + len(unremovable_part_fpt), + 0x30 + len(partitions), b"\xff") + + if (not blacklist and "EFFS" not in whitelist) or \ + "EFFS" in blacklist: + print("Removing EFFS presence flag...") + mef.seek(0x24) + flags = unpack("<I", mef.read(4))[0] + flags &= ~(0x00000001) + mef.write_to(0x24, pack("<I", flags)) + + if me11: + mef.seek(0x10) + header = bytearray(mef.read(0x20)) + header[0x0b] = 0x00 + else: + mef.seek(0) + header = bytearray(mef.read(0x30)) + header[0x1b] = 0x00 + checksum = (0x100 - sum(header) & 0xff) & 0xff + + print("Correcting checksum (0x{:02x})...".format(checksum)) + # The checksum is just the two's complement of the sum of the first + # 0x30 bytes in ME < 11 or bytes 0x10:0x30 in ME >= 11 (except for + # 0x1b, the checksum itself). In other words, the sum of those + # bytes must be always 0x00. + mef.write_to(0x1b, pack("B", checksum)) + + print("Reading FTPR modules list...") + if me11: + end_addr, ftpr_offset = \ + check_and_remove_modules_me11(mef, me_end, + ftpr_offset, ftpr_length, + min_ftpr_offset, + args.relocate, + args.keep_modules) + else: + end_addr, ftpr_offset = \ + check_and_remove_modules(mef, me_end, ftpr_offset, + min_ftpr_offset, args.relocate, + args.keep_modules) + + if end_addr > 0: + end_addr = max(end_addr, extra_part_end) + end_addr = (end_addr // 0x1000 + 1) * 0x1000 + end_addr += spared_blocks * 0x1000 + + print("The ME minimum size should be {0} bytes " + "({0:#x} bytes)".format(end_addr)) + + if me_start > 0: + print("The ME region can be reduced up to:\n" + " {:08x}:{:08x} me" + .format(me_start, me_start + end_addr - 1)) + elif args.truncate: + print("Truncating file at {:#x}...".format(end_addr)) + f.truncate(end_addr) + + if args.soft_disable or args.soft_disable_only: + if me11: + print("Setting the HAP bit in PCHSTRP0 to disable Intel ME...") + pchstrp0 |= (1 << 16) + fdf.write_to(fpsba, pack("<I", pchstrp0)) + else: + print("Setting the AltMeDisable bit in PCHSTRP10 to disable " + "Intel ME...") + pchstrp10 |= (1 << 7) + fdf.write_to(fpsba + 0x28, pack("<I", pchstrp10)) if args.descriptor: print("Removing ME/TXE R/W access to the other flash regions...") - fdf.write_to(fmba + 0x4, pack("<I", 0x04040000)) + if me11: + flmstr2 = 0x00400500 + else: + fdf.seek(fmba + 0x4) + flmstr2 = (unpack("<I", fdf.read(4))[0] | 0x04040000) & 0x0404ffff + + fdf.write_to(fmba + 0x4, pack("<I", flmstr2)) if args.extract_descriptor: if args.truncate: @@ -664,12 +846,14 @@ if __name__ == "__main__": if bios_start == me_end: print("Modifying the regions of the extracted descriptor...") print(" {:08x}:{:08x} me --> {:08x}:{:08x} me" - .format(me_start, me_end - 1, me_start, end_addr - 1)) + .format(me_start, me_end - 1, + me_start, me_start + end_addr - 1)) print(" {:08x}:{:08x} bios --> {:08x}:{:08x} bios" - .format(bios_start, bios_end - 1, end_addr, bios_end - 1)) + .format(bios_start, bios_end - 1, + me_start + end_addr, bios_end - 1)) - flreg1 = start_end_to_flreg(end_addr, bios_end) - flreg2 = start_end_to_flreg(me_start, end_addr) + flreg1 = start_end_to_flreg(me_start + end_addr, bios_end) + flreg2 = start_end_to_flreg(me_start, me_start + end_addr) fdf_copy.seek(frba + 0x4) fdf_copy.write(pack("<II", flreg1, flreg2)) @@ -678,31 +862,33 @@ if __name__ == "__main__": "isn't equal to the end address of the ME\n region: if " "you want to recover the space from the ME region you " "have to\n manually modify the descriptor.\n") - - fdf_copy.close() else: print("Extracting the descriptor to \"{}\"..." .format(args.extract_descriptor)) - fdf.save(args.extract_descriptor, fd_end - fd_start).close() + fdf_copy = fdf.save(args.extract_descriptor, fd_end - fd_start) + + fdf_copy.close() if args.extract_me: if args.truncate: print("Extracting and truncating the ME image to \"{}\"..." .format(args.extract_me)) - mef_copy = mef.save(args.extract_me, end_addr - me_start) + mef_copy = mef.save(args.extract_me, end_addr) else: print("Extracting the ME image to \"{}\"..." .format(args.extract_me)) mef_copy = mef.save(args.extract_me, me_end - me_start) - sys.stdout.write("Checking the FTPR RSA signature of the extracted ME " - "image... ") - print_check_partition_signature(mef_copy, ftpr_offset + - ftpr_mn2_offset - me_start) + if not me6_ignition: + print("Checking the FTPR RSA signature of the extracted ME " + "image... ", end="") + print_check_partition_signature(mef_copy, + ftpr_offset + ftpr_mn2_offset) mef_copy.close() - sys.stdout.write("Checking the FTPR RSA signature... ") - print_check_partition_signature(f, ftpr_offset + ftpr_mn2_offset) + if not me6_ignition: + print("Checking the FTPR RSA signature... ", end="") + print_check_partition_signature(mef, ftpr_offset + ftpr_mn2_offset) f.close() diff --git a/util/me_cleaner/setup.py b/util/me_cleaner/setup.py new file mode 100755 index 0000000000..92fd359ad4 --- /dev/null +++ b/util/me_cleaner/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/python + +from setuptools import setup + +setup( + name="me_cleaner", + version="1.2", + description="Tool for partial deblobbing of Intel ME/TXE firmware images", + url="https://github.com/corna/me_cleaner", + author="Nicola Corna", + author_email="nicola@corna.info", + license="GPLv3+", + scripts=['me_cleaner.py'], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + ] +) |