CTF竞赛权威指南(Pwn篇)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.3 No-eXecute

4.3.1 简介

No-eXecute(NX),表示不可执行,其原理是将数据所在的内存页(例如堆和栈)标识为不可执行,如果程序产生溢出转入执行shellcode时,CPU就会抛出异常。通常我们使用可执行空间保护(executable space protection)作为一个统称,来描述这种防止传统代码注入攻击的技术——攻击者将恶意代码注入正在运行的程序中,然后使用内存损坏漏洞将控制流重定向到该代码。实施这种保护的技术有多种名称,在Windows上称为数据执行保护(DEP),在Linux上则有NX、W?X、PaX、和Exec Shield等。

NX的实现需要结合软件和硬件共同完成。首先在硬件层面,它利用处理器的NX位,对相应页表项中的第63位进行设置,设置为1表示内容不可执行,设置为0则表示内容可执行。一旦程序计数器(PC)被放到受保护的页面内,就会触发硬件层面的异常。其次,在软件层面,操作系统需要支持NX,以便正确配置页表,但有时这会给自修改代码或者动态生成的代码(JIT编译代码)带来一些问题,这在浏览器上很常见。这时,软件需要使用适当的API来分配内存,例如Windows上使用VirtualProtect或VirtualAlloc,Linux上使用mprotect或者mmap,这些API允许更改已分配页面的保护级别。

在Linux中,当装载器将程序装载进内存空间后,将程序的.text节标记为可执行,而其余的数据段(.data、.bss等)以及栈、堆均为不可执行。因此,传统的通过修改GOT来执行shellcode的方式不再可行。但NX这种保护并不能阻止攻击者通过代码重用来进行攻击(ret2libc)。

如下所示,Ubuntu中已经默认启用了NX。GNU_STACK段在禁用NX时权限为RWE,而开启后权限仅为RW,不可执行。

脚本checksec.sh对NX的检测也是基于GNU_STACK段的权限来进行判断的。

4.3.2 实现

我们来看看NX在binutils和Linux内核里的相关实现,首先是处理编译参数,当传入“-z execstack”时,参数解析的调用链如下所示,在handle_option()函数中会对link_info进行设置(execstack和noexecstack)。

然后,需要做一些分配地址前的准备工作,比如设置段的长度,调用链如下所示,根据link_info里的值设置GNU_STACK段的权限stack_flags。

最后,就是生成ELF文件,调用链如下所示。在第2章中我们讲过,每段都包含了一个或多个节,相当于是根据不同的权限对这些节进行分组,从而节省资源。因此,首先要将各个section和对应的segment进行映射,我们主要关心GNU_STACK段,可以看到程序根据stack_flags的值来设置p_flags。

到这里ELF文件已经编译完成,接下来我们看Linux-4.15将其加载执行时的情况。在load_elf_binary()函数中根据p_flags进行权限设置。

然后将executable_stack传入setup_arg_pages()函数,通过vm_flags设置进程的虚拟内存空间vma。

当程序计数器指向了不可执行的内存页时,就会触发页错误,在__do_page_fault()里将vma作为参数传入access_error(),成功捕获到错误。

4.3.3 示例

下面给出一个存在缓冲区溢出的示例程序,我们将分别在关闭和开启NX保护的情况下进行漏洞利用。

先看关闭NX的情况,为了避免其他安全机制的干扰,我们还需同时关闭canary和ASLR。可以看到程序a.out存在一个RWX权限的段。

在gdb里调试一下,输入一段超长字符串,程序成功崩溃,出错的地址是0x6261616b,位于缓冲区偏移140字节的位置,通过计算$esp-140-4即可得到缓冲区地址,减4是因为程序执行到最后从栈里弹出EIP,所以抬升了4字节。

构造的利用代码是这样的形式“shellcode+AAAAAAAA...+ret”,其中ret指向shellcode,也就是缓冲区地址。payload如下所示:

但由于真实环境与gdb环境存在差距,所以上面的脚本并不会成功,返回地址是需要通过core dump来确定的,如下所示。

于是就得到了真实环境的地址0xffffcd00,替换后重新运行exp,即可获得shell。

接下来,我们来看开启NX保护的情况,重新编译得到b.out。

此时我们自己注入的、放在栈上的shellcode就不可执行了,因此只能使用程序自有的代码进行重放攻击,例如ret2libc,改变程序执行流到libc中的system("/bin/sh")。在关闭ASLR的情况下,libc的地址是固定的,system()和"/bin/sh"相对基地址的偏移也是固定的,所以可以直接硬编码到exp里,如下所示。