off-by-one 利用技巧
Linux free函数原理:
由堆块头部形成的隐式链表可知,一个需释放堆块相邻的堆块有两个:前一个块(由当前块头指针加pre_size确定),后一个块(由当前块头指针和size确定)。从而,在合并堆块时会存在两种情况:向后合并、向前合并。当前一个块和当前块合并时,叫做向后合并,当后一个块和当前块合并时,叫做向前合并。
1 | /* Treat space at ptr + offset as a chunk */ |
off-by-one 利用思路
向前合并
1 | /* consolidate forward */ |
若有一个chunk P将被free,则Glibc 首先通过 P + P->size 取出其物理相邻的后一个chunk BK,紧接着通过 BK + BK->size 取出与BK 块物理相邻的后一个 chunk 并首先检查其 prev_inuse 位,若此位为清楚状态则证明 BK 为 freed 状态,若是则进入下一步检查:
- 此时若证明 BK 是 allocated 状态,则将 BK 的 Prev_inuse 位清除,然后直接执行 free后返回;
接下来检查 BK 是不是 Top chunk,若不是则进入向前合并的流程:
- 若BK 是 Top chunk,则将其和P进行合并。
向后合并流程如下:
- 让 BK 进入 unlink 函数
- 修改 P-> size为 P-> size + BK-> size,以此来表示 size大小已经合并
向后合并
1 | /* consolidate backward */ |
首先检查 P 的 prev_inuse 位是否为清除状态,若是则进入后向合并的流程:
- 首先通过 p - p->prev_size 取出物理相邻的前一个 chunk FD
- 修改 p->size 为 p->size + FD->size(以此来表示 size大小已经合并)
- 让 FD 进入 unlink 函数
构造 Heap Overlap
有三种方式:
- 通过 off by one 漏洞来修改 chunk 的 size域 涉及到的 Glibc 堆管理机制中空间复用的相关知识;
- 若内存中有如下布局(Chunk B、Chunk C 均为 allocated 状态):
1 | +++++++++++++++++++++++++++++++++++++++++++ |
在 Chunk A 处触发 off-by-one 漏洞,将 Chunk B的 size 域篡改为 Chunk B + Chunk C 的大小,然后释放 Chunk B,再次取回,就可以对Chunk C 的内容进行任意读写
注意:
- 篡改 Chunk B的 size 域时,仍要保持 prev_issue 位 为1,以免触发堆块合并
- 篡改 Chunk B 的 size 域时,需要保证将 Chunk C完全合并,否则将无法通过以下所述的验证。
1 | // /glibc/glibc-2.23/source/malloc/malloc.c#L3985 |
- 若内存中有如下布局(Chunk B 为 freed 状态、Chunk C 为 allocated 状态):
1 | +++++++++++++++++++++++++++++++++++++++++++ |
在 Chunk A 处触发 off-by-one 漏洞,将 chunk B 的 size 域篡改为 Chunk B + Chunk C 的大小,然后取回 Chunk B,此时就可以对 Chunk C 的内容进行任意读写了。
注意:
- 篡改 Chunk B 的 size 域时,仍要保持 prev_inssue 位为1,以免触发堆块合并
- 篡改 Chunk B 的 size 域时,需要保持将 Chunk C 完全包含,否则将无法通过验证
- 接下来是一种比较困难的构造方式,首先需要内存中是以下布局:
1 | +++++++++++++++++++++++++++++++++++++++++++ |
其中,Chunk A 的 prev_inuse 位 置位,此时 三个 chunk 均为 alloacted 状态
保证 Chunk C 的 size 域一定要是 0x100 的整数倍,那么首先释放 Chunk A,再通过 Chunk B 触发 off-by-null,此时 Chunk C 的 prev_inuse 位被清除,同时构造 prev_size 为 Chunk A -> size + Chunk B -> size,然后释放 Chunk C,此时 因为 Chunk C 的 prev_inuse 位被清除,会导致向后合并的发生,从而产生一个大小为 Chunk A, Chunk B, Chunk C 之和的 chunk,再次取回后即可伪造 Chunk B 的结构。
Glibc 利用思路
Glibc 2.3.2(or < Glibc 2.3.2)
首先我们利用的重点是 unlink:
1 | // In /glibc/glibc-2.3.2/source/malloc/malloc.c |
初始 Glibc 中,unlink 函数没有任何防护,直接就是简单的拖链操作,一旦我们能控制 P 的 fd 域为 Fake_value,bk 域为 Addr - 3 * size_t,那么在那之后执行 BK->fd = FD 时将会实际执行 (Addr - 3*Size_t)+ 3 * Size_t = Fake_value 进而完成任意地址写。
In Glibc 2.23
1 | // /glibc/glibc-2.23/source/malloc/malloc.c#L1414 |
在 Glibc 2.23 中,加入了两个检查,一个是在执行实际拖链操作前的链表完整性检查。
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |
这里就是检查 (P->fd) -> bk == p == (P->bk)->fd,若我们能得到 P的 地址位置,如假设 P 的地址存储在 BSS 段中的 chuk_addr 处,那么篡改 P->fd 为 Chunk_addr - 4 * Size_t, P-> bk 为 Chunk_addr - 3*Size_t。那么在进行检查时:
1 | Chunk_addr - 4*Size_t + 4*Size_t == Chunk_addr == Chunk_addr - 3*Size_t + 3*Size_t |
实际执行拖链后:
1 | Chunk_addr - 4 * Size_t + 4 * Size_t = Chunk_addr - 3 * Size_t |
也就是 Chunk_addr = Chunk_addr - 4 * Size_t,若还有其他 Chunk 地址在 Chunk_addr 周围,我们就可以直接攻击对应项,如果程序存在读写 Chunk 的函数且没有额外的Chunk 结构验证,就可以进行任意地址读写了。
Glibc 2.27(Ubuntu 18.04)
1 | * consolidate backward */ |
Unlink内部变化:
1 | // In /glibc/glibc-2.27/source/malloc/malloc.c#L1404 |
和 Glibc 2.23 相比,最明显的是增加了关于 prev_size 的检查:
1 | if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) |
这一项会检查即将脱链的 chunk 的 size域是否与他下一个 Chunk 的 prev_size 域相等,这一项检查事实上对向后合并的利用没有造成过多的阻碍,只需要提前将 chunk 0 继续宁一次释放即可:
1 | 1. 现在有 Chunk_0、Chunk_1、Chunk_2、Chunk_3。 |
Glibc 2.29(Ubuntu 19.04) 的新变化
由于 Ubuntu 19.04 是非 LTS版本,因此其软件源已经失效,因此若需要继续使用,需要把apt 源修改为 18.04 的软件源,两个版本相互兼容:
合并操作变化:
1 | * consolidate backward */ |
合并操作增加了新保护:
1 | if (__glibc_unlikely (chunksize(p) != prevsize)) |
这里和上文所述 (chunksize(p) != prev_size(next_chunk(p)))
是有本质区别的:
1 | 1. 检查 prev_inuse 位是否置位,来决定是否触发向后合并。 |
unlink 内部变化:
1 | // In /glibc/glibc-2.29/source/malloc/malloc.c#L1460 |
和 Glibc 2.27 相比,最明显的其实是整个宏定义被变更成了函数,其中保护并没有发生更多的改变。
区别是:如果我们要继续完成利用,就需要修改 fake chunk 的size 域,这样就能满足 size 和 prev_size相等。
2020-GKCTF Domo
程序分析
程序在申请chunk时存在 off-by-null 漏洞,会在输入的末尾加上一个 0。
在edit 函数里,允许我们修改一次 任意地址 一字节。
利用分析
- 泄露地址
首先需要泄露 libc 地址和 heap 地址。泄露 Libc 地址可以利用 unsortedbin 来泄露,分配一个 unsortedbin 在fd 和Bk 指针中会有 main_arena+88 的地址。 泄露 heap 地址可以利用 fastbin 来泄露,fastbin 会在 fd 和 bk 中存储后续空闲堆块的 地址。
- 分配伪造堆块
这道题,由于开启了各种保护,同时还对 malloc_hook 和 free_hook 的值进行了检查,所以 只能通过 FSOP 修改 vatbel 指针来getshell。 首先就需要 先伪造一个堆块到 _IO_list_all ,然后修改 _IO_list_all 到我们的 fake_IO_struct;
- 伪造 fake_IO_struct
需要自己伪造一个 fake__IO_file_jumps,将 IO_vtable 的值伪造为指向 fake_IO_file_jumps 地址,在 fake_IO_file_jumps 布置上 one_gadgets 地址。
EXP
1 | from pwn import * |
hitcon-2018 children Tcache
程序分析
strcpy() 时,会把字符串结束符 \x00 也一同拷贝到目的地址,所以存在一个 off-by-null 漏洞。
利用分析
EXP
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/09/15/Off-by-one漏洞练习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!