Unsorted Bin Attack

Reading time: 15 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

基本信息

有关什么是 unsorted bin 的更多信息请查看此页面:

Bins & Memory Allocations

Unsorted lists 能够在 chunk 的 bk 字段中写入 unsorted_chunks (av) 的地址。因此,如果攻击者能够修改位于 unsorted bin 内某个 chunk 的 bk 指针地址,就可能在任意地址写入该地址,这对于 leak Glibc 地址或绕过某些防护很有帮助。

所以,基本上,这个攻击允许在任意地址设置一个很大的数值。这个大数值是一个地址,可能是 heap 地址或 Glibc 地址。传统目标是 global_max_fast,用于允许创建更大尺寸的 fast bin(并从 unsorted bin attack 转为 fast bin attack)。

  • 现代说明 (glibc ≥ 2.39):global_max_fast 已变为 8 位全局变量。通过 unsorted-bin 写入指针到该位置将会破坏相邻的 libc 数据,并且不再可靠地提升 fastbin 限制。在对抗 glibc 2.39+ 时,优先考虑其他目标或其他原语。见下文“Modern constraints”,并在得到稳定原语后考虑结合其他技术,例如 large bin attackfast bin attack

tip

T> 看一下 https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle 提供的示例,并将 chunk sizes 从 0x400/0x500 改为 0x4000/0x5000(以避免 Tcache),可以看到现在会触发错误 malloc(): unsorted double linked list corrupted

因此,现在这个 unsorted bin 攻击(以及其他检查)也要求能够修复双向链表以绕过 victim->bk->fd == victim 或者 victim->fd == av (arena) 的检查,这意味着我们想写入的地址在其 fd 位置必须保存伪造 chunk 的地址,并且该伪造 chunk 的 fd 指向 arena。

caution

注意这个攻击会破坏 unsorted bin(因此也会影响 small 和 large)。因此我们现在只能使用来自 fast bin 的分配(更复杂的程序可能会执行其他分配并崩溃),并且为了触发这一点我们必须分配相同大小否则程序会崩溃。

注意覆盖 global_max_fast 在这种情况下可能有帮助,前提是信任 fast bin 能够处理所有其他分配直到 exploit 完成。

来自 guyinatuxedo 的代码对此解释得很好,不过如果你修改 mallocs 以分配足够大的内存从而不进入 Tcache,你会看到前文提到的错误阻止了该技术:malloc(): unsorted double linked list corrupted

写入是如何实际发生的

  • unsorted-bin 写入在 free 时触发,当被释放的 chunk 被插入到 unsorted list 的头部。
  • 在插入过程中,分配器执行 bck = unsorted_chunks(av); fwd = bck->fd; victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;
  • 如果你能在调用 free(victim) 之前将 victim->bk 设置为 (mchunkptr)(TARGET - 0x10),最后的语句将执行写入:*(TARGET) = victim
  • 之后,当分配器处理 unsorted bin 时,完整性检查会验证(除其它检查外)bck->fd == victimvictim->fd == unsorted_chunks(av),否则会中止并报 malloc(): unsorted double linked list corrupted。因为插入时已经把 victim 写入了 bck->fd(我们的 TARGET),如果写入成功这些检查可以被满足。

现代限制(glibc ≥ 2.33)

为了在当前 glibc 上可靠地使用 unsorted‑bin 写入:

  • Tcache 干扰:对于落入 tcache 的大小,free 会被导向 tcache 而不会触及 unsorted bin。要么
    • 使用大于 MAX_TCACHE_SIZE 的请求(在 64‑bit 上默认 ≥ 0x410),要么
    • 填满对应的 tcache bin(7 个条目),使更多的 free 到达全局 bins,或者
    • 如果环境可控,禁用 tcache(例如 GLIBC_TUNABLES glibc.malloc.tcache_count=0)。
  • 对 unsorted list 的完整性检查:在下一次检查 unsorted bin 的分配路径上,glibc 会做检查(简化):
    • bck->fd == victimvictim->fd == unsorted_chunks(av);否则会以 malloc(): unsorted double linked list corrupted 终止。
    • 这意味着你目标的地址必须能容忍两次写入:第一次在 free 时写入 *(TARGET) = victim;随后在 chunk 被移除时,分配器会再次将 *(TARGET) = unsorted_chunks(av)(分配器会把 bck->fd 重写回 bin head)。选择那些单纯强制一个非零大值就有用的目标。
  • 现代利用中典型的稳定目标
    • 应用或全局状态中将“较大”值视作标志/限制的变量。
    • 间接原语(例如,为随后的 [fast bin attack](Fast Bin Attack) 做设置,或为之后的 write-what-where 做准备)。
    • 在新 glibc 上避免 __malloc_hook/__free_hook:它们在 2.34 中被移除。对于 ≥ 2.39 避免 global_max_fast(见上注)。
  • 关于近期 glibc 中的 global_max_fast
    • 在 glibc 2.39+ 中,global_max_fast 是一个 8 位全局变量。将堆指针写入其中以增大 fastbins 的经典技巧不再干净可行,并且可能会破坏相邻的分配器状态。优先使用其他策略。

最小利用流程(modern glibc)

目标:使用 unsorted‑bin 插入原语,实现一次将 heap 指针写入任意地址的任意写,且不崩溃。

  • 布局/预处理
    • 分配 A、B、C,大小足够大以绕过 tcache(例如 0x5000)。C 用以防止与 top chunk 合并。
  • 损坏
    • 从 A 溢出到 B 的 chunk header,设置 B->bk = (mchunkptr)(TARGET - 0x10)
  • 触发
    • free(B)。在插入时分配器执行 bck->fd = B,因此 *(TARGET) = B
  • 后续
    • 如果你打算继续分配且程序会使用 unsorted bin,预期分配器稍后会把 *(TARGET) = unsorted_chunks(av)。这两个值通常都很大,在仅对“大”值做检查的目标中可能足以改变大小/限制语义。

Pseudocode skeleton:

c
// 64-bit glibc 2.35–2.38 style layout (tcache bypass via large sizes)
void *A = malloc(0x5000);
void *B = malloc(0x5000);
void *C = malloc(0x5000); // guard

// overflow from A into B’s metadata (prev_size/size/.../bk). You must control B->bk.
*(size_t *)((char*)B - 0x8) = (size_t)(TARGET - 0x10); // write fake bk

free(B); // triggers *(TARGET) = B (unsorted-bin insertion write)

note

• 如果你无法通过大小绕过 tcache,在释放被破坏的 chunk 之前,先把所选大小的 tcache bin 填满(7 次 free),这样 free 会进入 unsorted。
• 如果程序因为 unsorted-bin 的检查在下一次分配时立即 abort,请重新确认 victim->fd 仍然等于 bin head,并且在第一次写入后你的 TARGET 保存的是精确的 victim 指针。

Unsorted Bin Infoleak Attack

这其实是一个非常基本的概念。unsorted bin 中的 chunks 会包含指针。unsorted bin 中的第一个 chunk 的 fdbk 链接实际上会指向 main arena (Glibc) 的一部分。
因此,如果你能 put a chunk inside a unsorted bin and read it(use after free),或者 allocate it again without overwriting at least 1 of the pointers 然后 read 它,你就能获得一个 Glibc info leak

A similar attack used in this writeup,是滥用一个 4 个 chunks 的结构(A、B、C 和 D — D 只是用来防止与 top chunk 合并),通过在 B 中的 1 字节 null overflow 使 C 显示 B 为未使用。此外,在 B 中修改了 prev_size 数据,使得其大小不再是 B 的大小,而是 A+B。
然后释放了 C,并与 A+B 合并(但 B 仍然被使用)。分配了一个大小为 A 的新 chunk,随后把 libc 地址写入到 B,从 B 中 leak 出来。

参考与其他示例

  • https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#hitcon-training-lab14-magic-heap
    目标是用一个大于 4869 的值覆盖一个全局变量,从而获取 flag,并且 PIE 未启用。
  • 可以生成任意大小的 chunks,并且存在一个具有所需大小的 heap overflow。
  • 攻击开始时创建 3 个 chunks:chunk0 用于滥用 overflow,chunk1 将被 overflow,chunk2 用来阻止 top chunk 合并前面的 chunks。
  • 然后释放 chunk1 并对 chunk0 进行 overflow,使得 chunk1 的 bk 指向:bk = magic - 0x10
  • 随后,以与 chunk1 相同的大小分配 chunk3,这将触发 unsorted bin attack 并修改该全局变量的值,从而有可能拿到 flag。
  • https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html
    merge function 存在漏洞,因为如果传入的两个索引相同,它会对该索引执行 realloc 然后 free,但返回的却是可以继续使用的指向已 free 区域的指针。
    因此,2 chunks are createdchunk0 将与自身合并,chunk1 用于防止与 top chunk 合并。然后对 merge function 使用 chunk0 两次,这会导致 use after free。
    随后,对索引 2 调用 view 函数(即 use after free 的 chunk 的索引),这将 leak a libc address
    由于该二进制有保护,只允许 malloc 大于 global_max_fast 的大小,所以不会使用 fastbin,因此采用 unsorted bin attack 来覆盖全局变量 global_max_fast
    然后,可以用索引 2(use after free 指针)调用 edit function 并将 bk 指针覆盖成指向 p64(global_max_fast-0x10)。随后创建新 chunk 时会使用之前被妥协的 free 地址(0x20),这将触发 unsorted bin attack,覆盖 global_max_fast 为一个很大的值,从而允许在 fast bins 中创建 chunks。
  • 之后执行一个 fast bin attack
    • 首先发现可以在 __free_hook 位置处理大小为 200 的 fast chunks
      gef➤  p &__free_hook
      

$1 = (void (**)(void *, const void *)) 0x7ff1e9e607a8 <__free_hook> gef➤ x/60gx 0x7ff1e9e607a8 - 0x59 0x7ff1e9e6074f: 0x0000000000000000 0x0000000000000200 0x7ff1e9e6075f: 0x0000000000000000 0x0000000000000000 0x7ff1e9e6076f <list_all_lock+15>: 0x0000000000000000 0x0000000000000000 0x7ff1e9e6077f <_IO_stdfile_2_lock+15>: 0x0000000000000000 0x0000000000000000

  • 如果我们设法在该位置获得一个大小为 0x200 的 fast chunk,就可以覆盖一个将被执行的函数指针。
  • 为此,创建一个大小为 0xfc 的新 chunk,并对该指针调用 merge 函数两次,这样可以在 fast bin 中得到一个指向已 free 的、大小为 0xfc*2 = 0x1f8 的指针。
  • 然后对该 chunk 调用 edit function,将该 fast bin 的 fd 地址修改为指向先前的 __free_hook
  • 随后,创建一个大小为 0x1f8 的 chunk 从 fast bin 中取出之前的废弃 chunk,然后再创建另一个大小为 0x1f8 的 chunk,以在 __free_hook 处得到一个 fast bin chunk,并用 system 的地址覆盖它。
  • 最后,释放一个包含字符串 /bin/sh\x00 的 chunk(调用 delete 函数),触发指向 system 的 __free_hook,并以 /bin/sh\x00 作为参数执行。
  • CTF https://guyinatuxedo.github.io/33-custom_misc_heap/csaw19_traveller/index.html
    另一个例子,滥用 1 字节 overflow 在 unsorted bin 中合并 chunks 以获取 libc infoleak,然后执行 fast bin attack 覆盖 malloc hook 为 one gadget 地址
  • Robot Factory. BlackHat MEA CTF 2022
    • 只能分配大于 0x100 的 chunks。
    • 使用 Unsorted Bin attack 覆盖 global_max_fast(由于 ASLR,仅有 1/16 成功率,因为需要修改 12 位,但必须修改 16 位)。
    • Fast Bin attack 修改全局 chunks 数组。这提供了任意读写原语,从而可以修改 GOT 并将某个函数指向 system

参考资料

  • Glibc malloc unsorted-bin integrity checks (example in 2.33 source): https://elixir.bootlin.com/glibc/glibc-2.33/source/malloc/malloc.c
  • global_max_fast and related definitions in modern glibc (2.39): https://elixir.bootlin.com/glibc/glibc-2.39/source/malloc/malloc.c

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks