系统内核漏洞利用迁移技术

更新时间:2024-04-24 11:36:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

系统内核漏洞利用迁移技术

自黑客的攻击方向开始瞄向操作系统开始,内核安全性问题日益凸显出它的重要性。攻击兴趣的主要助推器还是因为现代操作系统内核的特性所决定。系统内核包含许多子系统模块的实现代码,子系统之间通过复杂的接口交互;另外,系统内核还包含大量的用户数据接入点,如系统调用、IOCTLs、文件系统及网络连接等,允许可控的用户数据成功 访问到内核中的重要代码区,这些都是可能被黑客利用的漏洞,从而形成各种系统BUG。本文是在参考Patroklos在bh-eu-2011上发表的文章“Protecting the Core-Kernel Exploitation Mitigation”、 Adam 在Phrack-67上发表的文章“Scraps of notes on remote stack overflow exploitation”以及Tarjei Mandt在bh-dc-2011上发表的文章“Kernel Pool Exploitation on Windows 7”的基础上并结合自己的理解分析完成的,主要分析Linux与Windows系统上可能存在的各种漏洞利用及其迁移保护技术。 1. 内核内存污染漏洞

NULL指针解引用是最常见的漏洞之一。在内核代码中NULL值常在变量初始化、设置为缺省值或者作为一个错误返回值时使用。在系统中,虚拟地址空间分为两个部分,分别称为内核空间和用户进程空间,当内核尝试解引用一个NULL指针时,若用户又允许映射NULL地址(如首个页面包含0地址),则可直接或间接控制内核代码路径。(注:NULL指针引用BUG主要是因进程在执行指针解引用时,没有检查其合法性造成的,若待解引用的地址为NULL,则在内核态访问NULL指针时会引发Oops,此时若黑客在用户态将NULL地址设置为可执行并注入恶意代码,则内核代码将会执行NULL地址的恶意指令。

系统内核中另一个利用最为频繁的漏洞即缓冲区溢出。内核通常使用两种类型的栈结构:内核态堆栈和用户态堆栈。在内核内存分配中,通过邻居堆对象污染或者破坏内存分配中的元数据实现内存溢出。内存污染漏洞的bugs实现技术已经在很多公开文献中出现过,这里我们只做简要分析,如用户输入验证不足依旧是内存污染的主要漏洞之一,可采用传统的边界检查或用户数组索引(边界)控制等防止漏洞产生;有符号Bugs也可产生内核漏洞,如:

int func ( size_t user_size){

int size = user_size;

i f (size < MAX_SIZE) {

/* do some operation with size considered safe */

} }

若无符号变量user_size值大于231 – 1,则有符号变量size的值将变为负数,此时if语句恒为true. 尽管目前这种有符号bugs并不常见,但在特定的系统内核中还是可以找到。数字运算时可能产生整数溢出,如乘法运算可能导致内核分配比实际所需内存小的内存块。当前大多数处理器都是SMP结构,故内核中共享的资源需要慎重使用,避免资源在使用有效期内发送状态的改变。 2. 用户级内存污染迁移

大量传统的用户级漏洞利用方法已经被系统中的各种防范措施堵死。栈canaries可能是最著名的漏洞迁移技术,用来保护栈内的元数据信息,这些栈元素(如保存的返回地址、栈桢指针)因程序bugs可能被污染,如栈缓冲区溢出可能导致安全漏洞;一个类似的保护技术是堆canaries的使用,有效探测堆内元数据是否被覆写;另一个堆迁移技术是目的地址验证,即验证链表中待卸载元素从链表中拆除之前节点指针值,如:

BOOLEAN RemoveEntryList (IN PLIST_ENTRY Entry) { PLIST_ENTRY Blink,Flink; Flink = Entry->Flink; Blink = Entry->Blink;

if (Flink->Blink != Entry) KeBugCheckEx(...); if (Blink->Flink != Entry) KeBugCheckEx(...);

return (BOOLEAN) (Blink->Flink == Flink->Blink);

//即验证双端队列中的恒等式:Entry->Flink->Blink == Entry->Blink->Flink == Entry; } 地址空间随机化(ASLR)是针对缓冲区溢出的漏洞利用迁移技术,通过对栈、共享库映射等线性区布局的随机化,防止攻击者定位攻击代码位置,达到阻止溢出攻击的目的。不同的系统会在不同区域使用ASLR,如用户级栈地址通常在应用程序实例化时随机,同时用户堆地址也是随机化的,即在可执行文件与堆之间会添加一个随机偏移量,同样动态库与可执行文件的基地址在ASLR实现中也是随机化的,使得传统的返回地址覆盖法很难实现。 一些CPUs提供了一种禁用某些内存区域的执行权限,标记为non-executable.这种技术在不同的平台上被惯用不同的名字如NX-Non-eXecute, XD-eXecute Disable,XN-eXecute Never等。操作系统使用此特性在用户级栈和堆上标记一些不可执行区域。强制访问控制是许多系统中使用的另一种安全防御技术,对操作系统的各种客体(如文件、socket、系统FIFO、SCD、IPC等)进行细粒度的访问控制,以降低系统整体风险。

3. Linux内核漏洞利用迁移技术

本文探讨的linux内核迁移涉及的内核版本为2.6.37. Linux内核支持由GCC的”-fstack-protector” 选项提供Stack-smashing Protection(SSP)栈保护机制,GCC的SSP机制实现了两种类型的栈保护,其一:变量在栈中的顺序发生了改变;其二:启用了canary值检测栈EIP是否被修改。栈中局部变量的组织方式重排列即编译器将所有的局部数组变量放置在栈的高地址位,尽可能地将所有buffers放置在接近canary的位置上,且尽可能地远离各个变量指针,此时利用数组溢出方式无法覆盖其它关键变量如局部函数指针等,实例分析如下: int func(char *arg1, char *arg2) { int a;

char b[10]; char c[2];

strcpy(b, arg1); strcpy (c, arg2); a = 1; return 0;}

启用SSP前后,上述代码的栈结构布局分别如图1, 2 所示。 不启用SSP保护机制,编译上述程序gcc –E exp.c –o exp.i,gcc –S exp.i,生成的汇编编码: func: pushl ?p movl %esp, ?p subl $24, %esp movl 8(?p), êx ;将参数argv1拷贝到êx处 movl êx, 4(%esp) ;再将êx存放的argv1拷贝到%esp+4 leal -14(?p), êx ;将buff b[]地址拷贝到êx movl êx, (%esp) ;将êx存放的b[]拷贝到%esp

call strcpy ;调用strcpy函数拷贝(%esp,%esp+4),即(b[],argv1) movl 12(?p), êx ;将参数argv2拷贝到êx处 movl êx, 4(%esp) ;再将êx存放的argv2拷贝到%esp+4 leal -16(?p), êx ;将buff c[]地址拷贝到êx movl êx, (%esp) ;将êx存放的c[]拷贝到%esp

call strcpy ;调用strcpy函数拷贝(%esp,%esp+4),即(c[],argv2) movl $1, -4(?p) ;将值1拷贝到?p-4处,注意:?p-4存放的地址即&a; movl $0, êx leave ret

函数参数Arg2函数参数Arg1返回地址Ret栈指针EBP栈指针EBP局部变量 a CanaryBuffer b[10]Buffer c[2]strcpy(argv1或argv2)strcpy(b[10]或c[2])24字节低地址高地址函数参数Arg2函数参数Arg1返回地址RetBuffer b[10]20字节Buffer c[2]局部变量 a

图1. SSP启用之前栈结构布局 图2. SSP启用之后栈结构布局

启用SSP保护机制,编译上述程序“gcc –E exp.c –o exp.i –fstack-protector”,“gcc –S exp.i –fstack-protector”,生成的汇编编码: func: pushl ?p movl %esp, ?p subl $56, %esp movl 8(?p), êx movl êx, -36(?p) movl 12(?p), êx movl êx, -40(?p) movl %gs:20, êx ;取出canary movl êx, -4(?p) ;将canary放置在ebp-4处 xorl êx, êx movl -36(?p), êx ;将参数argv1拷贝到êx处 movl êx, 4(%esp) ;再将êx存放的argv1拷贝到%esp+4 leal -14(?p), êx ;将buff b[]地址拷贝到êx movl êx, (%esp) ;将êx存放的b[]拷贝到%esp call strcpy ;调用strcpy函数拷贝(%esp,%esp+4),即(b[],argv1) movl -40(?p), êx ;将参数argv2拷贝到êx处

movl êx, 4(%esp) ;再将êx存放的argv2拷贝到%esp+4 leal -16(?p), êx ;将buff c[ ]地址拷贝到êx movl êx, (%esp) ;将êx存放的c[ ]拷贝到%esp

call strcpy ;调用strcpy函数拷贝(%esp,%esp+4),即(c[],argv2) movl $1, -20(?p) ;将值1拷贝到?p-20处,注意:?p-20存放的地址即&a; movl $0, êx movl -4(?p), íx ;取?p-4位置处地址,将其拷贝给íx,即canary值 xorl %gs:20, íx ;比较%gs:20与栈内保存的canary是否相等; je .L3

call __stack_chk_fail

事实上,编译器将所有局部函数指针都搬迁到了栈中已分配空间的低地址位,如程序exp.c在启用-fstack-protector编译后的汇编代码,编译器优化了栈结构布局,将b[10]、a[2]两个数组紧邻canary存放,而把变量a防止在栈顶位置。这种类型的防御措施非常关键,可有效降低攻击者使用各种shellcode 地址覆盖栈内局部函数指针从而实施攻击的可能性。GCC SSP栈保护机制中同样也使用了随机数“canary”探针,在局部变量与函数返回地址EIP之间插入一个canary探针,如图2所示。在函数的末尾代码中,内核校验栈内的canary值是否发生改变,若两个值不一致,则内核终止函数执行,如: prologue: movl %gs:20, êx ;取出canary movl êx, -4(?p) ;将canary放置在ebp-4处

. . .

epilogue : movl

xorl je .L3

call __stack_chk_fail

首先读取存放在%gs:0x20位置处的探针值,并插入到栈的指定位置,然后在函数指令末尾片段处重新读取事先插入的canary值,并将其与%gs:0x20位置处canary进行比较,若校验失败,GCC会调用__stack_chk_fail函数,输出错误消息中止运行。 系统启动进入保护模式后,start_kernel()会调用boot_init_stack_canary()来初始话一个stack canary,为每CPU变量产生一个canary探针,boot_init_stack_canary函数的实现可参考Linux源码文件:arch/x86/include/asm/stackprotector.h.在内核进一步初始化cpu的时候,会调用setup_stack_canary_segment()来设置每个cpu的GDT的stack canary描述符项,因为内核刚进入保护模式的时候, stack canary描述符的基地址被初始化为0,在cpu初始化的时候要重新设置为每cpu变量stack_canary的地址,而不是变量保存的值。通过这些设置当内核代码在访问%gs:0x14的时候,就会访问stack_canary保存的值。随后,每创建一个轻量级进程(LWP)时,它都会收到一个随机canary值,代码如下: unsigned int get_random_int ( void ){ struct keydata *keyptr; __u32 *hash = get_cpu_var ( get_random_int_hash ); int ret; keyptr = get_keyptr( ); hash[0] += current->pid + jiffies + get_cycles ( ); ret = half_md4_transform ( hash, keyptr->secret ); put_cpu_var (get_random_int_hash); return ret;

-4(?p), íx ;取?p-4位置处地址,将其拷贝给íx,即canary值

%gs:20, íx ;比较%gs:20与栈内保存的canary是否相等;

}

LWP大家可以理解为内核中的一个线程调度,每个LWP拥有一个canary,即意味着在处理每个线程的系统调用时,都会获取到一个canary,故多线程的用户级应用程序在内核端会产生多个canaries,与线程数一一对应。

虽然内核使用GCC选项-fstack-protector编译程序启用堆栈保护,但只为局部变量中含有8字节或更多的char数组的函数插入canary. 在内核镜像中有16604个函数,但大约仅有378个函数会得到“canary”探针保护,而且还存在着各种可能的溢出方式:(1)若程序中包含一个或多个结构体变量,SSP是不会对结构体内的成员变量重排列;(2)若函数接受多个形参变量如*printf(), SSP无法预测需要多少个参数,故编译器无法拷贝arguments到一个安全区域;(3)若程序中包含alloc函数、new等函数以创建一个动态数组,SSP则会将所有的这些动态数组放置到栈顶。Adam给出了一个远程canary利用技术,一字节一字节的测试,每次执行一次连接,返回一个错误信息,每个字节探测256次,大概方法:(1)首先定位出canary存放位置(2)使用“byte-by-byte”暴力方式破解canary值;(3)在canary位置,填充一个真实的canary值,然后在ret地址处安放shellcode地址,实施攻击。

Linux内核支持多类型的slab分配器,slub分配器是内核2.6.22版本引入的一种新解决方案。Slab分配器是一个动态的内存分配器(如内核堆分配器),可为每个对象的存储分配连续多个slabs. 通过为每种类型的对象预分配内存空间,slab分配器支持重用已被释放的对象以快速创建同类型的对象。Slub分配器会在每个已分配对象的末尾插入一个类型“canary”的区域即“Red Zone”,若对象正在使用中,Red Zone区包含‘0xcc’字节,空闲时为‘0xbb’.但,Red Zone并不是一个内核堆污染的安全防御机制,仅仅是一个帮助开发者确定“错误”代码是否超出了缓冲区的边界。为了激活Red Zone特性,编译内核时需要加SLUB_DEBUG选项,并带slub_debug=FZ参数重启内核。一旦检测到Red Zone被覆写,内核会做出如下响应: (1) 在控制台打印出调试信息,如图3所示; (2) 恢复Red Zone区内容;

(3) 恢复完毕后,继续执行程序。

图 3. RedZone 区污染后的内核输出信息

4. Windows内核漏洞利用迁移技术

Windows 7(NT 6.1)已经提供了大量内核利用迁移技术。Visual Studio编译内核组件和

驱动时,可以附带选项/GS(缓冲器安全性检测),其原理是函数开始执行时在栈中保存一个cookie,当函数返回前检查cookie是否被覆盖。在驱动上下文里,cookie一般存放在异常处理表和EIP、EBP寄存器之前,在函数退出之前,系统检验cookie是否污染。/GS保护函数条件:整个数组大小至少为5个字节,缓冲区使用_alloc函数分配且数据结构大小至少大于8字节。在win32k内核组件中,系统调用GsDriverEntry函数中的win32k! security init cookie函数初始化/GS内核cookie。安全Cookie的检查通过win32k!__security_check_cookie函数实现,若堆栈上的cookie值与win32k! security init cookie函数初始化的cookie一致,则函数正常退出。否则,程序

会转而执行错误异常处理函数__security_error_handler. 图4给出了/GS保护机制下栈结构布局:

low addressLocal variablesxor of cookiewith ebphigh addressException handler table...saved esp数据增长方向saved ret addr栈增长方向highest address图 4. /GS保护机制下的栈结构布局

saved frame ptrXor of cookieWith handlerTable’s addrhigher address

Windows内核的/GS保护机制的一个典型应用案例是ICMPv6路由播发缓冲区溢出漏洞

(MS10-009/CVE-2010-0239). 由于Windows TCP/IP栈在处理特制的ICMPv6路由播发报文时

没有执行充分的边界检查,导致攻击者可远程通过向启用了IPv6的计算机发送特制的ICMPv6报文来利用此漏洞。此漏洞触发后,进程在检查GS内核栈cookie值时发现已经受到破坏,内核立即中断执行,转入BSoD状态。文献《A Guide to Kernel Exploitation: Attacking the Core》中给出了两种绕过/GS内核栈cookie方法,但都需要一些特殊要求支持。第一个方式即在不破坏cookie的前提下覆盖返回地址EIP,这需要严格控制黑客攻击利用的shellcode地址。第二种方式是覆盖存储在堆栈上的异常处理表各函数地址,但异常处理表函数可能并不在内核内存中,因为目标驱动可能没有注册此异常处理函数。<>一文中给出了如何猜测__security_cookie地址值方法,要求本地特权提升攻击以获取内核模块的全局变量虚拟地址。Matthew借助NtQuerySystemInformation函数获取动态内核模块结构,如ntoskrnl.exe镜像文件,然后开始计算EBP和CPU tick(节拍)技术,因为他们发现驱动及模块的安全cookie并不是核心组件(如ntoskrnl.exe)而主要是通过KeTickCount产生的,如系统节拍计数值。基于此,他们计算了成功推算出安全cookie的概率大概为46%. Windows 7针对内核堆分配器也启用了堆元数据污染检测,使得传统的内存块unlink技术很难实现(利用堆区溢出,覆盖下一个将要被释放的chunk,然后在被覆盖的位置上构造攻击者自己的fake_chunk,并确保在free()函数调用过程中运行unlink宏,该宏所操作的内存将修改程序的流程转向注入的shellcode,达成攻击目的)。Windows 7实施的迁移保护机制完全类似其它内存分配器中的安全链表节点卸载(safe unlinking)。bh-dc-2011大会文章“Kernel pool exploitation on windows 7”给出了5种不同的方法实施堆利用攻击:

(1)当从ListHeads[n]队列中分配一个缓冲池chunk时,safe unlinking实现机制在unlink节

点时未验证待释放chunk的_list_entry,而是验证ListHeads[n]的_list_entry结构体,此时hacker可覆写待unlink的节点的Flink指针地址为一个恶意的shellcode实施攻击,如图5所示。

图 5. ListHeads[n]中分配chunks时的溢出攻击模式

图5中,safe unlinking机制仅仅是验证了ListHeads[n].Flink.Blink == ListHeads[n].Blink.Flink等式的成立性,但却忽略了unlinked节点。这里假设unlinked节点为A,若unlinking机制同时验证了A.Flink.Blink == A.Blink.Flink,则图5中的溢出方法则完全失效,因为此时A.Flink.Blink==ListHeads[n],而A.Blink.Flink==FakeEntry,显然无法通过safe unliking验证。 (2)系统未验证后备队列Lookaside list(用于分配固定大小的的内存池)中的每个entry节点的next指针值的合法性。攻击者可使用缓冲池污染漏洞,覆盖某个entry节点的next指针,使其指向一个恶意shellcode地址,故待系统为用户分配一个空闲lookaside chunk时,缓冲池分配并不会验证它的next指针是否合法,而直接返回next地址,导致pool污染。注:lookaside pool chunks的next指针节点存在一个8字节的pool header,故覆盖一个next指针至多需要12字节。

(3)PendingFrees链表中存储了待释放的pool entries节点,系统在遍历PendingFrees链表时未验证每个节点指针的合法性。故attacker同样可以利用pool污染漏洞破坏PendingFrees链表中任一Entry的next指针值,导致攻击者可将任意shellcode地址释放到一个选择好的pool描述符ListHeads链表中,接下来可能接管相应的缓冲池内存分配。

(4)利用池描述符结构体成员PoolIndex值在释放时未检查的缺陷,实施数组边界溢出,使其指向一个NULL指针的Pool描述符,通过映射一个虚拟的null页,攻击者可完全控制该pool描述符和它的ListHeads。

(5)污染指向进程对象的pool chunk指针。pool chunks 一般会选择性地存放一个负责本次池内存分配的进程对象指针。在x64位系统中,进程对象存放在pool header结构体的最后8字节位置,而在x86中,则直接附着在pool结构体内。攻击者通过覆盖此指针,可释放一个正在使用的进程对象。同样在释放任意一个pool分配时,释放算法都会交回此pool内存给相应的空闲链表或lookaside之前,检查pool类型中的配额(quota)位(0x8)。若此bit位已置位,则系统会调用nt!PspReturnQuota返回此quota. 故,攻击者通过覆盖进程对象指针,可降低任一进程对象指针索引值,导致进程对象引用计数不一致;更进一步,若进程对象指针被替换成用户模式下的内存指针,则攻击者完全可伪造一个EPROCESS结构体对象,从而控制结构体EPROCESS_QUOTA_BLOCK。

Windows 7允许未特权模式下实现NULL页映射,故内核NULL指针解引用是可利用的。攻击者可以把shellcode放置在内存0地址上,此时内核就会去运行用户进程的shellcode而不会抛出Oops。而且系统也没有对所有重要的内核结构实施ASLR机制保护,如页表/页目录。Windows NT内核导出了许多对内核模式shellcode开发有用的函数,故,在开发内核shellcode之前,需要使用nt组件下的函数寻找nt函数基地址。 5. 结论

尽管操作系统内核提供了一些基本的主动安全防御措施,但系统的安全性主要还是依赖于内核代码质量是否健壮,庞大的内核代码需要在每个功能设计时都考虑到安全性能是很难做到的,而且大量的安全性能加载到内核中,系统性能上是否能够得到保障也是一个很大的挑战。总之,内核利用并不是一个静态的域,故未来针对内核的各种利用手段依旧还不断出现,同样各种针对这些漏洞利用的内核迁移保护机制也会紧随其后,特别是虚拟化技术背后的内核利用技术将会是未来漏洞利用的导火索,最后给大家推荐Enrico Perla最新出版的一本有关内核漏洞利用的书籍<>,感兴趣的读者可以品读一番。

本文来源:https://www.bwwdw.com/article/rspp.html

Top