基础知识
漏洞概述
bug与漏洞
bug:影响软件的正常功能,例如,执行结果错误,图标显示错误
。
漏洞:通常情况下不影响软件的正常功能,但被攻击者利用后,有可能引起软件去执行额外的恶意代码。
漏洞挖掘 漏洞分析 漏洞利用
漏洞挖掘
安全性的漏洞往往很难被QA工程师的功能性测试发现。从技术的角度讲,漏洞挖掘实际上是一种高级的测试。学术界一直热衷于静态分析的方法寻找源代码中的漏洞;而在工程界,普遍采用的漏洞挖掘方法是Fuzz。
漏洞分析
当fuzz捕捉到软件中一个严重的异常的时候,当你想要透过厂商公布的简单描述了解漏洞细节的时候,就需要一定的漏洞分析能力。
当分析漏洞时,如果搜到POC(proof of concept)代码,就能重现漏洞。无法获取POC时,一个比较通用的方法是使用补丁比较器,比较patch前后可执行文件有哪些地方被修改,之后可以利用反汇编工具(如IDA PRO)重点分析这些地方。
漏洞利用
exploit,通过漏洞利用执行payload以达成攻击的目的。
漏洞的公布与0day
0day是未被公布,修复的漏洞。
公布漏洞的权威机构:
1.CVE
2.CERT
此外微软的安全中心所公布的漏洞也是所有安全工作者和黑客们感兴趣的地方,微软每个月第二周的星期二发布补丁,由于并非所有的用户都会打补丁,所以可以对这些漏洞进行利用。
二进制文件格式概述
PE文件格式
PE(portable executable)是win32平台下可执行文件遵守的数据格式。
PE文件将可执行分成若干个section,这些section是相同属性数据的集合。一个典型的PE文件包含以下seciton:
- .text 编译器产生,存放着二进制的机器代码
- .data 初始化的数据块,如宏定义,全局变量,静态变量等
- .idata 可执行文件使用的动态链接库等外来函数与文件的信息
- .rsrc 存放程序的资源,图标,菜单等。
section名是可以随意更改的,通过编译指示符号#pragma data_seg()可以把代码中的任意部分编译到PE的任意section
虚拟内存
windows内存分为两个层面,物理内存和虚拟内存,物理内存需要进入windows内核级别ring0才嫩南瓜看到。通常,在用户模式下,我们调试器看到的内存地址都是虚拟内存(如ollydbg里面的内存,寄存器存储的也是虚拟内存的值)
windows让所有的进程都“相信”自己拥有4GB的独立的内存空间,最后虚拟内存是通过虚拟内存管理器映射到真正的物理内存中。
PE文件与虚拟内存之间的映射
在调试漏洞时,可能经常需要做这样两种操作。
- 静态反汇编工具看到的PE文件中某条指令的位置时相对于磁盘文件而言的,即文件偏移。
- 在调试的时看到某条指令的地址时虚拟内存地址,可能在需要在文件中找到对应机器码。
文件偏移地址:数据在PE文件中的地址,这个地址时相对于文件头而言的
装载基址:PE装入内存的基地址,exe文件在内存中的基地址是0x00400000,dll是0x10000000,当然都是可以修改的。
虚拟内存地址:windows装载器装在PE文件后,其指令和数据的地址
相对虚拟地址:内存地址相对于映射基址的偏移量。
节偏移=相对虚拟地址-文件偏移
文件偏移地址=虚拟地址-装载基址-节偏移
=RVA-节偏移
有一些PE工具帮忙分析这些数据。如PE Editor
必备工具
ollydbg
一个集成了反汇编分析,16进制编辑,动态调试等功能的调试器。无法调试内核。
SoftICE
“ICE”是“In Circuit Emulator”的缩写,是用于截获cpu所有动作的一种设备。softICE以软件的方式监视CPU所有动作,可以调试和修改很底层的东西。
IDA Pro
静态反汇编很好用的软件,IDA会自动标识一些常用的函数。
栈溢出原理和实践
系统栈的工作原理
内存的不同用途
根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么操作系统,计算机架构,进程使用的内存都能大致分为以下部分:
- 代码区:用于存储二进制机器代码,处理会到这个区域取指令
- 数据区:用于存储全局变量
- 堆区:进程可以在堆区动态的请求一定大小的内存,并在用完后还给堆区
- 栈区:用于动态的存储函数之间的调用关系,以保证函数返回后正确的执行。
windows平台中,源代码被编译链接后就变成了文件,PE文件被装载后,就成为了所谓的进程。
栈与系统栈
当用高级语言的时候,系统栈是由系统自动维护的,对于程序而言是透明的。只有当使用汇编语言开发程序的时候,才需要和它直接打交道。
函数调用时发生了什么
寄存器与函数栈帧
每一个函数独占自己的函数空间。正在运行的函数总是在栈顶。win32系统提供两个特殊的寄存器用于标识位于系统站顶端的栈帧。
- ESP:栈指针寄存器,其内 存放着一个指针,指向系统栈最上面的一个栈帧的栈顶
- EBP:基址指针寄存器,其内 存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈底
函数的栈帧中一般包含以下几类信息
- 局部变量
- 栈帧状态值,保存前栈帧的栈顶和栈底,一般只保存栈底,栈顶可以通过堆栈平衡的计算得到
- 函数的返回地址
一般栈的空间是系统自动分配的
函数的调用约定与相关指令
不同的语言,参数入栈顺序不同,恢复栈平衡操作的位置也不同。
函数调用大致包含以下操作:
- 参数入栈
- 返回地址入栈
- 代码区跳转
- 栈帧调整
所以代码区跳转后,前一个栈帧的EBP会保存在该站在EBP所指向的内存空间中。
修改邻接变量
修改邻接变量的原理
函数的局部变量一般是挨着的(大多数情况),如果存在数组之类的缓冲区,且程序存在数组越界的错误,就会破坏栈中的相邻变量的值。
突破密码验证程序实验
参考网站内文章
开发shellcode的艺术
shellcode概述
exploit是漏洞利用的过程,shellcode是逆向执行的代码。
shellcode需要解决的问题
shellcode的地址可能在变化,所以我们需要用跳转指令定位shellcode。
定位shellcode
栈帧移位和jmp esp
函数返回时,ESP所指的地址恰好是我们淹没返回地址的下一个位置(ESP所指的位置与函数调用约定,返回指令等有关,retn 3和retn 4在返回后,ESP所指的位置有所差异)
如果用jmp esp指令的地址淹没返回地址,CS:IP指向jmp esp并执行后,就会跳到esp所指的位置,如果我们用shellcode从esp位置开始覆盖,那么就会执行我们所写的shellcode。
获取跳板的地址
程序会加载dll文件,dll文件中的jmp esp可以利用
缓冲区的组织
缓冲区的组成
如果选用jmp esp定位shellcode那么函数返回后要根据缓冲区的大小,shellcode的长度灵活的布置缓冲区,送入缓冲区的数据可以分为以下几种。
- 填充物,可以是任何值,但是一般用nop指令对应的0x90来填充缓冲区,并把shellcode放在其后,这样即使没有精准跳跃到shellcode,也没什么问题。
- 淹没返回地址的数据,可以是跳转指令的地址,shellcode的起始地址,或者是一个shellcode相近的地址
- shellcode
当缓冲区较大的时候可以像图1这样将shellcode布置在缓冲区内,这样的好处由:
- 合理利用缓冲区,使攻击段总长度减小,对于远程攻击,往往数据要在一个数据包内
- 对于程序破坏小,只发生在当前栈帧内
抬高栈顶保护shellcode
当缓冲区较小的时候,shellcode离栈顶较近,push操作可能破坏shellcode。所以可以在shellcode开始就抬高栈顶让esp在shellcode之上,这样pushpop操作就不会影响到shellcode了
使用其他跳转指令
除了esp之外,eax,ebx,esi等寄存器也可能指向栈顶附近,mov eax,esp和jmp eax等序列也可完成进入栈区的功能。
开发通用的shellcode
定位api原理
在使用Dependency Walker计算API入口地址的时候,会发现不同的电脑有差异。原因有以下几点
- 不同操作系统的版本
- 不同的补丁版本
由于这些因素,手工查出的api地址很可能在其他计算机上失效,如果使用静态函数地址调用api会使得exploit的通用性受到很大的显示,所以,实际使用的shellcode必须要能动态的获取自身需要的api函数地址。
windows的api是通过dll中的导出函数来实现的。dll会由一个导出表,和exe的导入表计算后,IAT会指向所调用函数的入口地址。
所有win32成需都会加载ntdll.dll和kernel32.dll这两个最基础的动态链接库。如果想要在win_32平台下定位kernel32.dll的api地址,可以采用如下方法。
- 首先通过段选择字FS在内存中找到当前的线程环境快TEB
- 线程环境快偏移0x30的地方存放着指向进程环境块PEB的指针
- 进程环境快中偏移地址为0x0c的地方存放着指向PEB_LDR_DATA结构体的指针,存放着已经被进程载入的动态链接库的信息。
- PEB_LDR_DATA结构体偏移位置为0x1c的地方存放着指向模块初始化链表的头指针InitializationOraderModuleList
- 模块初始化链表InitializationOraderModuleList中按顺序存放着PE装入运行时初始化的模块的信息,第一个时ntdll.dll,第二个时kernel32.dll
- 找到kernel32的结点后,偏移0x08的部分就是kernel32.dll在内存中的加载基地址
- 从kernel32.dll的加载基址算起,0x3c的部分时pe头
- PE头偏移0x78的地方存放着指向函数导出表的指针
- 至此就可以计算函数的入口地址了
- 导出表偏移0x1c处的指针指向了存储导出函数的函数偏移地址(RVA)的列表
- 导出表偏移0x20处的指针指向存储导出函数函数名的列表
- 函数的RVA地址和名字按照顺序存放在上述的列表中,可以在名称列表中找到所需函数是第几个,然后从地址列表中得到RVA
- 获得RVA后,加上dll的加载基址就可以得到函数的虚拟地址。
shellcode 的加载和调试
char shellcode[] = "\x90\x90...." // shellcode 的机器码
void main()
{
_asm
{
lea eax,shellcode // 计算shellcode的偏移
push eax
ret // 将eax弹给eip
}
}
shellcode 的编码技术
为什么要对shellcode编码
- 字符串函数会对null字节进行限制
- 有些函数要求shellcode必须是可见字符的ascii或者unicode
- 基于特征的IDS系统会对常见的shellcode进行拦截
所以可以先完成shellcode的逻辑随后使用编码技术对shellcode进行便阿玛,最后再构造十几个字节的解码程序,放在shellcode开始执行的地方。
当exploit成功的时候,先执行shellcode顶端的解码程序,最后指示原始的shellcode。
会“变形”的shellcode
最简单编码莫过于异或运算,对应的解码过程也简单。不过使用的时候需要注意:
- 用于异或的特定数据选取是不应该与shellcode已有字节相同,否则会产生null字节
- 可以选用多个密钥分别对shellcode不同的区域进行编码,但是会增加解码操作的复杂性
- 可以对shellcode进行很多轮的编码运算
为shellcode“减肥”
shellcode瘦身大法
选择短“指令”
x86指令集中指令所对应的机器码长短是不一样的,有时候功能相似的指令的机器码长度差异会很大。这里给出一些有用的单字节指令
xchg eax,reg 交换eax和其他寄存器中的值
lodsd 把esi指向的一个dword装入eax,并增加esi
lodsb 把esi指向的一个byte装入eax,并增加esi
stosd
stosb
pushad/popad 从栈中存储/恢复所有寄存器的值
这条指令再eax<0x8000000的时候可以用作mov edx,null
"复合"指令功能强
两件事情用一条指令完成,如 xchg,lods,stos
妙用内存
有些api参数中有很多参数是null,通常的做法是多次向栈中压入null,如果我们换一个思路,将栈中的一大片区域全部置为null,再调用api的时候就可以质押如那些非null的参数。
我们惊颤会遇到api中需要一个很大的结构体作为参数的情况,通过实验可以发现大多数情况下,健壮的api都可以允许两个结构体相互重叠,尤其是当一个参数是输入结构体[in],另一个用作接受结构体[out]时,如果让参数指向同一个[in]结构体,函数往往能正确执行。这种情况下往往用一个字节的短指令“push esp”就可以代替一代段out结构体的代码。
代码也可以当数据
很多windows的api都会要求输入参数是一种特定的数据类型。如常见的api参数是一个结构体指针和一个指明结构体的大小的值,往往后者足够大,就可以正确运行。所以当恰好有一个很大的数值,即使是指令,也可以当作数据使用,省下一条参数压栈的指令。
调整栈顶回收数据
如果栈顶之上有需要的数据,可以调高栈顶使用。
巧用寄存器
按默认的函数调用约定,再调用api的时候有些寄存器总是被存放在栈中。许多函数运行的时候往往用不到EBP寄存器,可以直接将数据存储到EBP中,而不是压入栈中。
一些x86寄存器有着自己特殊的用途,有些指令要求只能使用特定的寄存器。如果寄存器中含有调用函数时需要的数值,尽管不是立刻调用这些函数,可能还是要考虑提前把寄存器压入栈内以备后用,避免到时候还得另用指令重新获取。
hash
实用的shellcode通常需要超过200字节或者300字节的机器码,所以对原始的shellcode进行编码或压缩时值得的。在需要的api比较多的时候,可以将api名计算出它的摘要,这样可以减少shellcode的篇幅。
binshell实验
详见站内其他页面
堆溢出利用
堆的工作原理
windows堆的历史
微软操作系统堆管理机制的发展大致可以分为三个截断
- windows 2000 ~ windows xp sp1,堆管理系统只考虑完成分配任务和性能因素,丝毫没有考虑安全因素,可以比较容易的被攻击者利用
- windows xp 2 ~ windows 2003 加入了安全因素,比如修改了块首的格式并加入安全cookie没双向链表结点在删除时会做指针验证等。这些安全防护措施使堆溢出攻击变得非常困难,但利用一些高级的攻击技术在一定情况下还是有可能成功的。
- windwos vista ~ windows 7不论在堆分配效率上还是安全与稳定性上,都是堆管理算法的一个里程碑
堆和栈的区别
栈变量在申请的时候不需要额外的申请操作,系统会根据函数中的变量声明自动在函数栈帧中给其预留空间。栈空间由系统维护。
堆是一种程序运行时动态分配的内存。
堆的数据结构与管理策略
现代操作系统的数据结构一般包括堆块和堆表两类
堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆为单位进行标识,而不是传统的按字节标识。一个堆块包括两部分,块首和块身。块首是一个堆块头部的几个字节用来标识这个堆块自身的信息,例如,本块的大小,空闲还是占用等信息,块身是紧随在块首后面的部分,也是最终分配给用户使用的数据区。
堆表:堆表一般位于堆区的起始位置,用于索引堆区所有堆块的重要信息,包括堆块的位置,大小,空闲还是占用,现代操作系统的堆表往往不止一种数据结构。
在windwos中,占用态的堆块被使用它的程序索引,而堆表值索引所有空闲态的堆块,其中,最重要的堆表有两种:空闲双向链表Freelist,快速单项链表Lookaside
空表
空闲堆块的块首包含一堆重要的指针,这对指针用于将空闲堆块组成双向链表。按照堆块的大小不同,空表总共被分为128条。
堆区一开始的堆表区中有一个128项的指针数组,被称为空表索引,该数组的每一项有两个指针,用于表示一条空表。
空表索引的第二项(free[1])标识了所有大小为8字节的空闲堆块,之后每个索引项指示的空闲堆块递增8字节,例如free[2]标识大小为16字节的空闲堆块。
空表的第一项所标识的空表比较特殊,这条双向链表链入了所有大于等于1024字节的堆块(小于512KB)。这些堆块按照各自的大小在表中升序的排列。
快表
块表是用来加速堆块分配的一种堆表。这里的堆块不会发生合并。
快表总是被初始化为空,而且每条快表只有4个结点,故很快就会被填满。
堆块操作可以分为堆块分配,堆块释放,堆块合并。其中,“分配”和“释放”是在程序提交申请和执行的,堆块合并是操作系统自动完成的。
- 堆块分配:
- 堆块分配分为三类,快表发呢配,普通空表分配,0号空表分配。
- 快表分配堆块比较简单,包括寻找大小匹配的空闲堆块,将其修改为占用态,从堆表中移除,返回一个指向堆块块身的指针。
- 普通空表分配首先寻找最优的空闲块分配,若失败,则寻找次优的空闲块分配,即最小的能满足要求的空闲堆块。
- 零号表在分配时先看最后一个结点能否满足要求,若可以,则从正向搜索最小能满足要求的堆块。
- 当空表无法找到最优堆块的时候,会从一个较大的堆块中分配其请求的内存空间,随后剩下的部分重新标注块首链入空表。
- 堆块释放:
- 释放堆块的操作包括将堆块状态改为空闲,链入相应的堆表,所有的释放块都链入链表的末尾,分配的时候也从堆表末尾拿。
- 堆块合并:
- 当堆管理系统发现两个空闲堆块相邻的时候,将两个块从空表中卸下,合并堆块,调整块首信息,重新链入链表。
在进行堆块分配和释放时,根据内存大小不同分为三类。
小块:<1KB
大块:1KB<=SIZE<512KB
巨块:>=512KB
1.快表中的空闲块设被设置为占用态,不会进行合并
2.快表只有精确匹配才会分配
3.快表时单链表,操作比双链表简单,插入删除少很多指令
4.由于快表操作快,故在分配和释放的时候总是优先使用快表,失败才使用空表
5.由于快表只有4项很快被填满,所以空表也是被频繁使用的
在堆中漫游
堆分配函数之间的调用关系
堆的调试方法
堆分配的算法依赖于操作系统,编译器版本,编译选项,build类型等因素。
调试态堆管理策略和常态堆管理策略有很大差异,集中体现在:
- 调试堆不使用快表,只用空表分配
- 所有堆块都被加上了多余的16字节尾部来防止溢出,包括了八个字节的0xAB和八个字节的0x00
- 块首的标志位不同
DWORD SHOOT
DWORD SHOOT的利用方法
DWROD SHOOT的常用目标(windows xp sp1之前的平台),大概可以概括为以下几类:
- 内存变量:修改能够影响程序执行的重要标志变量,往往能改变程序流程,DWORD SHOOT比栈溢出强大很多,因为溢出时候的数据往往必须是连续的,而DWORD SHOOT可以改变内存中任意地址的数据
- 代码逻辑:修改代码段中重要函数的关键逻辑可以达到一定的攻击效果
- 函数返回地址:这里由于栈帧的移位,DWORDSHOOT此时有局限性
- 攻击异常处理机制:当程序异常的时候,程序会转入异常处理机制。堆溢出很容易引发异常,因此异常处理机制所使用的重要的数据结构往往会成为DWORDSHOOT的目标,这包裹S.E.H(structure exception handler),F.V.E.H(First Vectored Exception Handler),进程环境块P.E.B中的U.E.F(Unhandled Exception Filter),线程环境块中存放第一个S.E.H指针(T.E.H)
- 函数指针:系统有时往往使用一些函数指针,比如调用动态链接库中的函数,C++中的虚函数调用等。改写这些函数指针后,在函数调用发生后往往可以成功的劫持进程。不是每一个漏洞都能使用这项技术,这取决于软件的开发方式。
- P.E.B中线程同步函数的入口地址:每个进程的P.E.B中存放着一堆同步函数指针,指向RtlEnterCriticalSection()和RtlLeaveCriticalSection(),并且在进程退出的时候会被ExitProcess()调用。如果能通过DWROD SHOOT修改这对指针中的其中一个,那么进程退出的时候就会调用我们写的shellcode。由于P.E.B在内存中的偏移不变不变,所以这很好定位。
狙击P.E.B中的RtlEnterCritical
windows为了同步进程下的多个线程,使用了一些同步措施,如锁机制,信号量,临界区等。许多操作都需要用到同步机制。
当进程退出时,ExitProces()需要做很多善后工作,其中必然需要用到临界区函数RtlEnterCriticalSectio()和RtlLeaveCriticalSection()来同步线程防止“脏数据”的产生。
0x7ffdf020存放着RtlEnterCriticalSection()指针,0x7ffdf024存放着RtlLeaveCriticalSection()指针。
详细查看堆溢出实验
形形色色的内存攻击技术
狙击windows异常处理机制
S.E.H概述
操作系统和程序运行的时候,难免遇到错误,为了能继续运行下区,windows会为运行在其中的程序提供一次补救的机会,这就是异常处理机制。
S.E.H即异常处理结构体(Structure Exception Handler),它是windows异常处理基址所采用的重要的数据结构。每个S.E.H包含两个DWROD指针:S.E.H链表指针和异常处理的句柄,共八个字节。
- S.E.H结构体存放在系统站中。
- 当线程初始化时,会自动向栈中安放一个S.E.H,作为线程默认的异常处理
- 如果程序源代码中使用了___try{}__excpet{}或者Assert宏等异常处理机制,编译器最终通过向当前函数栈中安装一个S.E.H来实现异常处理。
- 栈中一般会同时存在多个S.E.H
- 栈中的多个S.E.H通过链表指针在栈内自由栈顶向栈底串成单向链表,位于链表最顶端的S.E.H通过T.E.B0字节偏移处的指针标识。
- 当异常发生时,操作系统会中断程序,并首先从T.E.B的0字节偏移处取出距离栈顶最近的S.E.H,使用异常处理函数句柄所指向的代码来处理异常。
- 当离“事故现场”最近的异常处理函数运行失败时,将顺着S.E.H链表依次尝试其他的异常处理函数。
- 如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通常这个函数会弹出一个对话框,然后终止程序。
后续详见实验
深入挖掘windows异常处理
不同级别的S.E.H
异常处理的最小作用域是线程,每个线程拥有自己的S.E.H链表。县城发生错误时,首先将使用自身的S.E.H进行处理。
一共进程中可能同时存在很多给线程。此外,进程中也有一共能够"纵览全局"的异常处理。当线程自身的S.E.H无法"摆平"错误的时候,进程S.E.H将发挥作用。这种异常处理不仅仅能影响出错的线程,进程下属的所有线程可能都会收到影响。
除了线程异常处理和进程异常处理之外,操作系统还会提供一共默认的异常处理。当所有的异常处理函数都无法处理错误时,这个默认的异常处理函数将被最终调用。其结果一般是显示一个错误对话框(我们经常见到的程序崩溃时的那种对话框)。
现在我们可以将前面所给出的最简单的异常处理流程补充如下:
- 首先执行线程中距离栈顶最近的S.E.H的异常处理函数
- 若失败,则依次尝试S.E.H链表中后续的异常处理函数
- 若S.E.H链中所有的异常处理函数都没能处理异常,则执行进程中的异常处理
- 若仍然失败,系统默认的异常处理被调用,程序崩溃的对话框被弹出
线程的异常处理
线程通过TEB引用S.E.H链表依次尝试处理异常的过程。这里,首先需要补充的时异常处理函数的参数和返回值。
线程中的用于处理异常的回调函数有4个参数
- pExcept:指向一个非常重要的结构体EXCEPTION_RECORD。该结构体包括与异常相关的信息,如异常的类型,异常发生的地址等。
- pFrame:指向栈帧中的S.E.H结构体
- pContext:指向Context。该结构体中包含了所有寄存器的状态
- pDispatch:位置用途
在回调函数执行前,操作系统会将上述异常发生时的断电信息压栈。根据这些对异常的描述,回调函数可以轻松的处理异常。例如,遇到除零异常时,可以把相关寄存器的值修改为0;内存访问错误时,可以重新把寄存器指回有效地址。
这种回调函数返回后,操作系统会根据返回的结果决定下一步应该做什么。异常处理函数可能返回两种结果。
0(ExceptionContinueExcetution):代表异常被成功处理,将返回源程序发生异常的地方,继续执行后续指令。
1(ExceptionContinueSearch):代表异常处理失败,将顺着S.E.H链表搜索其他可用于异常处理的函数并尝试处理。
注意:操作系统是通过传递给回调函数的参数恢复断点信息的,这时的"断点"可能已经被异常处理函数修改过,例如,若干寄存器的值可能被更改以避免除0异常等。
线程的异常处理中还有一个比较神秘的操作叫做unwind操作,这个操作会对我们建立起来的异常处理流程概念做一些修改。
当异常发生时,系统会顺着S.E.H链表搜索能够处理异常的句柄,一旦找到了恰当的句柄,系统会将已经遍历过的S.E.H中的异常处理函数再调用一边,这个流程就是unwind操作,第二轮调用被称为unwind调用。
unwind调用的主要目的是"通知"前边处理异常失败的S.E.H,系统已经准备将它们"遗弃"了,请它们立刻清理现场,释放资源,之后这些S.E.H结构体被将被冲链表中拆除。
unwind操作很好的保证了异常处理机制自身的完整性和正确性。
异常处理函数第一轮调用用来尝试处理异常,而第二轮的unwind调用时,往往是释放资源等操作。
unwind调用实在回调参数中指明的,回调函数的第一个参数pExcept所指向的EXCEPTION_RECORD结构体如下:
typedef struct _EXCEPTION_RECORD{
DWORWD ExceptionCode;
DWORD ExceptionFlags; // 异常标志位
struct _EXCEPTION_RECODE *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameter;
DWORD ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS];
}EXCEPTION_RECORD;
当结构体中的ExceptionCode被设置为0xC000027(STATUS_UNWIND),并且ExceptionFlags被设置为2(EH_UNWINDING)时,对回调函数的调用就属于unwind调用。
unwind操作通过kernel.32中的一个导出函数RtlUnwind实现,实际上这个kernel32.dll会再去调用ntdll.dll中的同名函数。
void RtlUnwind(
PVOID TargetFrame;
PVOID TargetIP;
PEXCEPTION_RECORD ExceptionRecord;
PVOID ReturnValue;
)
在使用回调函数之前,系统会判断是否在调试状态,如果是,将把异常交给调试器处理。
进程的异常处理
所有的线程中发生的异常如果没有被线程的异常处理函数或调试器处理掉,最终将交给进程中的异常处理函数处理。
进程的异常处理回调函数需要通过API函数SetUnhandledExceptionFilter来注册,其原型如下
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter;
)
这个函数是kernel32.dll的导出函数
进程的异常处理函数有三种返回值:
- 1(EXCEPTION_EXEXCUTE_HANDLER):表示错误得到正确的处理,程序将退出
- 0(EXCEPTION_CONTINUE_SEARCH):无法处理错误,将错误转交给系统
- -1(EXCEPTION_CONTINUE_EXEXCUTION):表示错误得到正确的处理,并将继续执行下去。类似线程的异常处理,系统会用回调函数的参数恢复除异常发生时的断电情况,但这是引起异常的寄存器值应该已经得到了修复。
系统默认的异常处理U.E.F
如果进程异常处理失败或者用户根本没有注册进程异常处理,系统默认的异常处理函数UnhandledExceptionFilter()会被调用。看到函数名,顾名思义,这个函数好像一个"筛选器",任何无法处理的异常都会被他捕获。
MSDN中将U.E.F称为"top-level exception handler",即顶层的异常处理
将用户自定义的进程的异常处理SetUnhandledExceptionFilter理解为用户在顶层异常处理之前插入的自定义异常处理
异常处理流程的总结
- cpu执行时发生并捕获异常,内核接过进程的控制权,开始内核态的异常处理
- 内核异常处理结束,将控制权交给ring3
- ring3中第一个处理异常的函数时ntdll.dll中的KiUserExceptionDispatcher()函数
- KiUserExceptionDispatcher()首先会检查程序是否处于调试状态。如果是,交给调试器处理。
- 在非调试状态下,KiUserExceptionDispatcher()调用RtlDispatchException()函数对线程的S.E.H链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的S.E.H句柄,即unwind操作。
- 如果栈中的S.E.H都失败了,且用户使用过SetUnhandledExceptionFilter函数设定了进程异常处理,则这个异常处理将被调用
- 如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,则系统默认的异常处理UnhandledExceptionFilter将被调用。U.E.F会根据注册表决定关闭程序还是弹出错误对话框。
其他异常处理机制的利用思路
V.E.H利用
从windowsXP开始,在仍然全面兼容以前的S.E.H异常处理的基础上,微软增了一种新的异常处理:V.E.H(Vectored Exception Handler,向量化异常处理)
- V.E.H和进程异常处理类似,都是基于进程的,而且需要使用API注册回调函数。相关API如下所示。
- PVOID AddVectoredExceptionHandler(
- ULONG FirstHandler,
- PVECTORED_EXCEPTION_HANDLER VectoredHandler
- )
- V.E.H结构
- struct _VECTORED_EXCEPTION_NODE
- (
- DWORD m_pNextNode;
- DWORD m_pPreviosNode;
- DWORD m_pfnVectoredHandler;
- )
- 可以注册多个V.E.H,多个V.E.H结构体之间串称双向链表,因此比S.E.H多了一个前向指针。
- V.E.H处理优先级优先级次于调试器处理,高于S.E.H:KiUserExceptionDispatcher()先检查是否被调试,然后检查V.E.H链表,最后检查S.E.H链表
- V.E.H保存在堆中
- unwind只对栈帧中的S.E.H链起作用,不会涉及V.E.H这种进程类的异常处理
相关论文 David Litchfiled的"windows heap overflow"
攻击TEB中的S.E.H节点
异常发生时,异常处理机制会遍历S.E.H链表寻找合适的出错函数。线程的S.E.H链通过TEB的第一个DWORD标识(fs:0),这个指针永远指向离栈顶最近的S.E.H。
- 一个进程中同时存在多个线程
- 每个线程都有一个线程环境块TEB
- 第一个TEB开始于地址0x7FFDE000
- 之后新建的TEB将紧随前面的TEB,之间相隔0x1000字节,并向内存低地址方向增长(前面的是高地址,新建立的地址减少0x1000)
- 当线程退出时,相应的TEB也被销毁,腾出的TEB能被新建立的线程使用
当遇到多线程程序时我们很难判断当前的线程时哪一个及对应的TEB在上面位置。因此,攻击TEB中S.E.H头节点的方法一般用于单线程的程序
攻击U.E.F
U.E.F即i系统默认的异常处理函数,是系统处理异常的最后一个环节。如果能够利用堆溢出产生的DWORDSHOOT把这个异常处理的函数调用句柄覆盖为shellcode的入口地址,在制造一个其他异常处理函数都无法解决的异常,这样当使用U.E.F来解决异常的时候,就会执行shellcode。
将kernel32.dll拖进IDA,待自动分析结束,点击"Functions"选项卡,会列出所有函数的函数名,键入SetUnhandlerExceptionHandler()可以定位到这个函数,并显示这个函数的入口地址等信息。
利用跳板技术能够使得成功率更高。
David指出在异常发生时,EDI往往仍然指向堆中离shellcode不远的地方,把U.E.F的句柄覆盖成一条call dword ptr [edi+0x78]往往就能让程序跳转到shellcode处,此外还有:
call dword ptr [esi+0x4c]
call dword ptr [ebp+0x74]
总之,堆溢出的跳板选择依赖于调试时的具体情况,没有定法,有时需要一些灵感。
攻击PEB中的函数指针
ExitProcess()函数在清理现场的时候需要进入临界区同步线程,因此会调用RtlEnterCriticalSection()和RtlLeaveCriticalSection()
将这对指针修改为shellcode,在程序退出的时候就会执行shellcode
"off by one"的利用
Halvar Flake 在"Third Generation Exploitation"中,按照攻击的难度把漏洞利用分为三个层次:
- 第一类是基础的栈溢出利用。攻击者可以利用返回地址等轻松的劫持进程,植入shellcode。如strcpy,strcat等函数的攻击
- 第二类是高级的栈溢出利用。这是,栈中有诸多的限制因素,溢出的部分往往只能淹没部分的EBP,而无法抵达返回地址的位置。这种漏洞利用的典型的例子就是堆strncpy函数误用时产生的"off by one"利用。
- 第三类攻击则是堆溢出利用及格式化串漏洞的利用。
代码
void off_by_one(char* input)
{
char buf[200];
int i=0,len=0;
len=sizeof(buff);
for(i=0,input[i]&&(i<=len);i++)
{
buf[i]=input[i]
}
....
}
显然应该时i<len但是这里是i<=len多了一个字节,这多余的一个字节最终为EBP的最低位字节。故而我们能在255个字节的范围内移动EBP
当能够让EBP恰好植入可控制的缓冲区时,时有可能做到劫持进程的。此外,off by one问题有可能破坏重要的邻接变量,从而导致程序流程或者整数溢出等更深层次的问题。
后续见实验形形色色的内存攻击技术中的攻击c++虚函数及Heap Spray
其他类型的软件漏洞
格式化串漏洞
printf中的缺陷
#include<stdio.h>
main()
{
int a=44,b=77;
printf("a=%d,b=%d",a,b);
printf("a=%d,b=%d");
}
文章评论