我们制作了自己的x86外壳代码仿真器及其工作原理

2020-11-11 18:36:25

检测漏洞是管理程序内存自检(HVMI)的主要优势之一。通过监视访客物理内存页面以防止不同类型的访问(如写入或执行),HVMI可以对关键内存区域施加限制:例如,堆栈或堆页面可以在EPT级别标记为不可执行,因此当利用漏洞成功获得任意代码执行时,自省逻辑将介入并阻止外壳代码的执行。

理论上,拦截来自内存区域(如堆栈或堆)的执行尝试应该足以防止大多数利用漏洞。现实生活往往更加复杂,在很多情况下,合法软件使用的技术可能类似于攻击-浏览器中的即时编译(JIT)就是一个很好的例子。此外,攻击者可能会将其有效负载存储在堆栈或堆之外的其他内存区域中,因此区分好代码和坏代码的方法很有用。

我们将在这篇博客文章中谈论BitDefender Shellcode Emulator,简称bdshemu。Bdshemu是一个库,它能够模拟基本的x86指令,同时观察类似外壳代码的行为。与传统的外壳代码相比,合法代码(如JIT代码)看起来会有所不同,因此bdshemu正在尝试确定:模拟代码的行为是否与外壳代码类似。

Bdshemu是一个用C编写的库,是bddisasm项目的一部分(当然,它利用bddisasm进行指令解码)。Bdshemu库仅用于模拟x86代码,因此不支持API调用。事实上,仿真环境受到高度限制和精简,只有两个内存区域可用:

这两个内存区域都是虚拟化的,这意味着它们实际上是被仿真的实际内存的副本,因此对它们所做的修改不会影响实际的系统状态。仿真代码在这两个区域(我们将分别称为外壳代码和堆栈)之外进行的任何访问都将触发立即终止仿真。例如,API调用将自动导致外壳代码区域之外的分支,从而终止模拟。然而,在bdshemu中,我们所关心的是代码的指令级行为,这足以告诉我们代码是否是恶意的。

虽然bdshemu为检测来宾操作系统内的外壳代码提供了主要基础设施,但值得注意的是,这并不是HVMI确定某个页面的执行是恶意的唯一方式--还使用了另外两个重要指标:

执行的页面位于堆栈上-这在基于堆栈的漏洞中很常见;

当第一次执行页面并且RSP寄存器指向分配给线程的正常堆栈之外时,堆栈被旋转;

这两个指示器本身就足以触发攻击检测。如果这些代码没有被触发,则使用bdshemu来仔细查看执行的代码,并决定是否应该阻止它。

Bdshemu是作为一个独立的C库创建的,它只依赖于bddisasm。使用bdshemu相当简单,就像bddisasm一样,它是一个单一API库:

仿真器需要一个SHEMU_CONTEXT参数,该参数包含模拟可疑代码所需的所有信息。此上下文分为两个部分-输入参数和输出参数。输入参数必须由调用方提供,并且它们包含要仿真的代码或初始寄存器值等信息。输出参数包含诸如bdshemu检测到什么外壳代码指示符之类的信息。所有这些领域都在源代码中得到了很好的记录。

最初,使用以下主要信息填充上下文(请注意,仿真结果可能会根据提供的寄存器和堆栈的值而变化):

输入寄存器,如段、通用寄存器、MMX和SSE寄存器;如果它们未知或不相关,可以将它们留为0;

环境信息,如模式(32或64位)或振铃(0、1、2或3);

控制参数,例如最小堆栈串长度、最小NOP滑板长度或应该仿真的最大指令数;

主要输出参数是Flags字段,该字段包含在模拟期间检测到的外壳代码指示符的列表。通常,此字段的非零值强烈表明仿真代码实际上是外壳代码。

Bdshemu被构建为一个简单、快速、简单的x86指令仿真器:因为它只与外壳代码本身和一个小的虚拟堆栈一起工作,所以它不必模拟任何体系结构细节-中断或异常、描述符表、页表等。此外,由于我们只处理外壳代码和堆栈内存,bdshemu不执行内存访问检查,因为它甚至不允许访问其他地址。除了可以访问的寄存器之外,唯一可以访问的状态是外壳代码本身和堆栈,两者都是实际内存内容的副本-系统状态在仿真期间永远不会修改,只有提供的SHEMU_CONTEXT才会修改。这使得bdshemu非常快速、简单,并让我们将重点放在它的主要用途上:检测外壳代码。

就指令支持而言,bdshemu支持所有基本的x86指令,如分支、算术、逻辑、移位、位操作、乘除、堆栈访问和数据传输指令。此外,它还支持其他指令,例如一些基本的MMX或AVX指令-PUNPCKLBW或VPBROADCAST就是两个很好的例子。

为了确定模拟代码的行为是否与外壳代码类似,bdshemu使用了几个指示符。

这是外壳代码的经典表示形式;因为在获得代码执行时外壳代码的确切入口点可能是未知的,所以攻击者通常会在前面加上一长串NOP指令,编码为0x90。在调用仿真器时,可以通过NopThreshold上下文字段控制NOP-SLED长度的参数。默认值为SHEMU_DEFAULT_NOP_THRESHOLD,即75,这意味着所有仿真指令的最低75%必须是NOP。

外壳代码被设计为无论加载到哪个地址都能正常工作。这意味着外壳代码必须在运行时动态确定加载它的地址,因此可以用某种形式的相对寻址代替绝对寻址。这通常是通过使用众所周知的技术检索指令指针的值来实现的:

调用$+5/POP EBP-执行这两条指令将导致指令指针的值存储在EBP寄存器中;然后可以使用相对于EBP值的偏移量在外壳代码内访问数据;

FNOP/FNSTENV[esp-0xc]/POP EDI-第一条指令是任何FPU指令(不一定是FNOP),第二条指令FNSTENV将FPU环境保存在堆栈上;第三条指令将从esp-0xc检索FPU指令指针,它是FPU环境的一部分,包含最后执行的FPU的地址-在我们的例子中是FNOP;从那时起,可以使用相对于EDI的寻址来访问外壳。

在内部,bdshemu跟踪保存在堆栈上的指令指针的所有实例。稍后,以任何方式从堆栈加载该指令指针都会导致触发此检测。由于bdshemu跟踪保存的指令指针的方式,外壳代码何时、何地或如何尝试将RIP加载到寄存器并使用它并不重要,bdshemu将始终触发检测。

在64位中,可以直接使用RIP相对寻址,因为指令编码允许这样做。然而,令人惊讶的是,大量外壳代码仍然使用检索指令指针的经典方法(通常是调用/弹出技术),这有点奇怪,但这可能表明32位外壳代码只需极少的修改即可移植到64位。

最常见的情况是,外壳代码以编码或加密的形式出现,以避免某些恶意字符(例如,外壳代码中应该类似于字符串的0x00可能会破坏利用漏洞)或避免被安全技术(例如,反病毒扫描仪)检测到。这意味着在运行时,外壳代码必须自己解码(通常是就地解码),方法是修改自己的内容,然后执行纯文本代码。典型的解码方法涉及基于XOR或ADD的解密算法。

当然,bdshemu遵循这种行为,并在内部跟踪外壳代码中修改的每个字节。只要可疑的外壳代码写入自身的任何部分,然后执行它,就会触发自写检测。

一旦外壳代码获得代码执行,它就需要在各个模块中定位几个函数,以便承载其实际有效负载(例如,下载文件或创建进程)。在Windows上,最常见的方法是解析用户模式加载器结构,以便找到加载所需模块的地址,然后在这些模块中定位所需的函数。外壳代码将访问的结构顺序为:

线程环境块(TEB),位于fs:[0](32位线程)或gs:[0](64位线程);

进程环境块(PEB),位于TEB+0x30(32位)或TEB+0x60(64位)。

在PEB_LDR_DATA中,有几个包含已加载模块的列表。外壳代码将遍历这些列表,以便找到急需的库和函数。

在每次内存访问时,bdshemu将查看外壳代码是否尝试访问TEB内的PEB字段。BDSHEMU将跟踪存储器访问,即使它们是在没有经典的FS/GS段前缀的情况下进行的-只要识别到对TEB内的PEB字段的访问,就会触发TIB访问检测。

合法代码将依赖几个库来调用操作系统服务-例如,为了创建进程,正常代码将调用Windows上的CreateProcess函数之一。合法代码直接调用syscall的情况并不常见,因为syscall接口可能会随着时间的推移而改变。因此,只要bdshemu发现可疑的外壳代码使用syscall/SYSENTER/INT指令直接调用系统服务,就会触发syscall检测。

外壳代码屏蔽其内容的另一种常见方式是在堆栈上动态构造字符串。这可以消除编写位置无关代码(PIC)的需要,因为外壳代码将在堆栈上动态构建所需的字符串,而不是将它们作为常规数据在外壳代码内引用。实现这一点的典型方法是将字符串内容保存在堆栈上,然后使用堆栈指针引用该字符串:

上述代码最终会将字符串calc.exe存储在堆栈上,然后可以在整个外壳代码中将其用作普通字符串。

对于保存在堆栈上的每个类似于字符串的值,bdshemu都会跟踪堆栈上构造的字符串的总长度。一旦超过上下文中StrLength字段指示的阈值,就会触发堆栈字符串检测。此字段的默认值为SHEMU_DEFAULT_STR_THRESHOLD,等于8,这意味着在堆栈上动态构造等于或超过8个字符的字符串将触发此检测。

虽然上述技术是通用的,可以应用于任何操作系统和32位或64位的任何外壳代码(除了特定于Windows的TIB访问检测),但bdshemu也能够确定一些内核特定的外壳代码行为。

内核处理器控制区(KPCR)是Windows系统上的每个处理器的结构,其中包含许多对内核至关重要的信息,但也可能对攻击者有用。通常,外壳代码希望引用当前执行的线程,在32位系统上的偏移量为0x124,在64位系统上的偏移量为0x188,可以通过访问kpcr结构来检索该线程。

就像TIB访问检测技术一样,bdshemu跟踪内存访问,当仿真代码从KPCR读取当前线程时,它将触发KPCR访问检测。

SWAPGS是一条系统指令,只有在从用户模式转换到内核模式时才会执行,反之亦然。有时,由于某些内核攻击的特殊性,攻击者最终需要执行SWAPGS-例如,著名的EternalBlues内核有效负载拦截了syscall处理程序,因此它需要在syscall发生时执行SWAPGS,就像普通的系统调用所做的那样。

每当bdshemu遇到由可疑外壳代码执行的SWAPGS指令时,它都会触发SWAPGS检测。

一些外壳代码(如前面提到的EternalBlue内核有效负载)必须修改syscall处理程序才能迁移到稳定的执行环境(例如,因为初始外壳代码在较高的IRQL下执行,在调用有用的例程之前需要降低IRQL)。这是通过使用WRMSR指令修改系统调用MSR,然后等待系统调用执行(位于较低的IRQL)以继续执行(这也是SWAPGS技术派上用场的地方,因为SWAPGS必须在64位上的每个系统调用之后执行)来完成。

此外,为了在内存中定位内核镜像以及随后有用的内核例程,一种快速而简单的技术是查询syscall MSR(通常指向内核镜像内的syscall处理程序),然后向后遍历页面,直到找到内核镜像的开头。

只要可疑的外壳代码访问syscall MSR(在32位或64位模式下),bdshemu就会触发MSR访问检测。

Bdshemu项目包含一些合成测试用例,但演示其功能的最佳方式是使用真实的外壳代码。在这一点上,Metasploit在使用各种编码器生成不同类型的有效负载方面非常出色。让我们以下面的外壳代码作为一个纯粹的说教例子:

DA C8 D9 74 24 F4 5F 8D 7F 4A 89 FD 81 ED FE FFFF B9 61 00 00 00 8B 75 00 C1 E6 10 C1 EE 10 83 C5 02 FF 37 5A C1 E2 10 C1 EA 10 89 D3 09 F3 21 F2 F7 D2 21 DA 66 66 52 66 8F 07 6A 02 03 3C 24 5B 49 85 C9 0F 85 CD FF 1C B3 E0 5B 62 5B 5B 02 D2 E7 E3 E3 27 87 AC D。0D 4D 5F 5D D4 17 E8 9C A4 8D DC 6E 94 6F 45 3E CE 67 EE 66 3D ED 74 F5 97 CF DE 44 EA CF EB 19 DA E6 76 27 B9 2A B8 ED 80 0D F5 FB F6 86 0E BD 73 99 06 7D 5E F6 06 D2 07 01 61 8A 6D C1 E6 99 FA 98 29 13 2D 98 2C 48 A5 0C 81 28 DA 73 BB 2A E1 7B 1E 9B 41 C41。A0 2D DC 27 5C DC BC A9 B9 12 FE 01 8C 6E E6 6E B5 91 60 F2 01 9E 62 B0 07 C8 62 C8 8C。

将其另存为二进制文件shellcode.bin,然后查看其内容会生成一个密集打包的代码块,这高度表明是加密的外壳代码:

使用bddisasm项目中提供的disasmtool,可以使用-shemu选项在输入上运行外壳代码仿真器。

在我们的外壳代码上运行它将显示有关每个仿真指令的逐步信息,但是因为该跟踪很长,所以让我们直接跳到if的末尾:

Emulating:0x0000000000200053 XOR eax,eax RAX=0x0000000000000000 RCX=0x0000000000000000 RDX=0x000000000000ee00 RBX=0x0000000000000002 RSP=0x0000000000100fd4 RBP=0x0000000000100fd4 RSI=0x0000000000008cc8 RDI=0x000000000020010c R8=0x0000000000000000 R9=0x0000000000000000 R10=0x0000000000000000 R11=0x0000000000000000 R12=0x0000000000000000 R13=0x0000000000000000 R14=0x0000000000000000 R15=0x0000000000000000 RIP=0x0000000000200055 RFLAGS=0x0000000000000246Emulating:0x0000000000200055 MOV edx,dword ptr fs:[eax+0x30]Emulation terminated with status 0x00000001,flags:0xe,0 NOPs SHEMU_FLAG_LOAD_RIP SHEMU_FLAG_WRITE_SELF SHEMU_FLAG_TIB_ACCESS。

我们可以看到,最后一条模拟指令是MOV edX,dword PTR fs:[EAX+0x30],这是TEB访问指令,但也会触发模拟停止,因为它是外壳代码存储器之外的访问(请记住,bdshemu将在外壳代码或堆栈之外的第一次存储器访问时停止)。此外,这个小的外壳代码(使用Metasploit生成)在bdshemu中触发了3次检测:

SHEMU_FLAG_LOAD_RIP-外壳代码将RIP加载到通用寄存器中,以定位其在内存中的位置;

SHEMU_FLAG_TIB_ACCESS-外壳代码继续访问PEB,以定位重要的库和函数;

这些指标足以断定,仿真代码无疑是外壳代码。关于bdshemu,更令人敬畏的是,通常在仿真结束时,内存将包含外壳代码的解密形式。Disasmtool非常好,可以在模拟完成后保存外壳代码内存-创建了一个名为shellcode.bin_decded.bin的新文件,该文件现在包含已解码的外壳代码;让我们来看看它:

查看解码的外壳代码,人们不仅可以立即看到它是不同的,而且这是纯文本-敏锐的眼睛会很快识别出外壳代码末尾的calc.exe字符串,提示我们这是一个典型的calc.exe派生外壳代码。

我们在这篇博文中介绍了BitDefender外壳代码仿真器,它是HVMI漏洞检测技术的关键部分。Bdshemu的构建目的是在二进制代码级别检测外壳代码指示符,而无需模拟复杂的API调用、复杂的内存布局或复杂的体系结构实体(如页表、描述符表等)。bdshemu专注于什么是最重要的,模拟指令并确定它们的行为是否像外壳代码。

由于其简单性,bdshemu适用于针对任何操作系统的外壳代码,因为大多数检测技术都特定于指令级行为,而不是API调用等高级行为。此外,它既可以处理32位和64位代码,也可以处理特定于用户或内核的代码。