English:https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
0x00 简介
在本文我们将介绍如何使用直接系统调用(Direct System Calls)以及配合sRDI注入来绕过R3层的行为监控。
随着安全技术的防御能力逐渐增强,另一方面,攻击技术也在不断发展,作为一个Red Team需要研究更先进的技术来绕过当下比较流行的防御和检测机制。
近期一篇恶意代码的研究报告声称,使用"直接系统调用"技术来绕过安全软件用户层Hook的恶意样本正在与日俱增。
研究报告:https://www.cyberbit.com/blog/endpoint-security/malware-mitigation-when-direct-system-calls-are-used/
作为一名ReadTeamer,要与时俱进!! 现在轮到我们也来更新一波shellcode攻击代码了。
我们将接下来将使用这种技术证明,在不触碰磁盘的情况下绕过AV/EDR监控的用户层Hook,使用Cobalt Strike来dump LSASS.exe进程内存。
PoC代码可以在这里下载:https://github.com/outflanknl/Dumpert
0x01 什么是直接系统调用?
为了弄清楚直接系统调用的真正含义,首先我们需要先深入Windows操作系统底层架构。
如果你使用过MS-DOS年代Windows系统,也许你会记得,一个简单的程序崩溃可以引起整个操作系统瘫痪。
这是因为操作系统在实模式(Real Mode)运行,处理器在实模式下运行时,不会有内存隔离的概念(没有严格限制或者声明,哪些内存区域是可以访问,哪些不能访问)。
也就意味者,如果你写的程序出现了bug导致内存破坏(Memory Currption)会导致整个操作系统停止运行。
一直到后来出现了可以支持保护模式的新处理器和操作系统,这一现象才被改变。
为了防止一个进程崩溃导致操作系统也跟着崩溃,在保护模式下引入了许多安全措施,通过虚拟内存(Virtual Memory)和权限级别(Privilege Levels),和一个叫Rings的概念,来隔离运行的不同进程之间,以及进程和操作系统之间的内存访问。
Rings一共有4层,Ring0 ~ Ring3分别对应4个特权级别。
Windows操作系统中实际只使用了两个特权级别:
一个是Ring3层,平时我们所见到的应用程序运行在这一层,所以叫它用户层,也叫User-Mode。所以下次听到别人讲(Ring3、用户层、User-Mode)时,其实是在讲同一个概念。
一个是Ring0层,像操作系统内核(Kernel)这样重要的系统组件,以及设备驱动都是运行在Ring0,内核层,也叫Kernel-Mode。
通过这些保护层来隔离普通的用户程序,不能直接访问内存区域,以及运行在内核模式下的系统资源。
当一个用户层程序需要执行一个特权系统操作,或者访问内核资源时。处理器首先需要切换到Ring0模式下才能执行后面的操作。
切换Ring0的代码,也就是直接系统调用所在的地方。
我们通过监控Notepad.exe进程保存一个.txt文件,来演示一个应用层程序如何切换到内核模式执行的:
上面截图展示了Notepad.exe进程保存一个文件时的执行流程(call stack),从下往上看执行流程。
我们可以看到 notepad调用了kernel32模块中的WriteFile 函数,然后该函数内部又调用了ntdll中的NtWriteFile来到了Ring3与Ring0的临界点。
因为程序保存文件到磁盘上,所以操作系统需要访问相关的文件系统和设备驱动。应用层程序自己是不允许直接访问这些需要特权资源的。
应用程序直接访问设备驱动会引起一些意外的后果(当然操作系统不会出事,最多就是应用程序的执行流程出错导致崩溃)。所以,在进入内核层之前,调用的最后一个用户层API就是负责切换到内核模式的。
CPU中通过执行syscall指令,来进入内核模式,至少x64架构是这样的。我们可以通过下面 WinDBG截图中看到,反汇编的NtWriteFile指令:
把被调用函数相关的参数PUSH到栈上以后,ntdll中的NtWriteFile函数的职责就是,设置EAX为对应的"系统调用号",最后执行syscall指令,CPU就来到了内核模式(Ring0)下执行。
进入内核模式后,内核通过diapatch table(SSDT),来找到和系统调用号对应的Kernel API,然后将用户层栈上的参数,拷贝到内核层的栈中,最后调用内核版本的ZwWriteFile函数。
当内核函数执行完成时,使用几乎相同的方法回到用户层,并返回内核API函数的返回值(指向接收数据的指针或文件句柄)。
像NtWriteFile这样在进入内核层之前的函数,一般也是大部分安全产品,比如:AV、EDR和Sanbox软件经常设置Hook的地方,它们通过Inline Hook来劫持执行流程到自己引擎中,以便完成对一些敏感API的监控,
如果发现任何可疑的参数,则直接返回失败,或弹出窗口警告。
正如上图NtWriteFile函数的反汇编指令,你可能注意到了,它只有短短8行汇编指令,在这些指令中最重要的就是:系统调用号、syscall指令、进入NtWriteFile函数前,PUSH到栈上的正确的参数,以及使用正确的调用约定。
有了上面这些知识的铺垫,我们为何不自己来实现这几行汇编代码,模拟直接系统调用(Direct System Calls)就可以不用再调用NTDLL中的任何函数了,同时我们也Bypass了User-Mode(Ring3)下面任何设置在NTDLL函数中的Hook。
这正是本文的目的,在开始动手之前,我们先来简单了解一下Windows编程接口。
0x02 Windows编程接口
用户层的应用程序要想和底层系统交互,通常使用应用程序编程接口(Application Programming Interface )也就是所谓的API。如果你是编写C/C++应用的Windows程序开发程序员,通常使用 Win32 API。
Win32API是微软封装的一套API接口,由几个DLL(所谓的Win32子系统DLL)组成。在Win32 API下面使用的是Naitve API(ntdll.dll),这个才是真正用户层和系统底层交互的接口,一般称为用户层和内核层之间的桥梁。
但是ntdll中函数大部分都没有被微软记录到官方的开发文档中,为了兼容性问题,大多数情况在写程序时,应该避免直接使用ntdll中的API。
微软在Native API上面又封装一层的神奇之处正是因为Native API是用户层与内核层之间的桥梁,这样就可以在不影响Win32编程接口的情况下对系统结构进行修改。
现在我们对系统调用和Windows编程API有了一些了解,让我们看看如何通过编程来绕过Win32接口层,直接调用系统API并绕过潜在的Ring3层Hook。
0x03 直接使用系统调用
有一个小问题还没有提到,系统调用号会受OS版本影响而变化,有时甚至是Service Pack、内置版本号等。不过不用担心,Google Project Zero项目的 @j00ru 成员统计了所有Windows系统版本中的Native API的系统调用号。
在线查询系统调用号:https://j00ru.vexillium.org/syscalls/nt/64/ 有了这张表,我们可以直接搜索我们想要使用的Native API,就可以看到该API在不同系统中的调用号。
我们需要编写汇编来调用Driect System Calls。 在Virtual Studio项目中需要启用MASM编译依赖的支持,我们才能在项目中添加.asm文件。
0x04 使用直接系统调用,恢复Hook的API
要想通过这种方法来编写一个高级木马来完全绕过用户层API调用几乎是不可能的,至少实现起来非常麻烦,因为这些参数结构的问题、等等。
有时候可能你只是想在恶意代码中使用一个API函数,但是,不曾想这个API的调用堆栈某处早已被一些AV、EDR软件设置了Hook,随时等你上钩。
让我们来看看如何使用直接系统调用来卸载Hook。
通常情况下,基于用户模式的AV、EDR软件通过使用跳转指令(JMP),将API入口处前5个字节修改为指向安全软件的Hook函数。
卸载这种Hook的方法也早被 @SpecialHoang 和@domchell这两位大神公布过了: https://www.mdsec.co.uk/2019/03/silencing-cylance-a-case-study-in-modern-edrs/
如果你仔细研究这些卸载Hook的思路,你会注意到这些方法中用到了诸如:VirtualProtectEx、WriteProcessMemory之类的API来卸载Native API函数的Hook。
但是如果VirtualProtectEx这些API也被Hook和监视了呢?嘿嘿!这下我们就可以通过直接系统调用来卸载这些Hook,不怕半路杀出个程咬金了。
在我们的PoC代码中,基本上和普通卸载Hook的思路一样,恢复被Hook函数的前5字节原始的汇编指令代码。唯一的区别是我们执行恢复时调用的是Direct System Calls函数(ZwProtectVirtualMemory 和ZwWriteVirtualMemory)。
0x05 概念证明
如果你正在进行评估,并且你的攻击场景需要尽可能保持隐蔽,直接在终端上使用Mimikatz并不是最好的方法(即使是在内存中)。另外使用procdump等工具转储LSASS内存通常会被 AV、EDR的Hooks检测到。
因此我们需要一个替代方案来访问LSASS内存,@SpecialHoang 博客中公布了一个方法,先卸载相关函数的Hook,然后再创建LSASS的内存转储。
作为概念证明,我们创建了一个名为”Dumpert“的LSASS内存转储工具。此工具结合了直接系统调用和卸载API Hook,可以让你创建一个LSASS的minidump。并且可能会Bypass一些AV、EDR产品的检测。
得到Minidump文件后,我们就可以在自己机器上使用Mimikatz来提取凭证信息。
0x06 sRDI - (反射式注入DLL)Shellcode Reflective DLL Injection
如果我们不想磁盘落地,就得需要使用某种注入技术。我们可以写一个反射式加载的DLL,但是反射式DLL注入会留下可以被检测到的内存数据。
我的同事@StanHacked告诉我一种称为 "Shellcode Reflective DLL Injection"的DLL注入技术。
sRDI可以把一个普通的DLL文件转换为一段不依赖任何位置的Shellcode,这项技术是被Slient Break Security的Nick Landers(@monoxgas)开发,基本上属于RDI的升级版。
相对于标准RDI,使用SRDI的一些优点:
- 你可以转换任何DLL为无位置依赖的shellcode,并且可以使用标准的shellcode注入技术来使用它。
- 你的DLL中不需要写任何反射加载器代码,因为反射加载器是在DLL外部的shellcode中实现的。
- 合理使用权限,没有大量的RWX权限数据。
- 还可以根据选项,抹掉PE头特征。
想了解更多sRDI细节的朋友,可以参考这篇文章:https://silentbreaksecurity.com/srdi-shellcode-reflective-dll-injection
0x07 是时候展示真正的力量了!
- 我们使用直接系统调用和卸载Hook技术,创建一个DLL版本的"Dumpert"工具。这个DLL可以通过命令行运行:"rundll32.exe c:\Dumpert\outflank-Dumpert.dll,Dump",然后我们将它转换为sRDI shellcode。
- 使用Virtual Studio编译成Dumpert的DLL版本,然后通过sRDI项目中的ConvertToShellcode.py完成:“python3 ConvertToShellcode.py otflank-Dumpert.dll”
- 通过Cobalt Strike的shinject命令讲shellcode注入远程目标。Cobalt Strike支持一种强大的脚本语言,名为aggressor脚本,它可以让你自动完成这一步。
为了让这一步更方便,我们提供了一个aggressor脚本:https://github.com/outflanknl/Dumpert/tree/master/Dumpert-Aggressor,启用脚本以后,可以在beacon菜单中使用dumpert命令来一键搞定!!
- 这个Dumpert脚本通过shinject命令注入sRDI版本的shellcode到当前进程中(为了避免调用CreateRemoteThread API),执行后会等待一会,因为需要lsass的minidump转储成功,并回传!
- 最后,你可以在其他机器上来使用Minikatz命令来提取minidump中的凭证信息:”sekurlsa::minidump c:\Dumpert\dumpert.dmp“。
0x08 总结
能够绕过安全产品Hook的恶意软件正在逐渐增多,我们需要在我们的项目中也嵌入这种技术。
本文中,我们通过编写汇编代码,基于Native API的函数原型、以及不同版本的系统调用号,实现了直接系统函数调用。使用这些函数时,就像是在直接使用Native API中的函数一样。
我们将这种技术与API卸载Hook结合,实现了从LSASS中创建一个minidump,并且使用了sRDI结合Cobalt Strike来注入dumpert shellcode到目标系统内存中。
检测恶意使用系统调用比较困难,由于绕过了用户模式编程接口,因此唯一能查找恶意行为的地方只能内核中。但是由于内核被PatchGuard保护,安全产品更要想在运行的内核中创建钩子或修改内核文件,就更难了!!!
我希望这篇文章能够帮助理解黑客现在使用的这种高级攻击技术,希望这篇文章能在Read Team行动中给大家一些有用的启发!!