堆漏洞利用实验:从UAF到GOT劫持的完整实战

发布时间:2026/10/22 6:19:22
堆漏洞利用实验:从UAF到GOT劫持的完整实战
1. 堆漏洞利用实验的整体设计与思路拆解堆漏洞利用是二进制安全方向里绕不开的一道坎。很多人学完栈溢出之后觉得已经摸到了内存破坏的门槛结果一碰到堆就彻底懵了——栈上的返回地址清清楚楚摆在那里覆盖就完事了堆上却是一堆chunk在内存里你挨着我我挨着你free之后还带各种链表操作光是搞清楚“我到底覆盖了哪个结构的哪个字段”就得花上大半天。这个实验的核心目的就是把这层窗户纸捅破让你从“知道堆溢出这个概念”变成“能实际打出一条利用链”。1.1 为什么选择堆作为实验对象栈溢出的利用模型非常直观局部变量和返回地址在栈帧里是固定偏移溢出就是线性覆盖。但现实中的漏洞往往没这么友好。现代编译器默认开启栈保护之后栈溢出的门槛被大幅抬高而堆上的问题却依然大量存在——因为堆的管理逻辑复杂得多程序员在堆上犯错的机会也更多。堆漏洞利用之所以值得单独拿出来做一个实验原因有三点。第一堆的内存布局是动态的chunk的分配和释放顺序会直接影响内存中各个结构的相对位置这意味着利用方案必须考虑运行时状态不能像栈溢出那样写死偏移就完事。第二堆管理器本身维护着元数据比如chunk size、前后指针这些元数据一旦被破坏就能反过来被利用来实现任意地址写。第三堆漏洞的利用方式非常多样从最简单的unlink到复杂的House of系列每一种技巧背后都是对堆管理器行为的深度理解。这个实验通常会选用一个经典的堆漏洞场景比如UAFUse After Free、堆溢出覆盖相邻chunk元数据、或者double free。选这些场景不是因为它们简单而是因为它们足够典型——搞懂了这几个后面遇到更复杂的堆布局也能自己分析。1.2 实验环境的典型配置做堆漏洞利用实验环境配置不是随便装个系统就行的。堆管理器的行为跟glibc版本强相关不同版本之间的malloc/free实现差异可能直接导致同一种利用技巧在A版本能打通、在B版本就崩掉。常见的实验环境配置是这样的操作系统用某个主流Linux发行版的较老版本比如Ubuntu 16.04或18.04因为新版本glibc加入了很多缓解措施比如tcache的double free检测、safe-linking等会让入门实验变得过于复杂。glibc版本一般选2.23或2.27这两个版本既有tcache机制2.26引入又没有太严格的检查适合教学。编译选项方面实验通常会关闭PIE位置无关可执行文件、关闭栈保护Canary、关闭FORTIFY_SOURCE目的是把注意力集中在堆本身的漏洞上而不是被各种缓解措施分散精力。编译命令大致长这样gcc -o heap_lab heap_lab.c -m32 -fno-stack-protector -no-pie -z execstack这里用-m32编译成32位程序是有讲究的。32位程序的地址空间小指针只占4字节堆布局在调试器里看起来更紧凑手工计算偏移也更容易。64位虽然更贴近现代环境但地址随机化范围大、指针宽度翻倍初学阶段容易在细节上卡住。调试工具方面pwntools是标配用来写利用脚本gdb配合pwndbg或peda插件用来观察堆状态。pwndbg的heap命令、bins命令、vis_heap_chunks命令在分析堆布局时非常好用能直接把chunk的元数据和用户数据可视化出来。提示实验环境一定要和利用脚本中的地址假设保持一致。如果你在本地调试时用的是glibc 2.23换到另一台机器上跑的时候glibc版本变了很可能直接段错误。建议在实验开始前先用ldd --version确认一下。1.3 实验涉及的核心知识点这个实验表面上是在讲“怎么利用堆漏洞”实际上涉及的知识面相当宽。你需要理解堆管理器的基本数据结构chunk header、bins、top chunk需要知道malloc和free在不同情况下的行为差异需要会计算chunk之间的偏移还需要能读懂汇编层面的内存操作。具体来说实验会覆盖以下几个核心知识点chunk的结构每个已分配的chunk前面有一个8字节32位或16字节64位的header包含prev_size和size字段。size字段的低3位有特殊含义PREV_INUSE、IS_MMAPPED、NON_MAIN_ARENA这些标志位在利用中经常被用来绕过检查。bins的分类fastbin、smallbin、unsortedbin、largebin不同大小的chunk释放后进入不同的bin利用方式也不同。fastbin是单链表结构操作最简单也是入门实验最常用的。unlink操作当一个chunk被从bin链表中取出时会执行unlink操作涉及FD-bk BK和BK-fd FD这两次写操作。如果能控制FD和BK指针就能实现任意地址写。UAF的利用释放后的chunk如果指针没有被清空程序仍然可以通过旧指针读写这块内存。如果这块内存被重新分配给了另一个结构体就能通过旧指针篡改新结构体的内容。这些知识点单独拿出来都不算特别难但要在实际利用中把它们串起来需要对整个分配和释放流程有清晰的认识。1.4 实验的预期产出与评估标准一个完整的堆漏洞利用实验最终产出应该是一段能稳定拿到shell的利用脚本。评估标准不是“能打通就行”而是要看利用的稳定性和可复现性。具体来说脚本在实验环境中连续运行多次都能成功不依赖运气。利用过程中对堆布局的假设有明确的调试依据不是瞎试出来的。脚本中有清晰的注释说明每一步操作的目的和预期效果。能解释清楚为什么选择这种利用方式以及如果环境变化比如glibc版本升级需要做哪些调整。达到这个标准说明你不仅“会打这个题”而且真正理解了堆漏洞利用的内在逻辑。2. 堆管理器核心机制与利用原理解析要利用堆漏洞首先得搞清楚堆管理器在背后做了什么。很多人做实验的时候直接照着writeup抄脚本地址对不上就换个偏移继续试试通了就交差。这种做法在实验里可能能过但遇到新题目就完全没法下手。所以这一部分我们把堆管理器的核心机制拆开来讲重点讲清楚“为什么会有这个漏洞”以及“这个漏洞为什么能被利用”。2.1 chunk的内存布局与关键字段无论32位还是64位堆上的内存都是以chunk为单位管理的。一个已分配的chunk由两部分组成header和user data。header里最重要的是size字段它记录了整个chunk的大小包括header本身并且低3位被用作标志位。以32位为例一个chunk的header是8字节前4字节是prev_size只有当前一个chunk是free状态时才有意义后4字节是size。size字段的低3位分别是bit 0PREV_INUSE表示前一个chunk是否正在使用。如果前一个chunk被free了这个位会被清零同时当前chunk的prev_size字段会记录前一个chunk的大小。bit 1IS_MMAPPED表示这个chunk是否是通过mmap分配的。大块内存默认超过128KB会走mmap不放在主堆区。bit 2NON_MAIN_ARENA表示这个chunk是否属于非主线程的arena。这三个标志位在利用中非常关键。比如fastbin的利用中经常需要伪造size字段来绕过检查这时候就要注意低3位的设置。如果你伪造的size字段低3位不对malloc在后续分配时可能会触发assert或者直接崩溃。user data部分就是malloc返回给程序的指针指向的区域。注意malloc返回的指针指向的是user data的起始位置而不是chunk的起始位置。chunk的起始位置 malloc返回的指针 - 832位或 - 1664位。这个偏移在计算覆盖目标时经常用到。2.2 fastbin的分配与释放逻辑fastbin是堆管理器为小块内存32位下默认小于64字节64位下默认小于128字节准备的一条快速通道。它的结构是一个单链表数组每个bin维护一条链表链表中chunk的大小相同。fastbin的操作有几个特点需要特别注意释放时free一个fastbin大小的chunk时堆管理器不会立即合并相邻的空闲chunk而是直接把这个chunk插入到对应fastbin链表的头部。插入操作只修改chunk的fd指针bk指针不变。这意味着fastbin是一条单向链表只能从头到尾遍历。分配时malloc从fastbin取chunk时从链表头部取出第一个chunk同时把链表头指向该chunk的fd。这里有一个关键检查取出的chunk的size字段必须与bin的索引匹配。如果不匹配就会报错。double free检测在较新的glibc版本中free一个chunk时会检查该chunk是否已经是fastbin链表的头部。如果是就报double free错误。但这个检查只检查头部如果free的顺序是A-B-A就不会被检测到。这就是经典的fastbin double free利用手法。理解了这些逻辑就能明白为什么fastbin的利用通常围绕“控制fd指针”来做文章。如果你能覆盖一个已释放chunk的fd指针就能让下一次malloc返回一个你指定的地址。这个地址可以是任意可写内存比如GOT表、栈上的返回地址附近、或者某个函数指针。2.3 unsortedbin与unlink的前世今生当free的chunk大小超过fastbin范围时它会被放入unsortedbin。unsortedbin是一个双向链表chunk在其中的时候fd和bk指针分别指向前后相邻的chunk。当malloc在fastbin和smallbin中找不到合适chunk时会遍历unsortedbin把chunk整理到对应的bin中或者直接返回给调用者。unlink操作发生在chunk从bin链表中取出的时候。标准的unlink逻辑大致如下FD P-fd; BK P-bk; FD-bk BK; BK-fd FD;如果P是一个合法的空闲chunkFD和BK分别指向链表中前后相邻的chunk这两次写操作就是正常的链表维护。但如果P的fd和bk被攻击者控制了那么FD-bk BK就变成了“向FD1232位的位置写入BK的值”BK-fd FD变成了“向BK8的位置写入FD的值”。这就是unlink攻击的核心——通过控制fd和bk实现任意地址写。当然现代glibc对unlink加了检查FD-bk P和BK-fd P必须成立。这意味着你不能随便指定fd和bk必须让它们指向的chunk的bk/fd指针恰好回指P。这个检查在早期版本中是可以绕过的比如通过精心构造一个假的chunk让它的fd和bk满足条件。但在较新版本中绕过难度大幅增加。2.4 UAF漏洞的成因与利用窗口UAFUse After Free是堆漏洞中最常见也最容易理解的一种。它的成因很简单程序free了一块内存但没有把指向这块内存的指针清空。后续程序如果通过这个旧指针读写内存实际上操作的就是一块已经被释放、可能已经被重新分配的内存。UAF的利用窗口在于free之后、重新分配之前这块内存的内容可能还保留着原来的数据尤其是fastbin中的chunkfree时只修改fd指针user data部分基本不变。如果程序在free之后还能通过旧指针读取数据就可能泄露堆地址或libc地址。如果程序在free之后还能通过旧指针写入数据就可能覆盖下一个被分配到这个位置的chunk的内容。一个典型的UAF利用场景是这样的程序有一个结构体数组每个元素包含一个函数指针和一些数据。程序free了其中一个结构体但没有清空数组中的指针。然后程序分配了一个新的结构体恰好复用了刚才free的内存。此时数组中的旧指针和新结构体的指针指向同一块内存。通过旧指针修改函数指针再触发调用就能劫持控制流。2.5 从漏洞到利用的完整思维链条把上面这些机制串起来一个完整的堆漏洞利用思维链条大致是这样的确定漏洞类型是溢出覆盖、UAF、double free还是其他分析堆布局程序在运行过程中会分配哪些chunk大小分别是多少分配和释放的顺序是什么寻找可控点漏洞能让你控制哪个chunk的哪个字段是fd指针、size字段还是user data确定利用目标你想覆盖什么是GOT表中的函数地址、栈上的返回地址还是某个结构体中的函数指针构造利用链通过一系列malloc和free操作把堆布局调整到你需要的状态然后触发漏洞实现目标地址的写入。获取shell把目标地址覆盖为system函数的地址或者构造ROP链最终执行shell。这个链条中的每一步都需要调试来验证。堆的状态是动态变化的你在纸上推演出来的布局实际跑起来可能因为对齐、tcache、或者程序自身的其他分配操作而完全不同。所以做堆实验gdb断点调试是必不可少的环节。3. 实操过程与核心环节实现理论讲完了接下来进入实操环节。这一部分我会以一个典型的堆漏洞实验场景为例完整走一遍从分析程序到写出利用脚本的过程。为了让内容更具参考性我会把调试过程中的关键观察和计算步骤都写出来。3.1 程序分析与漏洞定位拿到一个堆实验的二进制文件第一步不是急着跑而是先做静态分析。用checksec看一下保护机制checksec ./heap_lab假设输出显示Arch是i386-32-littleRELRO是PartialCanary是NoNX是disabledPIE是No。这意味着没有栈保护、堆栈可执行、地址固定利用条件非常宽松。然后用IDA或者objdump反汇编重点看几个地方main函数的菜单逻辑、malloc和free的调用点、以及有没有明显的UAF或溢出。假设程序是一个经典的“笔记管理”程序支持创建笔记、编辑笔记、删除笔记、查看笔记。每个笔记是一个结构体包含一个content指针和size字段。在删除笔记的功能中发现代码大致是这样的void delete_note() { int idx; printf(Index: ); scanf(%d, idx); if (idx 0 || idx MAX_NOTES) return; if (notes[idx] NULL) return; free(notes[idx]-content); free(notes[idx]); // 注意这里没有把notes[idx]置为NULL }这就是一个典型的UAF漏洞。free了notes[idx]指向的结构体但没有清空notes[idx]指针。后续如果再次访问notes[idx]就是UAF。再看编辑笔记的功能void edit_note() { int idx; printf(Index: ); scanf(%d, idx); if (idx 0 || idx MAX_NOTES) return; if (notes[idx] NULL) return; printf(Content: ); read(0, notes[idx]-content, notes[idx]-size); }编辑功能通过notes[idx]-content指针写入数据写入长度由notes[idx]-size决定。如果notes[idx]已经被free但指针没清空那么notes[idx]-content和notes[idx]-size读的就是已经被释放的内存。如果这块内存被重新分配给了另一个结构体就能通过编辑功能篡改那个结构体的内容。3.2 堆布局调试与关键偏移计算静态分析只能看出漏洞的大致位置具体的堆布局必须通过动态调试来确定。在gdb中加载程序在malloc和free调用后下断点观察每次分配和释放后堆的状态。假设程序启动后先创建了两个笔记笔记结构体大小是16字节content指针4字节 size字段4字节 其他字段8字节content缓冲区大小是32字节。在32位系统中malloc(16)实际分配的chunk大小是24字节16字节user data 8字节header对齐到16字节边界后是32字节这里需要实际调试确认。用pwndbg的vis_heap_chunks命令可以直观地看到堆上的chunk分布。假设观察到chunk A笔记0的结构体地址0x0804b1a0大小0x18chunk B笔记0的content地址0x0804b1c0大小0x28chunk C笔记1的结构体地址0x0804b1f0大小0x18chunk D笔记1的content地址0x0804b210大小0x28现在删除笔记0。free(notes[0]-content)释放chunk Bfree(notes[0])释放chunk A。由于这两个chunk的大小都在fastbin范围内32位下小于64字节它们会被放入对应的fastbin链表。fastbin是LIFO后进先出的所以chunk A会在链表头部chunk B在链表第二个。此时notes[0]指针仍然指向chunk A的user data区域。虽然chunk A已经被free但它的user data部分也就是notes[0]指向的内存中的content指针和size字段仍然保留着原来的值——content指向chunk Bsize是32。接下来创建一个新笔记假设新笔记的结构体大小也是16字节。malloc会从fastbin中取chunk由于chunk A在链表头部新笔记的结构体会被分配到chunk A的位置。此时notes[0]和新笔记的指针指向同一块内存。新笔记的content缓冲区会从fastbin中取chunk B的位置。所以新笔记的content指针指向chunk B而notes[0]-content也指向chunk B。这就是UAF的利用点通过编辑笔记0实际上是在编辑新笔记的content通过编辑新笔记实际上是在编辑笔记0的结构体。3.3 利用脚本编写与关键步骤注释理解了堆布局之后就可以写利用脚本了。利用思路大致如下创建笔记0和笔记1确保堆上有足够的chunk供后续操作。删除笔记0触发UAF。创建一个新笔记笔记2使其结构体复用笔记0的内存content复用笔记0的content内存。此时notes[0]和notes[2]指向同一块结构体内存。通过编辑笔记0可以修改笔记2的结构体内容包括content指针和size字段。把笔记2的content指针改为某个目标地址比如GOT表中free函数的地址把size改大一些。通过编辑笔记2向目标地址写入数据。比如把free的GOT表项改为system函数的地址。创建一个content为/bin/sh的笔记然后删除它。程序调用free(/bin/sh)实际上调用的是system(/bin/sh)拿到shell。用pwntools写脚本的话大致框架如下from pwn import * p process(./heap_lab) elf ELF(./heap_lab) libc ELF(/lib/i386-linux-gnu/libc.so.6) def create(size, content): p.sendlineafter(bChoice: , b1) p.sendlineafter(bSize: , str(size).encode()) p.sendlineafter(bContent: , content) def delete(idx): p.sendlineafter(bChoice: , b2) p.sendlineafter(bIndex: , str(idx).encode()) def edit(idx, content): p.sendlineafter(bChoice: , b3) p.sendlineafter(bIndex: , str(idx).encode()) p.sendlineafter(bContent: , content) def show(idx): p.sendlineafter(bChoice: , b4) p.sendlineafter(bIndex: , str(idx).encode()) return p.recvline() # 创建笔记0和笔记1 create(32, bAAAA) create(32, bBBBB) # 删除笔记0触发UAF delete(0) # 创建笔记2复用笔记0的内存 create(32, bCCCC) # 通过编辑笔记0来修改笔记2的结构体 # 假设笔记2的结构体布局是content指针(4字节) size(4字节) # 把content指针改为free的GOT地址 free_got elf.got[free] system_plt elf.plt[system] # 注意这里需要根据实际结构体布局调整payload payload p32(free_got) p32(0x100) edit(0, payload) # 通过编辑笔记2把free的GOT改为system edit(2, p32(system_plt)) # 创建content为/bin/sh的笔记然后删除 create(32, b/bin/sh\x00) delete(3) p.interactive()这个脚本只是一个框架实际运行时需要根据调试结果调整偏移和地址。比如笔记结构体的布局可能不是简单的content指针size可能还有id字段、flag字段等。GOT地址和PLT地址也需要从实际的ELF文件中获取。3.4 调试过程中的关键观察点写利用脚本的过程中有几个关键观察点需要特别注意chunk大小的对齐32位系统中chunk大小必须是8的倍数且最小为16字节header 8字节 user data至少8字节。malloc(16)实际分配的chunk大小是24字节但会对齐到32字节。这个对齐规则会影响fastbin的索引进而影响chunk被分配到哪个bin。fastbin的LIFO顺序free的顺序决定了fastbin链表中的顺序。先free的chunk在链表尾部后free的在头部。malloc时从头部取所以后free的chunk会先被分配。这个顺序在构造利用链时非常关键。结构体字段的偏移笔记结构体中content指针和size字段的偏移需要从反汇编中确认。有时候编译器会插入padding或者字段顺序与源码中不同。用gdb在malloc返回后查看内存可以确认实际的布局。GOT表的可写性Partial RELRO意味着GOT表可写这是利用GOT劫持的前提。如果Full RELROGOT表在程序启动后就变成只读就不能用这种方法了。注意在调试UAF的时候free之后不要急着继续运行程序。先在gdb中查看被free的chunk的内容确认哪些字段被修改了、哪些还保留着。fastbin中的chunk只有fd指针会被修改user data部分基本不变。这个特性在利用中经常被用到。4. 常见问题与排查技巧实录堆漏洞利用的实验过程中几乎每个人都会遇到各种奇怪的崩溃和不符合预期的行为。这一部分我整理了一些典型问题和排查思路都是实际调试中踩过的坑。4.1 常见崩溃原因速查表崩溃现象可能原因排查方法malloc报错“invalid size”伪造的size字段不合法太小、太大、低3位标志位错误检查size字段是否满足对齐要求PREV_INUSE位是否正确free报错“double free detected”free了同一个chunk两次或者fastbin链表头部就是该chunk检查free的顺序确认没有连续free同一个指针unlink报错“corrupted double-linked list”fd和bk指针不满足FD-bk P和BK-fd P检查伪造的chunk的fd和bk是否指向正确的回指位置段错误在malloc内部fastbin链表被破坏malloc取chunk时访问了非法地址用gdb查看fastbin链表确认每个chunk的fd指针是否合法段错误在free内部chunk的size字段被覆盖free时计算下一个chunk的位置出错检查被free的chunk的size字段是否被意外修改system函数执行后没有shellsystem的参数不是有效的命令字符串或者GOT劫持的目标不对确认写入GOT的地址是否正确参数字符串是否以\x00结尾这个表格里的问题我几乎都遇到过。最让人头疼的是“段错误在malloc内部”因为崩溃点不在你的代码里而是在库函数里堆栈信息看起来跟你的程序没什么关系。这时候需要用gdb查看fastbin链表的状态看看是不是某个chunk的fd指针被覆盖成了非法地址。4.2 堆布局不符合预期时的排查思路堆布局不符合预期是另一个高频问题。你在纸上推演的时候觉得chunk应该按A-B-C-D的顺序排列实际跑起来却发现顺序完全乱了。这种情况通常有几个原因程序自身的额外分配很多程序在启动时会调用printf、scanf等函数这些函数内部可能会malloc缓冲区。这些额外的chunk会占用堆空间打乱你预期的布局。解决办法是在程序启动后、进行任何操作之前先在gdb中查看堆的初始状态把已有的chunk都记录下来。tcache的影响glibc 2.26之后引入了tcache机制free的chunk会优先进入tcache而不是fastbin。tcache是每个线程一个的缓存每个大小最多缓存7个chunk。如果你的实验环境glibc版本较新free的chunk可能直接进了tcache根本不会进入fastbin。这时候需要先把tcache填满才能让后续的free进入fastbin。chunk合并如果free的chunk不是fastbin大小且相邻的chunk也是free状态堆管理器会触发合并操作。合并后的chunk会进入unsortedbin大小变成两个chunk之和。这会导致后续malloc时分配到的chunk大小和位置都发生变化。排查堆布局问题最有效的方法是在每次malloc和free之后都用gdb的vis_heap_chunks命令查看堆的状态把每次操作后的chunk分布都记录下来。对比预期和实际就能找到是哪一步出了问题。4.3 利用脚本不稳定时的调整策略有时候利用脚本能打通但多跑几次就失败了。这种不稳定性通常来自以下几个方面地址随机化虽然实验环境通常关闭了PIE但堆的起始地址可能仍然有随机化brk随机化。如果脚本中硬编码了堆地址换一次运行就可能失败。解决办法是通过泄露或者相对偏移来计算地址而不是写死绝对值。堆风水不稳定如果利用依赖于特定的堆布局而程序中有一些不确定的分配操作比如根据输入长度决定分配大小那么堆布局可能会随输入变化。解决办法是尽量让输入长度固定避免触发不确定的分配。fastbin链表状态不确定如果程序在利用之前已经进行了一些free操作fastbin链表的状态可能不是你预期的。解决办法是在利用开始前先把fastbin清空通过malloc把里面的chunk都取出来或者通过show功能泄露fastbin的状态。提示写利用脚本的时候尽量把每一步的堆状态都打印出来。比如在每次malloc之后打印一下返回的指针在每次free之后打印一下fastbin链表的内容。这样即使脚本失败了也能快速定位是哪一步的堆状态不对。4.4 从实验到实战的经验迁移实验环境里的堆漏洞利用和真实场景有很大的差距。实验里关闭了各种保护堆布局简单利用目标明确。真实场景中你面对的是一个开启了Full RELRO、PIE、Canary、并且glibc版本很新的二进制文件堆布局复杂还可能有多线程竞争。但实验的价值在于帮你建立对堆管理器的直觉。当你在实验里反复调试过fastbin的LIFO顺序、unlink的检查条件、UAF的利用窗口之后面对真实场景时你至少知道该从哪里入手分析。你会知道先看glibc版本再确定哪些利用技巧可用你会知道先用gdb把堆布局摸清楚再动手写脚本你会知道崩溃的时候先检查fastbin链表而不是盲目改偏移。我个人在实际操作中的体会是堆漏洞利用最难的从来不是写脚本而是理解程序在运行时的内存状态。静态分析能告诉你漏洞在哪里但只有动态调试才能告诉你漏洞能怎么用。每次做堆实验我都会花至少一半的时间在gdb里观察堆的状态把每个chunk的地址、大小、fd/bk指针都记录下来。这些记录看起来枯燥但它们是写出稳定利用脚本的基础。最后再分享一个小技巧如果你在调试时发现堆布局总是和你预期的不一样可以试试在程序启动后先执行一次“分配-释放-再分配”的循环把堆的初始状态“熨平”。这样后续的分配和释放会更有规律更容易预测。这个技巧在真实场景中同样适用很多CTF选手在打堆题的时候都会先做一轮堆风水把堆整理成自己需要的状态然后再触发漏洞。