攻击者是如何利用Windows RPC绕过 CFG 防御机制的

2021-04-21 8,739

攻击者是如何利用Windows RPC绕过 CFG 防御机制的

原文地址:https://iamelli0t.github.io/2021/04/10/RPC-Bypass-CFG.html

 

在利用浏览器渲染进程中的漏洞的时候,通常的做法是:首先,利用该漏洞获取用户模式下的任意内存读写原语,篡改DOM/js对象的vtable结构体,从而实现代码执行流的劫持。然后,通过ROP链调用VirtualProtect,将shellcode所在内存空间的属性修改为PAGE_EXECUTE_READWRITE,最后,通过ROP链将代码执行流跳转到shellcode。但是,在Windows 8.1版本之后,微软引入了CFG(Control Flow Guard,CFG)[1]缓解措施来检测间接的函数调用,从而堵上了攻击者通过篡改vtable结构体来实现代码执行的老路。

 

然而,猫鼠游戏并没有结束。不久之后,绕过CFG缓解措施的新方法就出现了。例如,在chakra/jscript9中,攻击者可以通过篡改堆栈上的函数返回地址来劫持代码执行流;在v8中,攻击者可以利用具有可执行内存属性的WebAssembly来执行shellcode。2020年12月,微软在Windows 10 20H1版本中推出了基于英特尔Tiger Lake CPU的CET(Control-flow Enforcement Technology,CET)[2]缓解技术,以防止攻击者篡改堆栈上的函数返回地址。因此,如何在启用了CET保护机制的环境下绕过CFG已经成为漏洞利用的新问题。

 

在分析CVE-2021-26411漏洞的在野样本时,我们发现了一种利用Windows RPC(Remote Procedure Call,RPC)绕过CFG缓解机制的新方法[3]。并且,这种方法不依赖于ROP链。因此,只要构造相应的RPC_MESSAGE,攻击者就可以通过手动调用rpcrt4!NdrServerCall2来实现任意代码执行。 

简要回顾CVE-2021-26411漏洞

“CVE-2021-26411:Internet Explorer mshtml Use-Afere-Free”一文中,我们已经分析了该漏洞的成因:removeAttributeNode()触发了属性对象nodeValue的回调函数valueOf。在这个回调函数的执行过程中,通过手动调用clearAttributes()函数,就可以将保存在nodeValue对象中的BSTR提前释放。当回调函数valueOf返回后,并没有检查nodeValue对象是否存在,从而导致UAF漏洞。

 

微软对该漏洞的修复方法是:利用CAttrArray::Destroy函数删除该对象之前,对索引进行相应的检查:

 

image.png 

 

对于这种内存大小可控的UAF漏洞来说,利用的思路是:使用两种不同类型的指针(BSTR和Dictionary.Items)指向重用的内存,然后通过类型混淆漏洞实现指针泄漏和指针解引用(pointer dereference):

 

image.png 

 

Windows RPC简介与利用方法

 

Windows RPC用于支持分布式客户端/服务器函数调用的场景。利用Windows RPC,客户端可以像调用本地函数一样调用服务器函数。Windows RPC的基本架构如下所示:

 

image.png 

 

其中,客户机/服务器程序会把调用参数或返回值传递给低层的Stub函数。而Stub函数则负责将数据封装成NDR(Network Data Representation,NDR)格式。同时,与运行库的通信则由rpcrt4.dll提供。

 

下面给出了一个idl示例: 

 

[

uuid("1BC6D261-B697-47C2-AF83-8AE25922C0FF"),

version(1.0)

]

 

interface HelloRPC

{

int add(int x, int y);

}

 

当客户端调用add函数时,服务器会收到来自rpcrt4.dll的处理请求,并调用rpcrt4!NdrServerCall2函数:

 

image.png 

 

实际上,函数rpcrt4!NdrServerCall2只有一个参数PRPC_MESSAGE,其中包含函数索引及相关参数等重要数据。服务器的RPC_MESSAGE结构体及其主要的子数据结构体如下图所示(32位):

 

image.png 

 

如上图所示,在RPC_MESSAGE结构体中,函数调用的两个重要变量是Buffer和RpcInterfaceInformation。其中,Buffer中存储的是函数的参数,而RpcInterfaceInformation则指向RPC_SERVER_INTERFACE结构体。此外,RPC_SERVER_INTERFACE结构体则保存了服务器程序的接口信息,其中DispatchTable(+0x2c)保存了运行时库和存根函数的接口函数指针,而InterpreterInfo(+0x3c)则指向MIDL_SERVER_INFO结构体。此外,MIDL_SERVER_INFO结构体保存了服务器IDL接口信息,而DispatchTable(+0x4)则保存了服务器例程函数的指针数组。

 

下面,我们以RPC_MESSAGE结构体为例进行介绍。

 

根据上面给出的idl,当客户端调用add(0x111,0x222)时,服务器程序将在rpcrt4!NdrServerCall2处中断:

 

image.png 

 

我们可以看出,动态调试内存转储与RPC_MESSAGE结构体的分析一致,并且add函数被存储在MIDL_SERVER_INFO.DispatchTable中。

 

接下来,我们分析rpcrt4!NdrServerCall2是如何根据RPC_MESSAGE来调用add函数的。 

 

其中,函数rpcrt4!NdrServerCall2会在内部调用rpcrt4!NdrStubCall2,而后者会根据MIDL_SERVER_INFO.DispatchTable和RPC_MESSAGE.ProcNum计算出函数指针的地址,并将函数指针、函数参数和参数长度传递给rpcrt4!Invoke:

 

image.png 

 

最后,函数rpcrt4!Invoke将调用服务器提供的例程函数,具体如下所示:

 

image.png 

 

基于以上分析,在实现任意内存读写原语后,我们可以构造一个伪造的RPC_MESSAGE,以设置想要调用的函数指针和函数参数,然后手动调用rpcrt4!NdrServerCall2来实现任意函数的执行。

 

接下来,我们还需要解决两个问题:

 

1)如何在javascript中调用rpcrt4!NdrServerCall2;

2)观察rpcrt4!Invoke中的服务器例程函数调用时,我们可以看到:

 

image.png 

 

如您所见,这是一个间接函数调用,而且进行了相应的CFG检查。因此,我们面临的问题是:在篡改MIDL_SERVER_INFO.DispatchTable函数指针后,如何才能绕过这里的CFG保护呢?

 

我们先来解决第一个问题:如何在javascript中调用rpcrt4!NdrServerCall2?

 

实际上,我们可以用rpcrt4!NdrServerCall2替换DOM对象vtable的函数指针。这是因为rpcrt4!NdrServerCall2是一个保存在CFGBitmap中的合法指针,所以它完全可以顺利通过CFG检查。在本示例中,我们将MSHTML!CAttribute::normalize替换为rpcrt4!NdrServerCall2,并在javascript代码中通过“xyz.normalize()”语句来调用rpcrt4!NdrServerCall2。

 

接下来,我们开始解决第2个问题:如何绕过rpcrt4!NdrServerCall2中的CFG保护机制?

 

就本示例来说,我们采用的方法是:

 

1)使用伪造的RPC_MESSAGE和rpcrt4!NdrServerCall2调用VirtualProtect,并修改RPCRT4!__guard_check_icall_fptr的内存属性为PAGE_EXECUTE_READWRITE。

2)将保存在rpcrt4!__guard_check_icall_fptr中的指针ntdll!LdrpValidateUserCallTarget替换为ntdll!KiFastSystemCallRet,以干掉rpcrt4.dll中的CFG检查。

3)恢复RPCRT4!__guard_check_icall_fptr的内存属性。 

 

function killCfg(addr) {

  var cfgobj = new CFGObject(addr)

  if (!cfgobj.getCFGValue())

    return

  var guard_check_icall_fptr_address = cfgobj.getCFGAddress()

  var KiFastSystemCallRet = getProcAddr(ntdll, 'KiFastSystemCallRet')

  var tmpBuffer = createArrayBuffer(4)

  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, 0x40, tmpBuffer])

  write(guard_check_icall_fptr_address, KiFastSystemCallRet, 32)

  call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, read(tmpBuffer, 32), tmpBuffer])

  map.delete(tmpBuffer)

}

 

解决了这两个问题后,我们就可以用伪造的RPC_MESSAGE来调用任何函数指针所指向的代码了,其中包括在缓冲区存储的shellcode,因为rpcrt4.dll中的CFG检查已经被干掉了。

 

最后,本示例将shellcode写到了msi.dll+0x5000处,并通过rpcrt4!NdrServerCall2来调用该shellcode: 

 

var shellcode = new Uint8Array([0xcc])

var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000

var tmpBuffer = createArrayBuffer(4)

call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])

writeData(msi, shellcode)

call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])

call2(msi, [])

 

漏洞利用效果如下图所示:

 

image.png 

小结

 

在本文中,我们为读者详细介绍了某恶意软件样本利用CVE-2021-26411漏洞通过Windows RPC绕过CFG缓解机制的新型手段。这种方法的特点是,无需构建ROP链,直接通过伪造的RPC_MESSAGE即可实现任意代码执行。这种漏洞利用技术不仅简单有效,并且还非常稳定。我们完全有理由相信,它必将成为绕过CFG缓解机制的一种新型、有效的攻击技术。

 

参考资料 

 

[1] https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard

[2] https://windows-internals.com/cet-on-windows/

[3] https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page

 


本文作者:mssp299

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/157564.html

Tags:
评论  (0)
快来写下你的想法吧!

mssp299

文章数:51 积分: 662

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号