汇编Shellcode的技巧

汇编Shellcode的技巧

来源  https://www.4hou.com/technology/3893.html

 

本文参考来源于pentest

我们在上一篇提到要要自定义shellcode,不过由于这是个复杂的过程,我们只能专门写一篇了,本文,我们将会给大家介绍shellcode的基本概念,shellcode在编码器及解码器中的汇编以及几种绕过安全检测的解决方案,例如如何绕过微软的 EMET(一款用以减少软件漏洞被利用的安全软件)。为了理解本文的内容,大家需要具了解x86汇编知识和基本文件格式(如COFF和PE)。

专业术语

进程环境块(PEB):PEB(Process Environment Block)是Windows NT操作系统中的一种数据结构。由于PEB仅在Windows NT操作系统内部使用,其大多数字段不面向其他操作系统,所以PEB就不是一个透明的数据结构。微软已在其MSDN Library技术开发文档中开始修改PEB的结构属性。它包含了映像加载器、堆管理器和其他的windows系统DLL所需要的信息,因为需要在用户模式下修改PEB中的信息,所以必须位于用户空间。PEB存放进程信息,每个进程都有自己的 PEB 信息。准确的 PEB 地址应从系统的 EPROCESS 结构的 1b0H 偏移处获得,但由于 EPROCESS在进程的核心内存区,所以程序不能直接访问。

导入地址表(IAT):IAT (Import Address Table)由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中,当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。

数据执行保护(DEP):与防病毒程序不同,数据执行保护(DEP)是一组对内存区域进行监控的硬件和软件技术,DEP技术的目的并不是防止在计算机上安装有害程序。 而是监视你的已安装程序,帮助确定它们是否正在安全地使用系统内存。在Microsoft Windows XP Service Pack 2(SP2)和Microsoft Windows XP Tablet PC的2005版中,DEP是通过硬件和软件来运行的。DEP的主要优点是阻止代码在数据页中运行。通常,代码不会在默认堆和堆栈中执行。而硬件可以强制DEP检测在这些位置运行的代码,当运行发生异常时,软件就可以通过DEP阻止恶意代码利用Windows中的异常处理机制。

stdcall调用约定:__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。调用约定是用来清理堆栈的,和__cdecl类似,stdcall调用约定也是按从右至左的顺序压参数入栈,由调用者把参数弹出栈。stdcall调用是把函数参数列表的前三个参数放入寄存器eax,edx,ecx

。返回值存储在EAX寄存器中。 stdcall是Microsoft Win32 API和Open Watcom C ++的标准调用约定。stdcall是微软win32 API和open Watcom C++的标准调用约定。

Shellcodes在网络安全领域起着非常重要的作用,它们被广泛应用于许多恶意软件和漏洞。那么,为什么shellcode会如此受到恶意程序的欢迎? 

Shellcode实际是一段代码(也可以是填充数据),是用来发送到服务器利用特定漏洞的代码,一般可以获取权限。另外,Shellcode一般是作为数据发送给受攻击服务器的, Shellcode是溢出程序和蠕虫病毒的核心。

之所以命名为shellcode,是因为shellcode运行完以后都会返回一个命令行shell,但如今几乎所有的编译器生成的程序都能转化为shellcode。因为编写shellcode涉及到深入理解目标架构和操作系统的汇编语言。虽然我们可以在网上找到很多开源的shellcode,但为了达到不同的使用目的,每个安全研究员都会自己编写一个各自的shellcode,同时重新编写的过程也是理解shellcode的过程。

所以,本文的目标就是要教你如何在不同的检测环境下,重编自己的shellcode。

基本的Shellcode编程

与Windows不同,为不同操作系统而编写的shellcode需要不同的编写方法,比如基于UNIX的操作系统提供了通过int 0x80接口与内核通信的直接方式,基于UNIX的操作系统中的所有系统调用都有唯一的编号,比如int 0x80就代表系统调用时设置中断向量号0x80的中断描述符,内核使用给定的数字和参数执行系统调用,但是这里会有一个问题,Windows系统并没有直接的内核接口,这意味着系统调用必须有精确内存地址(函数指针),但经过硬编码的函数地址并不能完全解决这个问题,因为Windows系统的每个函数地址会因为版本中甚至配置不同而发生变化,所以,shellcode的编写一定要考虑系统的版本,如果要在Windows上编写不受系统版本影响的shellcode,我们只要解决地址问题,这可以通过在运行时动态地找到函数地址来实现。

解决寻址问题

在本文中,我们将会使用解析PEB的方法来解决寻址的问题,简单来说,该方法就是使用PEB数据结构来定位基地址的加载DLL,从而解析出EAT及其函数地址,目前几乎所有的不受系统版本影响的shellcode的编写都是用这个技术得到Windows API函数地址的。

在解析PEB的方法中,我们利用“FS”段寄存器,“FS”段寄存器可以指向在微软系统环境中线程环境块(TEB)的基地址,TEB是在用户模式中分配和初始化的内存块,TEB耗用一个内存页(4KB),主要包括:线程异常处理链首、线程本地存储数据以及由GDI和OpenGL图形使用的一些数据结构,当在内存区运行shellcode时,我们需要将TEB向前偏移48字节,

如下图所示,现在我们就找到了PEB的结构体指针,

在获得PEB的结构体指针后,现在我们将PEB结构的起始位置向前偏移12个字节,以便获得PEB块内的LDR数据结构的地址,

LDR数据结构包含关于进程已加载模块的信息,如果我们把LDR数据结构再进一步移动20个字节,我们就可以获得进程dll中的 “InMemoryOrderModuleList” 加载模块,

现在我们的指针指向InMemoryOrderModuleList,它是一个LIST_ENTRY结构,Windows将这个结构定义为一个“包含加载的进程模块的双向链表头”。列表中的每个项都是一个指向LDR_DATA_TABLE_ENTRY结构的指针,这个结构是我们的主要目标,它包含加载的DLL(模块)的全名和基地址,因为加载的模块的顺序可以改变,我们应该检查全名,以便选择正确的DLL,其中包含函数正在寻找,这可以很容易做到从LDR_DATA_TABLE_ENTRY开始向前移动40个字节,如果DLL名称匹配我们正在寻找的,我们可以继续,在LDR_DATA_TABLE_ENTRY内向前移动16个字节,我们现在终于有基地加载的DLL的地址,

现在PEB的指针指向了InMemoryOrderModuleList,InMemoryOrderModuleList是一个LIST_ENTRY结构,微软将其定义为“包含加载进程模块的双向链表头”。

由于InMemoryOrderModuleList字段是一个指针,指向LDR_DATA_TABLE_ENTRY 结构体上的LIST_ENTRY字段。但是它不是指向LDR_DATA_TABLE_ENTRY 起始位置的指针,而是指向这个结构的InMemoryOrderLinks字段。所以我们需要想办法让InMemoryOrderModuleList指向LDR_DATA_TABLE_ENTRY。

LDR_DATA_TABLE_ENTRY包含加载模块的所有地址和名称,因为模块加载的顺序可能改变,我们在加载的时候,应该校验加载模块的全名,以便选择包含我们要查找的函数的动态库。我们可以把LDR_DATA_TABLE_ENTRY向前位移40个字节,如果DLL名称匹配,则证明是我们需要的动态库,然后我们继续把LDR_DATA_TABLE_ENTRY向前位移16个字节,就能得到具有基地址的加载模块了。

至此,我们就完成了获得函数地址的第一步,现在我们有了包含所需函数的DLL的基地址,所以,我们就必须解析DLL的导出地址表,以便能找到所需的函数地址,导出地址表位于能在PE头部的可选位置内,从基址向前移60个字节,我们就得到了含有DLL的PE头的内存地址,

最后我们需要用“模块基地址+ PE头地址+ 120字节”这个公式计算出地址表的地址,至此,地址表的地址就被导出了,在得到EAT地址后,我们就可以访问由DLL导出的所有函数,IMAGE_EXPORT_DIRECTORY的结构如下所示,

此结构包含导出函数的地址,名称和数量,使用相同大小的遍历统计计算,可以在此结构内获得所需的函数地址,当然,导出函数的顺序可能会因系统版本不同而异,因此我们要在获取函数地址之前,对函数名称进行校验,在确定函数名称之后,函数地址的计算就好比计算几个Windows数据结构的大小,因为你可以理解这个方法是关于计算几个Windows数据结构的大小,并在内存中遍历,不过在计算函数地址时,真正的挑战是要建立一个可靠的名称比较机制来选择正确的DLL和函数,如果PEB解析技术太难实现,我们还会为你介绍一种更简单的方法。

Hash API

metasploit项目中几乎所有的shellcode都使用一个名为Hash API的汇编块,它是由Stephen Fewer编写的一段代码,自2009年被公布以来,大多数Windows的Windows shellcode就一直在使用它。这个汇编块使得解析PEB结构更容易,它使用基本的PEB解析逻辑和一些额外的Hash算法来快速找到所需的函数,通过计算ROR13函数和模块名的哈希值,可以让这个汇编块的使用起来更加容易,Hash API的汇编块使用stdcall调用约定时,所需的函数参数需要包含ROR13函数名的哈希值和包含该函数的DLL名,在得到我们所需要的参数和函数哈希值之后,如前面所述,这时就该解析PEB块,并找到模块名称了,然后计算ROR13哈希值并将其保存到堆栈,最后再把这个哈希值移动到DLL的导出地址表并计算每个函数名称的ROR13哈希值,ROR13哈希值等于每个函数名称哈希值和模块名称哈希值的和, 

如果匹配到我们要找的哈希值,意味着想要的函数被找到了,最后Hash API会使用栈上的参数跳转到找到的函数地址执行。虽然我们最后会得到一段非常简洁实用的代码,但由于其广泛的使用,一些杀毒软件已经可以检测到它了,甚至连一些杀毒软件已经把使用ROR13哈希的列入到恶意程序黑名单了,不过配合使用一些编码机制,目前该方法还是能绕过杀毒软件的。

不过我们还是能找到其他方法来找寻找到API函数地址。

编码器和解码器的设计

在开始设计之前,我们要先像大家讲清楚,单独使用这个编码器课不会产生完全不可检测的shellcode,因为在运行shellcode之后,解码器将直接运行并将整个shellcode解码为其原始形式,这当然不能绕过具有动态分析机制的杀毒软件了,

解码器逻辑非常简单,它将使用随机生成的多字节XOR键来解码shellcode,在解码操作之后它将执行它,在将shellcode放置在解码器头部之前,应该使用多字节XOR密钥加密, shellcode和XOR键应放在“<Shellcode>”,“<Key>”标签内,

解码器的逻辑非常简单,就是使用一个随机生成的多字节XOR密钥来解码shellcode,在解码操作完成后运行shellcode,在将shellcode放置在解码器头部之前,应该使用多字节XOR密钥来对shellcode加密,这时shellcode和XOR密钥就分别位于“<Shellcode>”和”<Key>”标签内。

我们在此使用了JMP及CALL的指令来获得shellcode和密钥的地址,然后在shellcode和密钥的每个字节都要执行一次逻辑XOR操作,每次解密密钥运行到末尾时,JMP及CALL指令都会把密钥重置为起始地址,在完成解码操作之后,JMP及CALL指令将调用跳转到shellcode,使用字节更长的XOR密钥来增加shellcode的随机性,但如此反复的操作也增加代码块信息熵的大小。

因此要避免使用太长的解密密钥,经过我们的测试,使用基本的逻辑运算如XOR,NOT,ADD,SUB,ROR,ROL,就能得到几百种编码shellcode的方法,而这些经过编码的shellcode更是能产生无限可能的shellcode运行方式,杀毒软件在解码序列之前检测到shellcode的概率很低,不过自从杀软开发出了启发式引擎以后,就能够检测到shellcode的解密循环过程。目前,安全测试人员在编写shellcode编码器时,还没有能够找到用于绕过用于检测解码循环的静态方法的有效方式。

干扰寄存器的办法

在x86架构中,所有寄存器都有特定的用途,例如ECX代表扩展计数器寄存器,它通常用作循环计数器,当我们在任何编译语言中编写一个基本循环条件时,编译器可能使用ECX寄存器作为循环计数器变量,这样代码块内连续增加的ECX寄存器就很容易触发杀毒软件的启发式引擎检测机制,解决这个问题的方法很简单,不把ECX寄存器用作循环计数器,但是这个办法对于所有的其它类型的代码片段(如函数epilogue/prologue等)也非常有效。由于很多代码识别机制取决于寄存器使用,所以我们可以通过干扰寄存器的办法来降低编写汇编代码时被检测到的风险。

垃圾混淆代码

目前几乎每个杀毒软件都有办法来识别代码块内的解码器,据我们统计,大概有可能有几百种方法。但不管是哪种方法,最终都要生成用于静态地检查的代码块签名,所以为了避免被检测到,我们可以使用解码器代码内部的随机NOP指令来绕过静态签名分析,不过不一定非要使用NOP指令,可以是任何保持原始代码功能的指令,其目的最终是向代码块总添加垃圾指令,以打破代码块内的恶意签名,不过也要注意垃圾指令的大小,因为过多的垃圾混淆代码会让恶意程序的信息熵变大。

NOP指令的代码如下:

从上图可以看出,唯一的改变在于EAX和ECX寄存器,由于在垃圾混淆代码时,负责对shellcode索引计数的寄存器是EAX,并且在每个XOR和MOV指令之间都会插入一些NOP填充,由于我们测试时使用的是 “windows/meterpreter/reverse_tcp ”命令,所以在加密之后shellcode有一个10字节长的随机XOR密钥,这些都会存放在解码器内,使用nasm -f bin Decoder.asm命令将解码器汇编成二进制格式(不要忘了移除shellcode中的换行符,否则nasm不能执行汇编命令)。

下面是杀毒软件对编码前的shellcode扫描的结果,

不出所料,大量的杀软扫描器能检测到编码之前的shellcode。让我们再来看看编码后的扫描结果,

巧妙利用杀软的缓解技术

虽然可以用很多办法实现免杀,但杀软的防范漏洞缓解技术却将免杀的技术推向了一个全新的水平。微软在2009年发布了增强缓解体验工具包(EMET),EME是加固Windows防止恶意程序利用操作系统或软件安全漏洞的最有效方法之一,它有几个保护机制,

动态数据执行保护(DEP)
结构异常处理程序覆盖保护(SEHOP)
NullPage分配
堆喷射保护
导出地址表地址过滤(EAF)
强制ASLR
导出地址表访问过滤(EAF +)
ROP缓解措施
加载库检查
内存保护检查
调用者检查
堆栈翻转(Stack pivot)
Attack Surface Reduction(ASR)

在这些缓解中,EAF,EAF +和调用者检查是我们最关注的三种技术,如前所述,metasploit框架中几乎所有的shellcode都使用Stephen Fewer的Hash API,并且由于Hash API应用了PEB 及EAT解析技术,EMET可以轻松检测和阻止shellcodes的运行 。

绕过EMET

调用者检查可以检测到EMET内发生的Windows API调用,并阻止Win API函数中的RET和 JMP指令,以便阻止所有ROP方式的漏洞利用,在HASH API中,在找到需要的API函数地址后,会使用JMP指令执行函数,但这会触发EMET的调用者检查,所以为了绕过调用者检查,应该避免使用指向Win API函数的JMP和RET指令,我们要用CALL指令替换JMP指令来指向Win API函数,从理论上讲Hash API应该能绕过调用者检查,但是当我们查看EAF及EAF+缓解机制时,Hash API却使用被调用的代码阻止程序访问导出地址表(EAT),并且还会检查堆栈寄存器是否在允许的边界内,或者尝试检测特定代码块的MZ / PE报头和KERNELBASE,这是一种非常有效的用于防止EAT解析技术的方法,但EATEAT不是唯一一个包含函数地址的结构,由于导入地址表(IAT)也保存有应用程序使用的Win API函数地址,如果恶意应用程序获取了所需的函数,则可以收集IAT结构内的函数地址。一个名为Joshua Pitts的网络安全研究员最近开发了一种新的IAT解析方法,即在导入地址表中获取LoadLibraryA和GetProcAddress Windows API函数,在获取这些函数地址后,可以从任何库中提取任何函数。

 

================ End

 

posted @ 2019-04-30 10:54  lsgxeva  阅读(1827)  评论(0编辑  收藏  举报