NX & ASLR



0. Introduction

谈起ASLR与NX,就不得不提到他们的前身,Pax
在wiki上,对Pax的定义如下:

PaX is a patch for the Linux kernel that implements least privilege protections for memory pages. The least-privilege approach allows computer programs to do only what they have to do in order to be able
to execute properly, and nothing more.

但是由于各种原因,Pax并没有能够完全应用在linux的内核中,致力于内核安全的开发者结合Pax的思想,不断的尝试把PaX的代码分拆成小的patch提交给Linux内核社区,最终也就出现了现在我们所熟知的ASLR/RELRO/NX/CANARY/FORITY/PIE等保护技术

Pax

PaX是针对linux kernel的一个加固版本的补丁,它让linux内核的内存页受限于最小权限原则,是这个星球上有史以来最极端和最优秀的防御系统级别0day的方案。
其基本的保护思想我个人总结为:Pax只让用户做正确的,应该做的事
Pax 由PaX Team负责维护,Grsecurity团队实现了包括了RBAC(基于角色的访问控制)和一系列对PaX的改进
Pax的涉及到NX与ASLR的设计思想如下:

PaX – SEGMEXEC
PaX – PAGEEXEC
PaX – KERNEXEC
PaX – ASLR

其中SEGMEXEC ,PAGEEXEC ,KERNEXEC 都是针对代码是否可执行下功夫,分段,页,内核三个相互关联的部分进行设计,它的实现是部分基于硬件,部分基于软件的
ASLR就是内存地址随机化,它的实现主要基于软件

NX&ASLR

缓冲区溢出(Buffer Overflow),简称BOF,目前仍就是威胁最大,最广的一类漏洞之一
在20年前在那个还没有出现NX的时代,攻击者能够利用BOF轻易地将shellcode
注入到内存中,然后利用类似jmp esp指令轻松地hack掉程序的控制流
这种攻击之所以能够奏效的一个重要原因:栈是可执行,而栈作为一个存储数据,程序上下文的一块内存区域,按道理来讲,是不应该有执行权限的
所以Nx (No-eXecute),我们也可以粗略地称之为栈不可执行应运而生

同样,ASLR的出现也有其特定的历史背景
由于NX的引入,出现了众多的Ret2libc,Offset2lib类似的攻击技术,尤其是ROP技术的出现,可以说基本上完全攻陷了NX
这样一种背景下,如何能够加大攻击者进行ROP的难度,从而减低危险等级,ASLR提供了一个很好的思路,就是将libc这样的库地址随机化,使得攻击者ret2libc时无法确定libc的正确地址,而仅仅利用程序本身的Ropgets又无法完成shellcode的功能,从而对攻击进行缓解(Mitigation)
ASLR的出现一定程序上提高了ROP的难度

在目前的操作系统中,NX,ASLR,Canary基本上是所有程序的保护标配(gcc编译默认开启),本次我们只针对NX与ASLR两种保护

1. Linux’s NX & ALSR

No-eXecute

NX在老版本的gcc参数选为-z execstack/noexecstack
新版本gcc 默认开启NX,经试验可以通过-fno-stack-protector -z execstack关闭NX

两个程序的区别,很明显,栈内存空间的权限不同,NX没有开启的test_NoNx的栈有可执行权限的

当我们编译一个程序时,Nx的选项大致的处理流程如下:感激Harden-linux精辟的讲解

gcc检查比并传递Nx的参数
ld生成elf时将相应的标志位置位
kernel捕获相应标志位
CPU中的具体实现Nx

在通过gcc的将相应的NX的参数传递给ld链接器是一个关于gcc处理参数的复杂的过程,这里不在赘述
在连接器中,描述段权限的宏位于binutils-2.26.1/include/elf/common.h中,类似文件权限是rwx型的描述方式

在ld中传入将stackexec的标志传入到bfd_link_info结构体中:

在ldmain.c中获取完相应的参数后进入parse_args中处理参数

其中在调用的最后
ldwrite()生成elf文件之前设置好了相应的elf的stack的权限

可见通过设置elf_tdata中的stack段的标志来更改确定elf相应的权限,最后通过ld_write()函数生成elf文件,其中在section到segment的映射时将该标志保留,并设置好Stack的执行权限

上面的整个链接过程后,将elf的STACK位设置位了不可执行,但是如果我们在Stack区域内执行代码,kernel同样有相关的支持阻止我们执行
如果违反了 访问规则,kernel将会捕获并处理这个非法访问
简单来说,kernel在初始化stack的地址时,在建立stack的虚拟内存结构vma struct时

当进程访问该进程Stack内存页时,由于只有虚拟内存结构,并没有实际映射物理内存,会触发page_fault(),这个异常的作用就是用来将页面调入到相应的位置,是页面调度算法的重要组成部分
在page_fault中,会对vma进行检查,调用access_error

在该函数中检查用户的操作是否时exec,如果是与我们的vma中的flag不相符合,access函数返回1,则整个kernel返回报错,程序abort

最后,nx的实现也需要一定的cpu的支持,其中Intel的帮助如下:

IA32_EFER.NXE仅仅对PAE和IA-32e模式起作用(因为只有PAE/IA-32e模式下的paging单元(页表项/页目录表项)是64位的).如果IA32_EFER.NXE = 1,从某一线性地址处的指令预取将会被 禁止,即使这一线性地址处的数据访问是允许的.
当然不同的cpu由于架构不同,对nx的支持方式也不尽相同

综上,整个NX对Stack不可执行的保护时一个十分复杂的过程,它需要ld,kernel,cpu等等的完美配合
但是同样NX的引入提高了整个BOF的攻击门槛,它其实是stack overflow攻击的最重要的解决方案.

Address space layout randomization

ASLR同样也是Pax中的一项重要的技术,对ASLR的大致介绍如下:

The success of many cyberattacks, particularly zero-day exploits, relies on the hacker’s ability to know or guess the position of processes and functions in memory. ASLR is able to put address space targets in unpredictable locations. If an attacker attempts to exploit an incorrect address space location, the target application will crash, stopping the attack and alerting the system.

由此可见ASLR也提高了整个栈溢出的难度,ByPass ASLR是Attackdr的基本功
在linux中,可以在su后通过echo 0 > /proc/sys/kernel/randomize_va_space关闭ASLR
例如:randomize_va_space开启的值时2,当aslr开启时,我们可以看到test程序所有的共享库地址时在不断变化的

而当我们关闭了ASLR后,整个库的地址就固定了不再改变了

本文中主要分析ASLR的思想,其具体原理在PIE保护中介绍
如下图是一个进程的地址空间分布图:

最简单理解下的ASLR就是在kernel Space的顶部加入一段随机大小的空间,Random offset,使得 整个Stack,libc,heap等等地址全部无法估计。
elf的加载地址时是不会随机化的,linux下ld会给elf指定一定固定的装载地址,在32bit下,这个地址在0x804000,64bit则为0x4000000
当然这只是最浅显的理解,真正的ASLR比这个复杂许多,对于一个i386的程序来说,32bit长的地址,不同的分配方式可能有不同的随机位方式,甚至有不同的随机算法,如下:

for i386 we have Rs = 24, Rm = 16, Rx = 16, Ls = 4, Lm = 12 and Lx = 12 (e.g., the stack addresses have 24 bits of randomness in bit positions 4-27 leaving the least and most significant four bits unaffected by randomization). The number of attacked bits represents the fact that in a given situation more than one bit at a time can be attacked (obviously A <= R), e.g., by duplicating the attack payload multiple times in memory one can overcome the least significant bits of the randomization.

这样的随机化当然造成了很多的麻烦,所以ASLR还有许多对各部分随机地址空间的起始位置和大小的限制
但是,站在一个attacker角度,程序运行的内存每次不同,无法估计,几乎无法碰撞,ASLR给漏洞利用带来了新的挑战,而关于ASLR的部分技术细节,会在PIE的介绍中再做研究。

2. Bypassing NX and ASLR

Bypass Nx与ASLR的技术在10年前就已经比较成熟了,基本分成了两个大步骤做绕过

1.Rop绕过Nx
2.leak memory绕过ASLR

Ret2libc

Ret2libc是一个非常经典的Bypass手段,我们以write函数为例,我们知道GOT表中在lazy-binding后储存着函数的真实地址,我们可以利用相关函数它来将函数的真实地址泄露出来,以write函数为例,构造栈如图:

虽然libc加载的地址空间时随机的,但是它是一段连续的内存空间,得知其中一个函数的地址,我们就可以根据offset计算出libc的基地址:
\[
libc\_base = write\_addr – libc.symbols[‘write’]
\]
这样就能够定位到其他libc中的函数,变量,字符串,重复利用漏洞,最终实现执行system("/bin/sh")

Ret2dl_resolve

Ret2dl_resolve早在15年前的Phark杂志中竟然就有提出,惊!The advanced return-into-lib© exploits
简单来说,它的基本做法就是欺骗dl_reslove函数让它错误解析成我们需要的libc中的函数,从而不需要泄露,我们就能够调用libc中的函数,相当于BYpass ASLR
其原理解释如下:
ELF文件的动态链接区段中包含了用于运行时解析函数地址的信息。其内容示例如下:

其中.rel.plt是用于函数重定位,.rel.dyn是用于变量重定位。具体地,其内容如下:

.rel.plt里包含4个条目。PLTRELSZ即为.rel.plt的总大小,32 bytes;PLTREL则指明这些条目的类型为REL;RELENT指明了每个REL类型条目的大小,8 bytes。于是32/8=4即为条目个数。
其类型是Elf32_Rel,其定义如下

我们以.rel.plt第一条,即read的条目为例,调试器结果:

而r_info则保存的是其类型和符号序号。其类型为ELF32_R_TYPE(r_info)=7,对应于R_386_JUMP_SLOT;其symbol index则为RLF32_R_SYM(r_info)=1。

注意到之前 readelf -r所得到的结果中,包含有Sym.Value和Sym. Name信息。而这些信息就是通过symbol index找到的。具体地,.dynamic section中的SYMTAB,即.dynsym section,保存的便是相关的符号信息。每一条symbol信息的大小在SYMENT中体现,为16 bytes。通过$ readelf -s来查看其内容如下:

可以看到,之前所说的read函数的符号信息条目index确实为1。我们通过调试器来看看其实际内容:

对比符号条目的定义如下:

其结果与\( readelf -r, \) readelf -s的结果相符。具体地,st_name保存的是该符号名称在STRTAB,即.dynstr中的地址:

上述就是完整一次动态链接符号解析所需要的内容

在Lazy-binding的方式下,第一次调用plt_read函数会调用dl_resolve来确定函数的真实地址,从而对GOT表进行绑定,它的大致过程如下:

dlruntime_resolve则会完成具体的符号解析,填充结果,和调用的工作。具体地。根据rel_offset,找到重定位条目:

根据rel_entry中的符号表条目编号,得到对应的符号信息:

再找到符号信息中的符号名称:

由此名称,搜索动态库。找到地址后,填充至.got.plt对应位置。最后调整栈,调用这一解析得到的函数。

于是,我们的思路是,提供一个很大的数作为rel_offset给_dl_runtime_resolve,使得找到rel_entry落在我们可控制的区域内。构造伪条目,使得所对应的符号信息、符号的名称,均落在我们可控的区域内,那么就可以解析我们所需的函数地址并调用了。值得注意的是,在解析过程中,还会对ELF32_R_TYPE(rel_entry->r_info)等进行检查。但这些数据我们只需仿照正常的来构造即可,重点是对应的伪条目的index应计算正确。

roputils工具里提供了ret2_dl_reslove的快速方式,它在伪造rel_entry的方式如下:

从base开始便是用户可控的区域,也是用来构造伪Elf32_Rel, 伪Elf32_Sym,和符号名称的地方。具体的存放地址,还是根据数组条目的大小进行了对齐。而需要检查的地方,则全部硬编码了,只需计算这些伪条目对应在数组中的index填充即可。

其次便是函数dl_resolve_call了。其定义如下:

可以看到,这里将所调用的函数的参数及返回的gadget放在栈上,再往上便是构造的伪Elf32_Rel条目的offset,最后则是.plt起始处的地址,在那里会完成将link_map放至栈上及调用_dl_runtime_resolve,完成我们需要的libc库函数的解析工作
对于ret2dl_resolve,我们进行漏洞利用的基本想法如下:

1.控制eip为PLT[0]的地址,传递一个index_arg参数
2.控制index_arg的大小,使reloc的位置落在可控地址内
3.伪造reloc的内容,使sym落在可控地址内
4.伪造sym的内容,使name落在可控地址内
5.伪造name为任意库函数,如system
其中伪造的过程,我们可以通过ropuntils帮助我们完成,最后实现ret2libc

mmap

mmap是linux中的一个重要的内存映射函数
定义函数:void *mmap(void *start, size_t length,int prot,int flags,int fd,off_t offsize)
函数说明:mmap()用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写。
参数说明

我们可以利用ret2libc来执行mmap映射一段rwx的内存区域
由于NX的存在,栈不可执行,我们无法在shellcode注入到栈中执行,但是我们可以设置mmap映射的空间可执行,这样将shellcode注入到mmap内存中,然后跳转执行shellcode即可

mmap的绕过方式用了另外一种方式绕过NX,它让之前被NX打击的shellcode技术重新有了用武之处,是一种很经典的利用思想

3. Exploitation

我们的binary如下,用到的工具pwntools与roputils,程序是一个很简单的栈溢出程序,主程序如下:

在调用sub_0x80483F4时发生了溢出,能够覆盖返回地址,并且程序仅仅开启了NX与ASLR保护

FristWay

首先我们利用ret2libc的方法期望实现

1.利用write函数进行泄露,选择got表,泄露出write函数的真实地址
2.根据该地址计算出libc_base,libc被加载到的基地址
3.根据libc中的偏移计算出system与/bin/sh字符串的真实地址
4.重用漏洞,ret2libc 执行system(“/bin/sh”)

SecondWay

接下来我们利用ret2dl_resolve
我们的目的时构造一个fake 来欺骗ld_resolve,不需要通过泄露地址直接获取到system的addr,之后ret2libc
利用roputils工具,该工具内置ret2dl_resolve的具体实现,能够快速完成整个过程

ThridWay

利用mmap内存映射来做shellcode的注入攻击

1.利用leak information绕过ASLR,计算出mmap的真实地址
2.利用mmap ret2libc,将0xbeef0000内存的一块内存映射到内存中,并设置好rwx的权限
3.重用漏洞,向0xbeef0000写入shellcode
4.跳入shellcode执行,geshell

4. Afterword

在目前的linux操作系统中,NX+ASLR+Canary是目前所有的Binary保护的标配,所以BYpass NX与ASLR是linux下漏洞利用者的一个基本功

NX与ASLR都是Pax项目的重要组成部分,Nx限制了栈,使其不可执行,而ASLR地址随机化技术,使得Attacker的利用更加困难
在相对深入地了解了NX与部分ASLR的思想与实现之后,我们更多的应该思考如何Bypass

其中ret2lib与ret2dl_reslove的利用思路基本一致,既然无法注入shellcode,那就在有限的binary和shared library中寻找ropGodgets来组合成shellcode。ret2dl_reslove更多的是在Bypass ASLR与ret2libc不同,ret2libc时利用地址泄露,计算偏移,定位库函数;ret2dl_reslove手动模拟解析linux的lazy binding的过程,得到库函数地址。这种利用方式一个重要限制就是如果目标binary中没有我们必须要用的指令,事情将会变得十分麻烦,并且在ASLR开启的情况下,库地址的随机化也会很大程度上提高漏洞利用的难度

而mmap的思想另辟蹊径,找到并设置一个内存空间防止shellcode并跳入执行,但是同样mmap的地址难以确定,并且mmap可是6参数的函数,在32bit的binary中参数存栈还比较好构造漏洞利用,在64bit的binary中,参数在寄存器上,想找到相应的godgets着实困难,所以除非在binary给出mmap的情况下,一般这种方式比较少见

综上所述,无论是NX,ASLR的保护策略,还是他们的Bypass都体现着attacker和defencer的攻防对抗,闪耀着网络攻防的魅力

5.reference

Phrak 58:The advanced return-into-lib© exploits
Hardened GNU/Linux NX(No-eXecute)的实现分析
Offset2lib: bypassing full ASLR on 64bit Linux
Design ASLR

发表评论