首页
社区
课程
招聘
[翻译]红队战术: 结合直接系统调用和sRDI来绕过AV / EDR
2019-8-3 21:36 26589

[翻译]红队战术: 结合直接系统调用和sRDI来绕过AV / EDR

2019-8-3 21:36
26589
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文件,来演示一个应用层程序如何切换到内核模式执行的:


                                                           WriteFile call stack in Process Monitor .


上面截图展示了Notepad.exe进程保存一个文件时的执行流程(call stack),从下往上看执行流程。


我们可以看到 notepad调用了kernel32模块中的WriteFile 函数,然后该函数内部又调用了ntdll中的NtWriteFile来到了Ring3与Ring0的临界点。


因为程序保存文件到磁盘上,所以操作系统需要访问相关的文件系统和设备驱动。应用层程序自己是不允许直接访问这些需要特权资源的。


应用程序直接访问设备驱动会引起一些意外的后果(当然操作系统不会出事,最多就是应用程序的执行流程出错导致崩溃)。所以,在进入内核层之前,调用的最后一个用户层API就是负责切换到内核模式的。


CPU中通过执行syscall指令,来进入内核模式,至少x64架构是这样的。我们可以通过下面 WinDBG截图中看到,反汇编的NtWriteFile指令:


                                                   Disassembled NtWriteFile API call in WinDBG.

把被调用函数相关的参数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编程接口


​下图中展示的是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 直接使用系统调用


我们前面已经展示了如何使用反汇编来找到Native API对应的系统调用号。使用调试器可能有点小麻烦,这次我们选择使用IDA来打开一个ntdll.dll文件的副本,来找到需要的NativeAPI。


                                               Disassembled NtWriteFile API call in IDA.

有一个小问题还没有提到,系统调用号会受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文件。



                 Assembly system call functions in .asm file.

现在我们只需要获取目标系统版本信息,并使用汇编语言定义函数时,带上具体的系统版信息。我们可以通过Native API  RtlGetVersion()获取系统版本信息,并将信息保存到一个版本信息的结构中。


                                            Reference function pointers based on OS info.

在VS代码中导出asm文件中定义的函数,并定义其函数原型:


                    Exported OS specific assembly functions + native API function definitions.

现在可以在我们代码中使用这些定义的System Call函数了,而不需要经过Native API这一层:


                        Using ZwOpenProcess systemcall function as a Native API call.


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



                                                          Using direct system call function to unhook APIs.

0x05 概念证明


​在攻击过程中,通常我们需要使用Mimikatz来获取目标系统上的凭证、Hashes、Kerberos票据。如今,终端检测软件和情报分析系统在检测和预防Mimikatz方面做的相当不错。


如果你正在进行评估,并且你的攻击场景需要尽可能保持隐蔽,直接在终端上使用Mimikatz并不是最好的方法(即使是在内存中)。另外使用procdump等工具转储LSASS内存通常会被 AV、EDR的Hooks检测到。


因此我们需要一个替代方案来访问LSASS内存,@SpecialHoang 博客中公布了一个方法,先卸载相关函数的Hook,然后再创建LSASS的内存转储。



作为概念证明,我们创建了一个名为”Dumpert“的LSASS内存转储工具。此工具结合了直接系统调用和卸载API  Hook,可以让你创建一个LSASS的minidump。并且可能会Bypass一些AV、EDR产品的检测。




得到Minidump文件后,我们就可以在自己机器上使用Mimikatz来提取凭证信息。



                                             Mimikatz minidump import.

当然了,在目标系统中释放一个可执行文件,并不是我们想要的,我们需要进一步优化它。

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 是时候展示真正的力量了!


目前我们具备了所需要的技术,来看看我们能否把这些技术结合起来,创建一些在Read Team行动过程中更有用的功能!!!

  • 我们使用直接系统调用和卸载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行动中给大家一些有用的启发!!




任何反馈或其它想法,请在下方留言! :)




[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-8-4 10:09 被Adventure编辑 ,原因: 修正错别字
收藏
点赞5
打赏
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  mb_ovrzbwwl   +2.00 2019/08/07 有事找你处理,如果有时间请加Q7620971,给报酬!!!
最新回复 (12)
雪    币: 5834
活跃值: (1863)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
小迪xiaodi 1 2019-8-4 12:18
2
0
不明觉厉
雪    币: 9344
活跃值: (3371)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
superlover 2019-8-4 18:17
3
0
PatchGuard还真是防君子不防小人
雪    币: 6977
活跃值: (1775)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
TopC 2019-8-5 18:02
4
0
想法可以的~棒!
雪    币: 378
活跃值: (2857)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
appview 2019-8-6 15:36
5
0
good
雪    币: 149
活跃值: (2028)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
saloyun 2019-8-18 11:53
6
0
厉害,mark一下.
雪    币: 9113
活跃值: (1996)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
PPTV 2019-10-8 00:36
7
0
mark,直接系统调用。
雪    币: 83
活跃值: (1047)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2019-10-19 15:06
8
0
good
雪    币: 73
活跃值: (3040)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
不懂就不懂 2 2019-11-26 17:20
9
0
卧槽,正好有个样本就是这么做的。。。
雪    币: 370
活跃值: (256)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
0aW5n 2021-7-17 09:56
10
0
nice啊,写的真不错
雪    币: 143
活跃值: (775)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
月生沧海 2021-8-6 16:22
11
0
这ZwProtectVirtualMemory未导出能这样用吗???
雪    币: 47
活跃值: (356)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xuha 2023-7-3 21:59
12
0
联想到Linux 中的Shellcode  就是写汇编代码直接实现系统调用
雪    币: 1158
活跃值: (389)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
ranshu 2023-7-4 08:15
13
0
感谢分享
游客
登录 | 注册 方可回帖
返回