想象你在一家繁忙的图书馆前台工作。桌面上贴着一沓便利贴:每接到一个任务,你就抽出一张,写下“办完回哪儿”(返回地址)和随手笔记(局部变量),压在桌面最上面;任务办完,撕掉这张,继续原来的活。这沓永远从顶上压、从顶上撕的便利贴,就是程序的“栈”,每张是一帧“栈帧”。麻烦在于桌面面积有限:如果有人让你写一张写不下的字条,或者同一个问题追问上百遍,便利贴就会堆出桌面,字迹漫到桌面其他区域——把你“办完回哪儿”的备注改掉。这就是栈溢出:未检查的越界写像洪水漫堤,破坏了不该碰的内存。好在图书馆有一套安保:管理员在每张便利贴下压一只小玩具金丝雀,字一漫过它就碎掉报警;桌面边缘贴着红胶带警戒区,一触即响;每天换班随机挪动桌子;前台还规定便利贴只能记录、绝不能当指令宣读。这篇,我们把这套“栈安全体系”讲透。
理解栈溢出与它的多层防护,是系统程序员、编译器开发者和安全工程师的必修课——它帮你看懂崩溃、写对加固,在漏洞报告中一眼识破攻击者的手法。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
栈溢出成因
图书馆前台的桌面并不是无底洞——操作系统只给这沓便利贴划了一块固定大小的桌面,也就是“栈”,默认配额通常只有几兆字节。你可以往上摞便利贴,但摞到桌面边缘就是极限。这一节我们来看看:便利贴到底是怎么“堆出桌面”的。
第一种堆法叫递归过深。阿栈遇到一个不放心的问题就问自己一遍,每问一次就压一张便利贴,问了一百遍就压了一百张——每张都是“我该怎么办”的重复记录。第二种堆法叫局部变量过大:一张便利贴上非要写几千行笔记,单张就占了半张桌面。在内存里,每次函数调用压入的“便利贴”就是栈帧,它包含返回地址、保存的基址指针以及局部变量;单帧越大、帧数越多,总占用就越接近栈上限。
到这里要特别注意:栈溢出的真正危险不是“空间不够用”本身,而是“写出去”这个动作——新的字迹落到了桌面之外,那里躺着堆数据、其他线程的栈,甚至磁盘缓存。便利贴堆出桌面只是事故,越界写才是漏洞。
- 递归深度 \(d\)每次函数调用压入一帧栈帧,\(d\) 表示当前已压入的帧数。
- 单帧大小 \(s\)一帧包含返回地址、保存的寄存器与局部变量,是每次调用的固定开销。
- 总需求 \(d \times s\)所有已压入帧占用的栈空间总和,随深度线性增长。
- 与上限 \(M\) 比较操作系统给栈设定的配额,需求一旦超过,新帧就会写进栈外内存。
缓冲区溢出攻击
这天,一个“热心读者”递来一张写满字的字条,比前台的标准便利贴大好几倍。阿栈的习惯是照抄,于是字迹从便利贴边缘漫出去,一路漫过了桌面。等抄完,她发现自己“办完回哪儿”的备注被人改成了“请走到仓库最里面”。这就是一场精心策划的缓冲区溢出攻击。
在 C 语言里,strcpy、gets、sprintf 这类函数从不检查目标数组的容量。攻击者传入超长字符串,数据就顺着栈帧从低地址往高地址写,越过局部缓冲区,依次淹没金丝雀槽和返回地址。只要把返回地址精确改成攻击者预先布置的恶意代码(shellcode)地址,函数一执行 ret,CPU 就跳到那里——程序的控制流被完全劫持。
回到字条的故事:那张字条不是胡乱写满的,而是被精心排版——前面 \(N\) 个字填满缓冲区,接着用无用字符趟过金丝雀槽,最后在偏移 \(o\) 处稳稳写上“请走到仓库最里面”。攻击者要做的,就是让阿栈在撕便利贴的那一刻,走错方向。
- 写入长度 \(L\)攻击者传入的数据总长度,比如一条超长的 URL。
- 缓冲区容量 \(N\)局部数组声明的大小,例如
char buf[16]。 - 偏移 \(o\)从缓冲区起点到返回地址槽的距离,中间隔着金丝雀等数据。
- 判定条件溢出量达到或超过偏移,返回地址就会被改写。
金丝雀 Canary
管理员决定给每张便利贴加一道机关:在“办完回哪儿”那行字下面,压一只小玩具金丝雀。任何人写字漫过小鸟,小鸟就会碎成粉末;撕便利贴继续干活之前,管理员会先低头检查——小鸟还在,才允许走。这就是栈金丝雀(Stack Canary),也叫栈探测。它不需要阻止别人乱写,只需要在“写坏返回地址之前”先被发现。
在编译层面,GCC 的 -fstack-protector 系列选项会在函数序言(prologue)中,从线程本地存储(FS:0x28 指向的位置)取出一个随机字节串作为金丝雀,写进局部变量与返回地址之间的“金丝雀槽”;在函数尾声(epilogue),重新读回槽中值与原始值比对。如果不一致,说明有人越界写触碰了金丝雀,直接调用 __stack_chk_fail 终止进程。金丝雀值常包含 \0 字节,这让字符串拷贝类攻击更难完整地把它原样抄过去。
注意编译器的插入策略有讲究:-fstack-protector 只保护含大缓冲区(一般 ≥ 8 字节)的函数;-fstack-protector-strong 扩大到所有含局部数组或对局部变量取地址的函数;-fstack-protector-all 则保护所有函数。一层比一层全面,也一层比一层慢。小鸟不是守门员——它是个“碎掉就会响”的警报器,拦在攻击者改写返回地址之前,让攻击在起跳的前一秒被截胡。
- 生成随机原值进程启动时从线程本地存储读取随机哨兵值,每个进程都不同。
- 序言写入函数入口把原值复制到金丝雀槽——缓冲区与返回地址之间。
- 尾声校验函数返回前重新读取槽中值,与原始值比对。
- 失败即终止一旦不等,调用
__stack_chk_fail打印告警并中止进程。
栈保护页守护
金丝雀防的是“字从便利贴里漫出来”,可如果整沓便利贴堆到塌方、直接滚出桌面边缘呢?管理员又在桌面最外沿贴了一圈红胶带警戒区:这圈区域不允许任何便利贴触碰,一触即响,整桌停止服务。这就是栈保护页(Guard Page,即栈守护页)。
保护页是映射在栈低地址端(栈顶方向)的一页或几页特殊内存:它没有对应的物理页,任何访存落到上面都会立刻触发缺页异常,内核随即发送 SIGSEGV 终止进程。递归过深、单帧过大导致栈指针一路向低地址增长,只要越过了合法区域的边界,下一次压栈就会撞上红胶带。它是对“栈耗尽型溢出”的兜底,与金丝雀针对“帧内缓冲区越界写”形成互补。内核线程默认栈只有 8–16KB,vmap_stack 机制会在每个内核栈后面强制放置保护页,用户态 pthread 创建线程时也能配置 guard 大小。
红胶带的价值不是抓小偷,而是止损:一旦塌方,立刻响铃清场,绝不让便利贴塔一路塌进旁边的堆内存或别的线程的栈里。它与金丝雀互相帮不上忙,却把“从中间漫出来”和“从边缘塌下去”两条路都堵住了。
- 栈地址增长每次调用或分配大局部量,栈指针向低地址移动,产生新的访存地址。
- 命中保护页保护页是没有物理内存映射的“禁地”,恰好位于栈合法区尽头。
- 硬件页错误CPU 发现该页不可访问,触发缺页异常并交给内核处理。
- 进程终止内核识别为栈越界访问,发送 SIGSEGV,进程被彻底杀死。
ASLR 随机化
攻击者为什么总能精准命中返回地址的偏移?因为他们提前画好了“桌面地图”——程序每次启动,栈的位置都不变,返回地址、shellcode 地址、库函数地址都是固定值。于是图书馆改了规矩:每天换班前,整张桌面连同书架随机平移一段距离。你今天画好的地图,明天就作废了。这就是地址空间布局随机化(ASLR)。
操作系统内核在每次 exec 加载程序时,为栈顶、堆、共享库和 mmap 区域注入随机偏移。栈地址的随机范围通常有几十位熵,攻击者盲猜地址的成功概率呈指数下降。注意一个关键配套:如果程序自身不是 PIE(位置无关可执行程序),它的代码段地址依然固定,攻击者可以借用程序内部的固定代码片段进行攻击,所以现代加固要求 -fPIE -pie 把程序本体也纳入随机化。
ASLR 让攻击者最头疼的“瞄准”环节变得不可靠。但它不是加密,没有秘密性——如果程序存在信息泄露漏洞,地址还是会被读走。它真正的作用,是把一次“随手可打的靶子”变成“需要额外信息才能命中的移动靶”。
- 熵值 entropy每次启动进程时内核掷出的随机数,栈通常有几十位熵。
- 对齐粒度 align地址按页对齐,低 12 位固定为 0,避免未对齐地址。
- 随机基址 base栈、堆、共享库各自加上随机偏移,构成当次运行的布局。
- 猜中概率攻击者盲猜地址成功的概率是 2 的负熵次方。
NX 不可执行
前台还有一条铁律:便利贴是“记录用”的,无论上面写了什么,都只能当笔记看,绝不能当成命令宣读——哪怕字条上写着“请把你钱包交给我”,大家也只能把它当作一行文字,而不是一个可以执行的指令。这就是 NX(No-eXecute)不可执行保护的思路。
在 x86 上它叫 XD 位,在 ARM 上叫 XN 位,本质是页表项中的一个权限位:标记该页“数据页,禁止取指”。CPU 一旦发现指令指针落在一个 NX 页上,立刻抛出异常。栈和堆默认都带 NX 标记,于是经典攻击剧情——把 shellcode 写到栈上、再用返回地址跳过去执行——直接被 CPU 一票否决,数据永远是数据。
不过攻击者并没有就此收手——他们发明了 ROP(返回导向编程):不注入新代码,而是把返回地址链改成程序自身和 libc 里已有的指令片段(gadget)的地址,一帧帧跳转,拼出攻击逻辑。这就像攻击者不宣读便利贴,而是借图书馆里“本来就有的操作手册”一页页拼出自己要的动作。所以 NX 必须与 ASLR 并肩作战:NX 让新代码无处安放,ASLR 让旧代码的地址难以寻找。
- 页表项权限位每个内存页在页表中有读、写、执行、NX 等权限位。
- 栈与堆打标操作系统默认把数据页的 NX 位置 1,表示“仅供参考,不得执行”。
- CPU 拒绝取指指令指针落到 NX 页时,CPU 直接抛出异常。
- shellcode 失效注入的数据只能当数据躺着,永远无法变成可运行代码。
纵深防御协同
回看整座图书馆的安保体系:门卫查包是金丝雀,桌面红胶带是保护页,换班挪桌是 ASLR,便利贴禁读是 NX——再加上监控、保险柜和权限登记。单独任何一层,都能被有备而来的高手绕过:金丝雀能被信息泄露读出,保护页只防栈尾不防帧内,ASLR 地址能被泄露,NX 能被 ROP 绕过。但真正的安全,来自它们叠加在一起。
纵深防御(Defense in Depth)的理念是:检测型防护(Canary、保护页)负责“报警止损”,缓解型防护(ASLR、NX、PIE、RELRO)负责“提高利用难度”。一次成功的攻击必须完成整套动作:找到越界写漏洞 → 摸清金丝雀 → 确认返回地址偏移 → 搞到 ASLR 后的地址 → 找到可用的 gadget。每一层都是独立关卡,攻击者少过一关就前功尽弃。
举个数字的例子:假设四道防线每道被绕过的概率都是 10%,整体攻破概率就是 \(0.1^4 = 0.0001\),万分之一。攻击者不仅要懂漏洞,还得凑齐一连串运气和信息。没有银弹——安全不是靠一面完美的盾,而是让攻击者的剧本每一页都被划掉一行,直到他写不下去。
- 单层绕过概率 \(p_i\)每道防线各自存在被绕过的概率,比如猜中金丝雀、猜中 ASLR 地址。
- 各层连乘完成整条攻击链必须逐层通过,总概率是各层概率的乘积。
- 乘积衰减每一层都小于 1,层数越多总概率下降得越快。
- 攻击成本上升攻击者必须为每层准备一个信息泄露原语,成本随层数线性增长。
AI 应用场景
漏洞崩溃自动分流:从海量崩溃里挑出真漏洞
阶段:漏洞分析与可利用性评估(静态/动态分析)
解决的问题:模糊测试(fuzzing)每天抛出成千上万份崩溃样本,人工判断“这次崩溃是金丝雀检测触发的主动终止,还是真正可控的栈越界写”极其耗时。AI 崩溃分类器根据崩溃指令、栈回溯和寄存器状态,自动区分 __stack_chk_fail、SIGSEGV 与真正的劫持点,把“金丝雀碎掉”这类的防护止损样本批量剔除,安全研究员只需处理可被利用的少数样本。
具体案例:某 Linux 发行版安全团队用 AI 对 10 万份 fuzzer 崩溃样本聚类,把“金丝雀被压碎导致 abort”的样本自动标注为不可利用,召回率达到 92%,漏洞分流耗时缩短 70%。
编译期加固:让 CI 流水线自带金丝雀
阶段:编译期防护与构建流水线(DevSecOps)
解决的问题:许多团队不清楚该开哪些编译防护选项,代码裸奔上线。AI 静态扫描在代码入库前识别高风险模式——大局部数组、无界 strcpy、深递归——并自动建议在构建命令中加入 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie -z noexecstack,把防线前移到编译期,从源头给每张“便利贴”装上金丝雀。
具体案例:某金融支付中间件项目接入 AI 扫描后,47 处递归函数被标记为高风险并改为迭代实现,CI 门禁强制开启 stack-protector 编译,上线一年内栈相关 CVE 为零。
内核崩溃诊断:一页页翻 panic 转储找元凶
阶段:运行时防护与崩溃诊断
解决的问题:内核线程栈只有 8–16KB,驱动里一条递归路径很容易“撞桌沿”触发保护页。传统做法是人工翻 panic 转储,AI 能自动解析栈回溯,区分“保护页缺页异常”与“金丝雀校验失败”两类故障,并定位到触发调用的最深函数链。
具体案例:某云厂商内核团队用 AI 分析 panic 日志,自动定位到 NFS 驱动的第 9 层递归导致守护页触发,修复后相关故障率下降 60%。
Python 演示
# -*- coding: utf-8 -*-"""栈溢出与金丝雀(Canary)保护的迷你模拟器""" import random class StackFrame: """一帧栈帧: 缓冲区在前, 金丝雀居中, 返回地址在后 (低地址 -> 高地址)""" def __init__(self, name, has_canary=True): self.name = name self.buf_size = 8 self.canary_size = 4 self.buffer = bytearray(self.buf_size) if has_canary: self.canary = random.getrandbits(32).to_bytes(4, 'little') else: self.canary = None self.has_canary = has_canary self.ret_addr = 0x12340000 def write(self, data): """把 data 写入缓冲区, 返回(越过缓冲区? 压碎金丝雀? 改写返回地址?)""" payload = bytearray(data) overrun = False canary_broken = False hijacked = False for i, byte in enumerate(payload): if i < self.buf_size: self.buffer[i] = byte elif i < self.buf_size + self.canary_size: overrun = True if self.has_canary: self.canary = bytearray(self.canary_size) # 金丝雀被打碎 canary_broken = True else: overrun = True self.ret_addr = 0xdeadbeef hijacked = True return overrun, canary_broken, hijacked def check_canary(self): """函数返回前校验金丝雀是否原封未动""" if self.has_canary and self.canary == bytearray(self.canary_size): raise RuntimeError('[' + self.name + '] 金丝雀被压碎! 检测到栈溢出, abort') return True class MiniStack: """模拟栈: 深度受限, 尾部就是'保护页'警戒线""" def __init__(self, limit=6): self.limit = limit self.frames = [] def call(self, name, has_canary=True): if len(self.frames) >= self.limit: raise RuntimeError('栈溢出(guard page): 便利贴堆出桌面边缘, 触及警戒区!') frame = StackFrame(name, has_canary) self.frames.append(frame) return frame def ret(self): frame = self.frames.pop() frame.check_canary() return frame def demo_overflow(use_canary): stack = MiniStack(limit=6) vuln = stack.call('vuln', use_canary) overrun, canary_broken, hijacked = vuln.write(b'A' * 4 + b'B' * 12) print('越界写=%s, 金丝雀碎=%s, 返回地址被改=%s' % (overrun, canary_broken, hijacked)) stack.ret() if hijacked: print('警告: 返回地址已被篡改, 控制流被劫持!') else: print('金丝雀完好, 正常返回') def demo_recursion(): stack = MiniStack(limit=5) depth = 0 try: while True: stack.call('func%d' % depth, has_canary=False) depth += 1 except RuntimeError as err: print('递归到第 %d 层: %s' % (depth, err)) if __name__ == '__main__': for flag in (True, False): try: demo_overflow(flag) except RuntimeError as err: print('防护生效(use_canary=%s): %s' % (flag, err)) print('-' * 20) demo_recursion()
self.canary = random.getrandbits(32).to_bytes(4, 'little')每个进程一个随机金丝雀值,模拟编译器从 FS:0x28 取随机哨兵。for i, byte in enumerate(payload):逐字节写入,模拟不检查长度的strcpy/gets。elif i < self.buf_size + self.canary_size:写入位置越过缓冲区,进入金丝雀槽,越界已经开始。self.canary = bytearray(self.canary_size)金丝雀被清零压碎,等于“小鸟碎了”。self.ret_addr = 0xdeadbeef溢出更深一档,直接改写返回地址。if len(self.frames) >= self.limit:模拟栈上限/保护页:帧数达到配额即报栈溢出。frame.check_canary()函数返回前的金丝雀校验,对应编译器的 epilogue 检查。
常见误区
误区递归写多了程序报 "stack overflow",不就是内存不够用嘛,跟磁盘满了差不多,重启一下就好了。
正解危险的是“越界写”这个动作——数据写入超过缓冲区边界,把相邻的返回地址、局部变量覆盖成攻击者指定的值。它不是被动耗尽,而是主动破坏内存布局,是可供利用的内存破坏原语。空间不足只是表象,未检查的写入才是病灶。
为何常见:递归过深是新手最常见的触发方式,让人第一眼看到的是“栈空间小”,忽略了更深层的“写入没有边界检查”。
误区编译时加上 `-fstack-protector` 就高枕无忧了,每个函数都有金丝雀,攻击者再也没法溢出栈。
正解金丝雀只防护“从低地址向高地址顺序覆盖返回地址”的经典溢出路径。攻击者可以先通过格式化字符串、越界读泄露金丝雀值,再原样写回绕过;也可以利用 C++ 虚表指针等“写哪里”型漏洞直接跳过金丝雀。且 -fstack-protector 只保护含大缓冲区的函数,小函数并不在保护范围内。
为何常见:安全宣传常说“canary 防栈溢出”,容易让人把一条防线误当成万能盾牌。
误区地址都随机了,攻击者找不到任何地址,程序就安全了。
正解ASLR 的随机熵有限(栈通常只有 16–32 位),且大量 fork 出的子进程共享同一份地址布局。攻击者可以通过堆泄露、/proc/self/maps、侧信道等方式读出真实地址,让随机化失去意义。ASLR 增加的是“攻击成本”,不是“绝对保密”。
为何常见:“随机化”听起来像加密,但随机化本身没有秘密性,它只是把一次瞄准变成了一场概率游戏。
误区栈不能执行了,攻击者写再多的 shellcode 也跑不起来,所以栈攻击已经被消灭了。
正解攻击者会改用 ROP/JOP 代码复用攻击——不注入新代码,而是从 libc 和程序自身已有的指令片段里拼出逻辑。真正压制 ROP 的是“金丝雀阻断裂片链 + ASLR 让 libc 地址不可知”的组合,单靠 NX 挡住的是最原始的攻击形态。
为何常见:NX 的防御效果立竿见影,让人低估了攻击技术的迭代速度。
误区栈末尾既然有保护页,任何越界写都会撞上它报错,那还要金丝雀干什么?
正解保护页只挡“栈耗尽型”溢出——栈指针一路生长到低地址尽头,访问到不可映射页。而缓冲区越界写发生在栈中间的已分配帧内,可能直接覆盖返回地址,根本碰不到栈尾的保护页。金丝雀才是针对帧内越界写的检测,两者分工不同、互相补充。
为何常见:都叫“栈保护”,很容易把“栈尾守护页”和“帧内金丝雀”两种机制混为一谈。
面试题
Q1. 栈溢出的本质是什么?
- A.程序运行时内存条损坏
- B.未检查边界的越界写入← 正确答案
- C.操作系统分配栈太慢
- D.编译器生成的代码过多
答案:B。栈溢出的本质是程序对缓冲区写入时没有检查边界,数据越过缓冲区破坏了相邻内存。它不是硬件故障,也不是性能问题,而是一种可被攻击者利用的内存破坏原语。“内存条损坏”是硬件事故,“分配太慢”“代码过多”都与越界写无关。
Q2. 函数调用时返回地址存放在哪里?
- A.CPU 的全局寄存器里
- B.调用者函数的栈帧中
- C.被调用函数的栈帧中← 正确答案
- D.操作系统内核的堆里
答案:C。调用指令 call 会把下一条指令地址压栈,这一地址属于被调用者(callee)的栈帧。返回地址不是全局寄存器,也不在调用者的帧里,更不在内核堆中。它紧挨着被调用函数的局部变量,这正是溢出攻击要篡改的目标。
Q3. 金丝雀(Canary)是如何发挥作用的?
- A.加密所有函数局部变量
- B.返回前校验哨兵值是否被改← 正确答案
- C.限制函数递归调用深度
- D.将整个栈标记为只读
答案:B。金丝雀是放在缓冲区与返回地址之间的随机哨兵值,函数序言写入、尾声校验。若越界写碰到它导致值变化,返回前校验即失败并终止进程。它不加密变量、不限制递归、也不改栈权限,只是“返回前查岗”。
Q4. 编译器 -fstack-protector 的插入策略是什么?
- A.在函数序言与尾声包围检测← 正确答案
- B.只在 main 函数插入检测
- C.只在递归函数插入检测
- D.在每行代码后插入检测
答案:A。该选项在函数序言末尾写入金丝雀、在尾声返回前校验,形成一对检测点。它不是只保护 main,也不限递归函数。默认策略只保护含大缓冲区(≥ 8 字节)的函数,-fstack-protector-all 才覆盖所有函数。
Q5. 栈保护页(guard page)的触发条件是什么?
- A.缓冲区内容被越界改写
- B.栈增长到不可访问页并访存← 正确答案
- C.函数返回地址与预期不符
- D.局部变量数量超过八个
答案:B。保护页是栈尾一页不可访问的内存,只有栈向低地址增长到尽头、访存落到该页时才会触发缺页异常并终止进程。它不检测缓冲区越界写(那是金丝雀的职责),与返回地址比对、变量数量都无关。
Q6. 递归过深可能引发哪些后果?
- A.栈空间耗尽程序崩溃← 正确答案
- B.触发栈保护页异常终止← 正确答案
- C.自动提升栈空间上限
- D.优化代码提升运行速度
答案:AB。每层递归压入一帧,深度超过配额就写进栈外内存;若栈尾有保护页,则会先触发缺页异常、SIGSEGV 终止。操作系统不会自动扩大栈配额,递归也不会带来性能提升,C、D 显然错误。
Q7. 下列哪组防护能有效阻止“返回地址改写成栈上 shellcode”?
- A.NX + ASLR← 正确答案
- B.-O2 加上静态链接
- C.增大栈加上提高优先级
- D.关闭 ASLR 并去掉 NX
答案:A。NX 让栈页不可执行,shellcode 即便注入也无法取指;ASLR 让攻击者无法预知栈地址。优化、静态链接、增大栈都改变不了“可执行”的事实,关闭 ASLR 并去掉 NX 反而是给攻击者开门。
Q8. ROP 攻击为什么能绕过 NX 防护?
- A.注入全新恶意指令代码
- B.复用已有代码片段拼逻辑← 正确答案
- C.直接修改页表关闭 NX
- D.用 DMA 绕过 CPU 取指
答案:B。ROP 不注入新代码,而是把返回地址链依次指向程序自身和 libc 中已有的指令片段(gadget),拼出攻击逻辑。它没有“从栈取指”,所以 NX 拦不住;它也没有改页表或走 DMA,A、C、D 都不符合实际机制。
Q9. 下列属于“检测型”栈防护的是哪些?
- A.金丝雀 Canary← 正确答案
- B.栈保护页 guard page← 正确答案
- C.ASLR 地址随机化
- D.NX 不可执行
答案:AB。金丝雀负责“返回前发现帧内越界”,保护页负责“栈耗尽时立即异常”,二者都是检测并止损的机制。ASLR 和 NX 属于缓解型防护——前者让地址不可预测,后者让数据不可执行,它们并不主动“报警”。
Q10. 纵深防御下攻击者仍可能成功,可能的原因包括哪些?
- A.信息泄露先读出金丝雀← 正确答案
- B.虚表指针等非顺序覆盖← 正确答案
- C.先泄露地址再绕过随机化← 正确答案
- D.所有防护必然同时失效
答案:ABC。金丝雀可通过越界读泄露后被原样写回;C++ 虚表等“写哪里”漏洞能跳过金丝雀直接改函数指针;ASLR 地址也能通过 /proc/self/maps 或堆泄露读出。防护是概率性的而非绝对,D 的说法过于绝对,任何防护都可能被绕过,但“必然失效”不是原因。