Posion Null Byte
how2heap 中的 poison_null_byte.c 的记录,但看来看去,其实就是 House of einherjar 的一个变体,而且属于是费尽心思最后绕半天路...
要求:
1. 只需 off by one,即可以复写某块下一个块的size,其中size只用改最后一位就可以了。
2. 知道 heap 地址效果:变体版的 Doube Reference,是一个指针指向内存 A,另一个指针指向内存 B,而 A 包含了 B
前置知识
务必要懂:
如下图所示,这是从 house of einherjar 拿来的图。我们构造了 size、fd 等数据,然后我们 free(c) 就会把红色框的部分合并。

重点就是要绕过检查 fd 和 bk 指针,如上图所示,我们设置成了 fd = bk = p,巧妙地顺利通过。因此这里隐含了一个要求:我们需要知道 heap 地址。
/* Take a chunk off a bin list. */
static void unlink_chunk(mstate av, mchunkptr p)
{
// ...
// 安全检查:如果当前chunk的fd的bk不等于当前chunk,或者bk的fd不等于当前chunk,说明双向链表链接出错,报错
if (__builtin_expect(fd->bk != p || bk->fd != p, 0))
malloc_printerr("corrupted double-linked list");
// 断链操作
fd->bk = bk;
bk->fd = fd;
// ....
}
细节说明
然后 posion null byte 本质上也是费尽心思绕过 fd 和 bk 的检查..
Step 1
构造如下的结构。其中 barrier 表示防止合并的块。prev 就是上面的 chunkA,victim 就是上面的 chunkC,我们其实完全可以按照 house of einherjar 的方法去做就行了...
... ...
padding
prev Chunk(addr=0x??0010, size=0x510)
victim Chunk(addr=0x??0520, size=0x500)
barrier Chunk(addr=0x??0a20, size=0x20)
a Chunk(addr=0x??0a40, size=0x500)
barrier Chunk(addr=0x??0f40, size=0x20)
b Chunk(addr=0x??0f60, size=0x520)
barrier Chunk(addr=0x??1480, size=0x20)
其实这里有一个要求,就是强行让 prev 从 0000 开始,这个是后面要用到的。
void *tmp = malloc(0x1);
void *heap_base = (void *)((long)tmp & (~0xfff));
size_t size = 0x10000 - ((long)tmp&0xffff) - 0x20;
void *padding= malloc(size);
Step 2
但是这里的方法是利用 a 和 b 两个新的块,按照下面的方式,把 prev 和这两个块都放在 large bin 中。
// 前三个 free 之后,放入到 unsorted bin 中
free(a);
free(b);
free(prev);
// 申请一个大块,会把上面三个都放到 large bin 中
malloc(0x1000);
此时结构是:
current large_bin: header <-> [b, size=0x520] <-> [prev, size=0x510] <-> [a, size=0x500]
此时 prev->fd_nextsize == prev->fd == a,prev->bk_nextsize == prev->bk == b。记住这,这个后面会大用处。
Step 3
重新申请得到这个 prev:
prev2 = malloc(prev.size)
和 house of einherjar 一模一样,我们溢出 victim 的 prev_in_use。这样 free(victim) 的时候,把 prev 合并了。

但是这里就有区别了:house of einherjar 需要构造 fake_chunk 的 fd 和 bk 指针,而这里不需要。
注意看我们这里 fake_chunk 的 fd,因为 fakechunk 就是 prev+0x10,所以它其实是 prev 的 fd_nextsize,而上一步中 prev 之前是在 largebin 中,而且从 largebin 中拿出来的时候 fd_nextsize 是不会修改的!!
PS: 从 largebin 拿出来之后的
fd也没被修改
所以此时 fake_chunk->fd = prev->fd_nextsize = a 和 fake_chunk->bk = prev->bk_nextsize = b,如果 a->bk == fake_chunk,那我们就绕过了对 fd 和 bk 的检查。
Step 4
但问题是 a->bk != fake_chunk 呀... 当 prev 在 largebin 的时候,a->bk == prev;当 prev 从 largebin 拿出来之后,此时 largebin 只有 b <--> a,即 a->bk == b 了。
但回顾 Step 1 的构造,我们有一个要求是:强制 prev 从 0x??0000 开始的(下面的 addr 是数据区)。
... ...
padding
prev Chunk(addr=0x??0010, size=0x510)
victim Chunk(addr=0x??0520, size=0x500)
barrier Chunk(addr=0x??0a20, size=0x20)
a Chunk(addr=0x??0a40, size=0x500)
barrier Chunk(addr=0x??0f40, size=0x20)
b Chunk(addr=0x??0f60, size=0x520)
barrier Chunk(addr=0x??1480, size=0x20)
一个关键信息:所以块的偏移量我们是知道的,即低两位字节是已知的。 举例而言,由于 a->bk == b,那么 a->bk 最后就是 0x??0f60,我们把 a->bk 修改成 0x??0010 就可以了,这里的 0x??0010 就是 fake_chunk 的头部...
所以我们把 a 和 b 也申请出来,然后进行修改即可,而且只用修改最后两个字节。
b2 = malloc(b.size)
((char*)b2)[0] = '\x10';
((char*)b2)[1] = '\x00'; // b->fd <- fake_chunk
// 注意这里是 char,所以用 8 和 9
a2 = malloc(a.size)
((char*)a2)[8] = '\x10';
((char*)a2)[9] = '\x00'; // a->bk -> fake_chunk
这里有个 Trick,当 b 取出来的时候,此时 large_bin <--> a,高地址被改变了。所以可以先把 a 放入到 unsorted 中,再把 victim 放入 unsorted 中。
void *a2 = malloc(a.size);
free(a2);
free(victim);
此时 unsorted 是 unsorted <--> victim <--> a,然后再拿出来就行了...
void *a3 = malloc(a.size);
((char*)a3)[8] = '\x10';
((char*)a3)[9] = '\x00';
victim2 = malloc(victim.size)
Step 5
之后就修改 victim 的 prev_in_use 和 prev_size,还有构造 fake_chunk 的 size,然后 free(victim) 即可,达到和 House of einherjar 一样的效果。
评价
整体上感觉就是好忙... 忙活大半天最后终于实现了 house of einherjar 的效果。
好处是不用知道 heap 地址,但这一点存疑:在最开始的时候需要 prev 从 0x??0000 开始,我感觉就很苛刻了,这一步可能就需要知道 heap 上变量的地址才行。how2heap 示例中关于这一步的代码:
void *tmp = malloc(0x1);
void *heap_base = (void *)((long)tmp & (~0xfff));
size_t size = 0x10000 - ((long)tmp&0xffff) - 0x20;
void *padding= malloc(size);