Windows(x86与x64) Shellcode技术研究

阅读量458124

|评论7

|

发布时间 : 2018-02-06 16:00:00

最近研究Windows x64 Shellcode技术,发现关于Shellcode技术相关文章比较杂而且有的存在错误,所以写下这篇文档。此文档主要是通过阅读吸收参考文献,进行总结。第一次使用markdown,如有格式错误、图片不清勿怪。
Windows下的32位与64位的Shellcode编写基本原理和过程是一致的,只是在寄存器、堆栈平衡与函数参数传递等方面存在不同。一般情况下,Shellcode是由汇编代码编写,并转换成二进制机器码,其内容和长度经常会受到苛刻的限制,所以与普通的汇编程序开发不同。 图1 Shellcode开发流程
如图1所示,Shellcode开发流程分为6个步骤:
(1).定位kernel32.dll 基址
所有Windows程序都会加载ntdll.dll和kernel32.dll,通过kernel32.dll中的LoadLibrary和GetProcAddress加载其他DLL与获取其他函数地址。
(2).定位GetProcAddress函数的地址(不是必需的)
GetProcAddress函数位于kernel32.dll,其功能为获取DLL中的函数地址。
(3).定位LoadLibrary函数的地址
LoadLibrary函数位于kernel32.dll,用于加载DLL。
(4)加载DLL获取基址
通过调用LoadLibrary函数,获取DLL基地址(如user32.dll)。
(5).获取其他函数地址
获取加载的DLL中的函数地址。
(6).函数调用
根据函数地址,通过call指令调用函数。

 

一、 定位Kernel32.dll基址

每个程序都会加载kernel32.dll,所以可以在进程环境信息(PEB)中获取kernel32.dll基址。下面分别介绍Windows x86和x64定位kernel32.dll基址方法。

1.1 Windows x86定位kernel32基址

图1-1 定位kernel32基址的过程
如图1-1所示,x86定位kernel32.dll的过程可以分为:定位TEB与 PEB、定位Ldr、定位LDR_DATA_TABLE_ENTRY与确定kernel32.dll基址等4个步骤,下面一一详述:

(1).定位TEB与PEB

TEB(Thread Environment Block,线程环境块)中保存频繁使用的线程相关的数据。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,存放在从0x7FFDE000开始的线性内存中,图1-2描述的是TEB结构体的详细信息。如图1-3所示,PEB(Process Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。进程环境块的地址在0x7FFDF000处。但是从Windows xp SP2中就引入了TEB与PEB随机化技术,TEB与PEB的基址不再固定。
虽然PEB和TEB的基址具有一定的随机性,可以通过FS寄存器可以获取TEB的基址。很多文档中提到FS中存储TEB基址,实际并不是这样。在FS存储的是TEB在GDT(Global Descriptor Table)中的序号,通过GDT获取TEB的基址。如图1-2所示,PEB结构体在TEB偏移0x30处,即FS:[0x30]。
图1-2 TEB结构体
1-3 PEB结构体
(2).定位Ldr

如图1-3所示,在PEB偏移0x00c处是Ldr,Ldr的类型为PEB_LDR_DATA结构体指针。Ldr的作用是存储进程已加载的模块(Module)信息。Module是指PE格式的可执行映像,包括EXE映像和DLL映像。如图1-4所示,Ldr通过3个队列存储进程加载的Module信息,即InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList。前两个模块队列都是模块加载队列,第三个是模块初始化队列。前两个模块队列的不同之处在于排列的次序,一个是按装入的先后,一个是按装入的位置。
图1-4 PEB_LDR_DATA结构体
(3).定位LDR_DATA_TABLE_ENTRY

每当为本进程装入一个模块时,就要为其分配、创建一个LDR_DATA_TABLE_ENTRY数据结构,并将其挂入InLoadOrderModuleList和InMemoryOrderModuleList,完成对这个模块的动态连接以后,就把它挂入InInitializationOrderModuleList队列,以便依次调用模块的初始化函数。由此可见进程加载的每个模块都会有一个LDR_DATA_TABLE_ENTRY,其作用为存储模块的基本信息。如图1-5所示,DLL基址(DLLBase)在偏移0x18处。
图1-5 LDR_DATA_TABLE_ENTRY结构体
Ldr中的3个字段InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList是这3个队列的起始位置,它们分别指向LDR_DATA_TABLE_ENTRY 结构体上的InLoadOrderModuleLinks、InMemoryOrderModuleLinks、和InInitializationOrderModuleLinks字段。如图1-5所示,在LDR_DATA_TABLE_ENTRY中可以通过相对偏移计算处DLLBase的地址。

(4).确定kernel32.dll基址

由Ldr中的3个List字段可以获得LDR_DATA_TABLE_ENTRY中3个Links的地址,通过相对偏移计算出DLLBase,自此只是计算出进程加载的第一个Module的基址。那么如何获取其他的Module的基址呢?这3个List的字段是List_Entry类型的指针,如图1-6所示,List_Entry中有两个List_Entry类型的指针,由此可见Ldr中的3个List是双向链表,那么如何获取到所有Module的基址就很明确了。
图1-6 LIST_ENTRY结构体
如图1-1所示,Module A的3个Links结构体的Flink指针分别指向Module B 的3个Links字段的地址,同理可以获取所有Module的Links字段的地址,从而计算出Module的基址。
其实通过LDR_DATA_TABLE_ENTRY中的3个Links字段的任一个都可以计算出Module的基址,那么该如何选择呢?下面先分析一段代码:

代码1-1 Windows XP获取kernel32.dll基址
代码1-1是Windows XP下Shellcode最通用的获得kernel32基址代码,但是无法在Windows 7和以上版本正常运行,因此大大降低了其通用性。通过在Windows 7和Windows XP中遍历InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList这3个队列,可以得到如图1-7的结果。Windows7和XP的不同在于:InInitializationOrderModuleList (图中的List3)中的kernel32.dll和kernelbase.dll的次序不同,导致Windows XP的Shellcode无法在Windows7中正常运行。Windows XP中选择InInitializationOrderModuleList队列的原因是kerneldll.32位于第二项,而其他两个队列中都是位于第三项。因此代码1-1只要在第4行之后加一次寻址操作即可在Windows 7中正常运行。如果使用队列nLoadOrderModuleList和InMemoryOrderModuleList就能同时兼容Windows XP和7 。
为什么在之前通用的shellcode的代码中不采用nLoadOrderModuleList和InMemoryOrderModuleList以提高普适性?主要是因为前2个List不适用于Windows 2000。
图1-7 遍历List
如代码1-2所示,综上所述可以得出适用于Windows XP和Windows7的代码汇编代码。

代码1-2 获取kernel32.dll基址
为了提高代码的通用性,可以不依赖于各个Windows各个版本的InLoadOrderModuleList等3个List中的kernel32.dll的位置,可以在遍历List时判断DLL的名称从而确定当前Module是否为kernel32.dll。如代码1-3所示,获取InInitializationOrderModuleList地址后,通过偏移计算出DLL基址,然后在DLL导出表中找到DLL名称字符串地址并与kernel32.dll比较,直至匹配成功。如何寻找DLL导出表以及导出表的结构会在第二章详细介绍。只要Windows的PE文件结构不改变,该代码均可通用。这样虽然提高了通用性,但是Shellcode的长度也增大了。
代码1-3 通过搜索DLL文件名获取kernel32.dll基址

1.2 Windows x64定位kernel32基址

Windows x64获取kernel32基址的基本原理与方法是一致的,存在的区别具体如下:

(1).指向TEB在GDT的序号不再是FS寄存器,改为GS寄存器。

如图1-8所示,GS寄存器指向TEB在GDT中的序号,PEB在TEB偏移0x60处,即GS:[0x60]
图1-8 Windows x64 TEB
(2).TEB、PEB、Ldr与LDR_DATA_TABLE_ENTRY的偏移量有所不同。

如图1-9所示,Ldr在PEB偏移0x18处。如图1-10所示,3个队列指针在Ldr中的偏移量分别为:0x10、0x20与0x30。在LDR_DATA_TABLE_ENTRY中3个队列节点指针偏移量分别为:0x0、0x10与0x20,DLL基址(DllBase)位于偏移0x30处。其中InMemoryOrderModuleLinks与DllBase的相对偏移量为0x20。
图1-9 Windows x64 PEB

1-10 Windows x64 Ldr

1-11 Windows x64 LDR_DATA_TABLE_ENTRY
通过上述分析,可以得出在Windows x64下通用的定位kernel32基址的代码如代码1-4所示。
代码1-4 Windows x64获取kernel32.dll基址

 

二、定位函数

2.1 Windows x86定位函数

Shellcode一般情况下是运行在其他进程中,无法确定所需函数的DLL是否已经加载到内存。受ASLR(地址空间布局随机化)机制的影响,系统不会每次都把DLL文件加载到相同地址上。而且DLL文件可能随着Windows每次新发布的更新而发生变化,所以不能依赖DLL文件中某个特定的偏移。开发Shellcode需要把DLL加载到内存,然后根据DLL基址获取函数地址。
加载DLL文件需要kernel32.dll中的LoadLibrary函数;获取目标函数地址可以通过函数名匹配与kernel32.dll中的GetProcAddress函数两种方法。由此可知:一是kernel32.dll基址是定位函数的基础;二是由kernel32.dll基址获取LoadLibrary与GetProcAddress函数地址和由加载的DLL基址获取其他函数的原理是一致的,总结为由DLL基址定位函数地址。所以本节需要解决的核心问题是:如何由DLL的基址获取函数地址。
图2-1PE文件DOS头
如图2-1所示,在DLL基址处是DOS头,在偏移0x03c处是e_lfanew字段,该字段存储的是NT头的偏移。如图2-2所示,在NT头偏移0x018处存储的是NT可选头结构体(OptionalHeader)。
图2-2 PE文件的NT头
如图2-3所示,在NT可选头的偏移0x060处存储的是DataDirectory数组。该数组的第一个成员DataDirectory[0]位PE文件的导出表信息(EXPORT Directory),包括导出表的地址和大小(如图2-4)。
图2-3 PE文件NT可选头

图2-4 导出表信息结构体DATA_DIRECTORY

图2-5 PE导出表
如图2-5所示,PE导出表中包含多个导出函数列表,NumberOfFunctions为DLL中导出函数的总数,NumberOfNames为具有函数名称的导出函数的总数,函数既可以用函数名方式导出,也可以用序号方式导出。下面详细介绍AddressOfFunctions、AddressOfNames与AddressOfNameOrdinals 3个列表。

(1).AddressOfFunctions

函数地址列表RVA(相对虚拟地址),位于导出表偏移0x01c处。该列表包含全部导出函数入口的RVA。

(2).AddressOfNames

函数名称列表RVA,位于导出表偏移0x020处。该列表包含函数名称字符串的RVA。有名称的导出函数的名称字符串RVA都定义在这个表中,但是不是所有的导出函数都有名称,如图2-6所示,在urlmon.dll的导出表中,前4个函数均无名称。
图2-6 urlmon.dll 导出表
(3)AddressOfNameOrdinals

函数索引列表的RVA,位于导出表偏移0x024处,该列表与函数名称列表中的项目一一对应,该列表的值代表函数名称列表中的函数在函数地址列表中的索引,这样就可以通过函数名称获取到函数的RVA。例如,函数名称列表的第n 项指向FunctionA,那么可以去查找函数索引列表中的第n 项,假如第n 项中存放的值是x,则表示函数地址表中的第x 项函数地址对应的就是FunctionA的地址。
图2-6 定位函数过程
至此可以得出由DLL的基址获取函数地址的方法。如图2-6所示,在DLL基址处存储的是PE文件的DOS头;在DOS头偏移0x03c处存储NT头的偏移,在NT头偏移0x18处存储的是NT可选头的地址;NT可选头的偏移0x60处存储的是DataDirectory数组,该数组的第一项是导出表信息结构体,包含导出表地址与大小;获取导出表的地址后,在偏移0x20处获取函数名列表的RVA,遍历函数名列表,与目标函数名FunctionA相比较,确定FunctionA在函数名列表中的索引NO.A,然后在函数索引列表中NO.A索引处获取目标函数在函数地址列表中的索引NO.A’,在函数地址列表的NO.A’索引处获取到的RVA就是FunctionA的RVA,将RVA与DLL基址相加即获得FunctionA的基址。
综上所述,kernel32.dll可以通过上述方法获取LoadLibrary与GetProcAddress函数地址。然后由LoadLibrary加载其他DLL并获取DLL在内存中的基址,同理可以获取其他函数地址。
不难发现获取函数地址有两种方法:一是直接根据DLL基址,通过上述在导出表中查找函数名的方法获取函数地址;二是通过上述方法获取kernel32.dll中的GetProcAddress方法,然后通过GetProcAddress获取函数地址。这两种方法各有优缺点:

(1)方法一:由于直接对PE文件进行操作,不需要Windows API,通用性较好,但是生成的Shellcode较长。

(2)方法二:通过GetProcAddress函数获取函数地址,依赖于Windows API 通用性较低,但是生成的Shellcode较短。

由于Shellcode最终是要放进缓冲区的,所以Shellcode的大小会影响其通用性。当Shellcode中涉及到的函数较多,一般在遍历导出表中的函数名列表时不会直接用函数名(如MessageBoxA),首先会对函数名进行hash运算,在搜索时也需要对函数名列表中的函数名进行hash运算,然后比较hash所得的摘要。

代码2-1 获取导出表地址
如代码2-1所示,根据上述可以得出由DLL基址获取导出表基址的汇编代码。如代码2-2所示,由DLL导出表的基址获取具体函数的地址的汇编代码相对复杂,其原理与上述分析一致。

代码2-2 获取函数地址

2.2 Windows x64 定位函数

Windows x64与x86定位函数的基本原理与方法是一致的。如图2-8所示,存在的主要区别是导出表在NT可选头中的偏移不同。导出表中的各个字段的偏移都是一致的。由于在PE文件中的偏移地址是32位的,所以32位的获取函数地址的汇编代码2-2试用于Windows x64。
图2-7 Windows x64 NT可选头
代码2-3 Windows x64 NT获取导出表地址

 

三、函数调用

通过第一章与第二章的分析,在Windows x86和x64中定位函数地址后,通过指令call 即可调用函数。在Shellcode中还需要解决两个问题:参数传递与堆栈平衡。在Windows x86中,函数调用约定采用stdcall方式,该方式的特点是:所有参数入栈,通过椎栈传递;二是被调用的函数负责栈指针esp的恢复。Windows x86的函数调用本文不再详述,下面详细分析Windows x64函数调用的参数传递和堆栈平衡两个问题。
如图3-1所示,Windows x64比x86增加了8个64位通用寄存器:R8、R9、R10、R11、R12、R13、R14、R15,另外还增加了8个128位XMM寄存器。x86中原有的寄存器在x64中均为扩展为64位,且名称的第一个字母从E改为R。仍可以在64位程序中调用32位的寄存器,如RAX(64位)、EAX(低32)、AX(低16位)、AL(低8位),相应的有R8、R8D、R8W和R8B。

图3-1 Windows x64寄存器
Windows x64下函数调用约定与x86的fastcall相似,使用寄存器传参递前四个参数,分别使用RCX、RDX、R8与R9传递,其他多余的参数使用堆栈传递。虽然前4个参数通过寄存器传递,仍然需要在堆栈中为其预留空间。
堆栈空间由函数调用者管理,程序不再是每经过一个call,堆栈就变化一次。而是在每一个call开头,先计算出所需要的堆栈总空间,然后一次分配全部的空间,到函数结束才全部释放。在调用其他函数时不会再次新建堆栈空间来存放参数,而是直接利用前面新建的空间来进行,入栈操作也从 push指令改为mov指令。所有的栈操作都通过rsp指针来完成,所以rsp在一个函数空间中的值不会变化。栈基指针寄存器ebp在x64中被弃用,只是作为一个普通寄存器使用,因此push 与 pop 这类会改变rsp值的指令是不能随便使用。

3-2 example:Windows x64函数调用       下面通过一个例子说明x64的参数传递与堆栈平衡这个问题。如图3-2所示,例子中定义一个函数add,该函数的功能是求和,共有6个参数。使用VS2008的x64平台编译生成可执行程序x64Test.exe。通过WinDbg x64将x64Test.exe反编译,结果如图3-3和3-4所示。

图3-3 main函数反编译

图3-4 add函数反编译
如图3-2所示,在main函数中首先通过指令[sub rsp,48h]为main函数分配0x48个字节的堆栈空间,在mian函数结束时通过[add rsp,48h]回收这48h的堆栈空间。分别通过rcx、rdx、r8与r9分别传递第1、2、3与4参数。如图3-5所示,堆栈前0x20 (32)个字节为前4个参数预留,接着就是第5和6个参数。在参数传递时还是按照从右至左的顺序。
如图3-4所示,在add函数开始时并没有重新分配堆栈空间,只是在栈顶处抬高了8个字节用于存储函数的返回地址(RIP)。如图3-5所示,add函数首先会把前4个参数入栈,由于栈顶指针已经被抬高8个字节,所以第一个参数rcx从[rsp + 8]处入栈,入栈顺序也是从右至左。在函数返回时不再需要通过[ret n]平衡堆栈了,因为堆栈没有变化。
值得一提的是栈需要16字节对齐,由于RCX、RDX、R8、R9四个寄存器预留是0x20个字节,但是call指令会入栈一个8字节的返回值(RIP),所以函数开始时分配堆栈空间时需分配16n + 8个字节来平衡堆栈。

图3-5 x64函数参数传递与堆栈管理
综上所述x64函数调用以下特点:

(1)的前4个参数通过寄存器传递,其他通过堆栈传递

(2) 函数调用者负责管理堆栈。

(3) 堆栈空间一次性分配与回收。

(4) 堆栈指针rsp不再变化,ebp被弃用,push与pop需慎用。不需要[ret n]平衡堆栈。

(5) 函数调用者需为寄存器参数预留堆栈空间。

所以,在开发x64的shellcode时,不需要过多的关注堆栈的变化,只需在开始时分配足够的堆栈空间即可。

 

四、Shellcode编码

Shellcode编码是Shellcode开发的过程中不可缺少的部分,关系到Shellcode能否正确部署与运行。因为在很多漏洞利用场景中Shellcode的内容会受到一些限制。

(1) 特殊字符
有的漏洞场景要求在Shellcode中只能是可见字符的ASCII或Unicode;有的漏洞场景不能出现某些字符如NULL(0x00),如在没有检查缓冲区大小时使用strcpy导致缓冲区溢出时,就不能将带有NULL的Shellcode拷贝进缓冲区。

(2) Shellcode特征

通过前三章的分析可知,Shellcode在定位kernel32.dll、定位函数等方面的代码相对固定,特征较明显,易被拦截。
可以通过编码的方法使Shellcode达到要求,但是需求将解码Shellcode的指令加到Shellcode的前面。在执行时,先执行解码指令,将Shellcode释放到指定内存位置,然后执行Shellcode指令。Shellcode编码技术在x86中已经成熟,而且x64与x86差别较小,本章不再详述。

 

五、总结

Windows x64与x86 Shellcode的原理与开发流程基本一致,但是在寄存器、地址偏移与函数调用等很多细节上还是存在较大差异。如果不能掌握这些细节,在开发与移植x64 Shellcode时会遇到很多问题。Shellcode的通用性和长度通常不能兼得,所以不要过度的追求其通用性。另外还有很多关于Shellcode的问题本文没有提到,如指令复用、Shellcode瘦身等。

 

参考

[1]. Windows下shellcode编写入门:http://blog.csdn.net/x_nirvana/article/details/68921334
[2]. Windows X64汇编入门:http://bbs.pediy.com/thread-43967.htm
[3]. Windows86与x64 Shellcode框架:http://www.freebuf.com/articles/system/58920.html
[4]. Windows x64 shellcode编写指南:http://bobao.360.cn/learning/detail/3911.html
[5]. DLL导出表结构:http://blog.csdn.net/evi10r/article/details/7216467
[6]. Windows x64汇编参数传递:http://www.chinapyg.com/thread-75685-1-1.html
[7]. Windows x86的LDR链:http://blog.csdn.net/osreact/article/details/7738593
[8]. PEB与TEB详解:http://blog.csdn.net/osreact/article/details/7738593
[9]. FS寄存器解析:http://bbs.pediy.com/thread-192523.htm
[10]. PEB数据解析:http://www.longene.org/techdoc/0843755001224576754.html
[11]. 0day安全软件漏洞分析技术.王清
[12]. 逆向工程核心原理.李承远

本文由TMXK22原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/97601

安全客 - 有思想的安全新媒体

分享到:微信
+16赞
收藏
TMXK22
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66