常见保护¶
本节作者:EROORvbsyes
前言¶
这一节我们要讲解常见的保护。 我们先总览一下
- Canary <相关视频:CTF--Pwn第一课--Canary保护原理及绕过-哔哩哔哩-EROORvbsyes>
- 泄露
- gs超长覆盖
- PIE :Position-Independent Executable(位置无关可执行文件)
- 泄露计算
- partial write
- ASLR :Address Space Layout Randomization(地址空间布局随机化)
- 泄露计算
- partial write
- RELRO :RELocation Read-Only(重定位表只读)
- partial relro
- full relro
- strip
- NX (No-eXecute)
- ROP绕过
- 改写映射权限
- SHSTK(影子栈)
- CFI(控制流完整性校验)
- IBT(Indirect Branch Tracking),间接跳转跟踪
Canary¶
Canary(金丝雀)是Linux下GCC编译器提供的一种栈溢出保护机制,得名于煤矿里的“金丝雀预警”。
其核心原理是在函数的返回地址与局部变量之间插入一个随机的“金丝雀”值。在函数入口处,程序会将该值存入栈中;在函数返回前,会检查该值是否被修改。
- 触发:如果攻击者通过缓冲区溢出覆盖返回地址,途中必然覆盖掉这个随机值。一旦检测到数值改变,程序会立即终止并报错,阻止攻击者劫持控制流。
- 强度:该值通常从进程启动时生成的随机值段(称为“终止符”段)获取,末尾常设\x00(空字节),用于截断字符串操作,增加了溢出的难度。
- 局限性:它只能保护开启了该机制的函数,无法防御仅篡改局部变量、未触及返回地址的溢出,且存在特定的信息泄露绕过手法。
栈初始化时会初始化TLS(线程局部存储) 而canary是初始化在TLS里的,即canary=[gs+0x28]
参考结构图:
| STACK |
|---|
| var1 |
| var2 |
| canary |
| rbp |
| rip |
对于其绕过也比较直接 1.我们可以尝试利用格式化字符串,泄露栈上的数据,其中也包含canary 2.我们可以通过利用puts函数的性质,在第一次写的时候多写一字节,覆盖截断符,在输出时就会把canary打印出来,通过字符拼接或者数学计算可以再次得到完整canary 3.在一些libc早期版本,TLS的数据会初始化在栈的高地址区域,在溢出长度允许的情况下,我们可以写入超长字符覆盖备份的canary,来达到不泄露绕过canary的目的 4.one by one 爆破,在一部分题目里面,可能遇到配置错误的服务,fork出来的程序canary是固定的,我们就可以用逐位爆破来获得canary。尝试最大方案数为256*7=1792种 第三种情况较为少见,笔者只在CVE复现看见过
在前期读者可能遇到smash泄露的题目,读者可以查阅资料了解一下,原理比较简单,在这里不过多赘述。
题目¶
我们有一个main函数和win函数
void win(){
execve("/bin/sh",0,0);
return;
}
int main(){
char name[64];
read(0,name,65);
puts(name);
read(0,name,256);
return 0;
}
这里很明显有一次回显,两次输入。 那我们在第一次输入的时候输入一个65的长度的字符串,不以00结尾就可以了 这样就能覆盖掉canary低位00,此时puts就不会被00截断了,就会直接泄露出canary 之后我们通过加00或者位移操作就可以还原canary。 后面就是经典的ret2text了 参考exp:
payload1=b""
payload1+=b"a"*65
p.send(payload1)
p.recvuntil(b"a"*65)
canary=p.recv(7)
canary=b"\x00"+canary
payload2=b"a"*64+canary+p64(0)+p64(win)
p.send(payload2)
ASLR¶
ASLR(地址空间布局随机化)是一项系统级安全机制,用于阻止攻击者利用已知内存地址进行精准攻击。
核心原理 每次运行程序时,系统会随机化关键内存区域的基地址,包括:
- 可执行文件(需开启 PIE)
- 共享库(如 libc)
- 栈、堆、mmap 映射区
这使得攻击者无法预判函数、指令或 gadget 的具体地址,从而难以构造 ROP 链或直接调用关键函数。
实现级别
- 完全随机化:基址在每次执行或每次页分配时随机偏移,偏移量通常为页对齐(4KB 或更大)。
- 内核设置:可通过 /proc/sys/kernel/randomize_va_space 控制:
- 0:关闭
- 1:随机化栈、库、mmap(传统模式)
- 2:开启堆随机化(默认)
面对ASLR的绕过,我们主要有以下几种方案
- 1.经典的地址泄露,基于偏移计算基地址
- 2.partial write(这个也是基于低地址偏移不变的特性)
- 3.爆破地址
那么我们这里不再详细讲解和提供例题,因为aslr本来是一个很基础的保护,只需要完成信息泄露即可。相信能看到这里的师傅应该都知道aslr怎么绕过
PIE¶
PIE(位置无关可执行文件) 是一种编译技术,让可执行文件本身也能像共享库一样在内存中随机化加载基址,从而配合 ASLR 实现完整的地址随机化。
核心作用
- 没有 PIE 时,ASLR 只能随机化 libc、栈、堆,但可执行文件的代码段、数据段固定加载到 0x400000 等已知地址,攻击者依然可以精准跳转。
- 开启 PIE 后,程序自身代码、全局变量、GOT 表等每次加载基址都随机,攻击者无法直接使用程序内的 gadget 或函数地址。
与 ASLR 的关系
- ASLR 是内核决定是否随机化;PIE 是程序是否支持随机化。
- 只有 ASLR 开启 + 程序编译为 PIE,可执行文件基址才会随机化。
- 二者常同时提及,但职责不同:ASLR 是开关,PIE 是被随机化的前提。 绕过技术同aslr。
RELRO¶
RELRO(重定位只读) 是一种动态链接器的安全增强机制,用于保护全局偏移表(GOT) 和重定位表不被恶意篡改,防止攻击者通过改写 GOT 劫持程序流程。
两种模式
- Partial RELRO(部分):编译时默认开启。特点包括:
- 将 .got 段(GOT 前半部分)标记为只读
- 初始化 .bss 段起始部分
- .got.plt 段(GOT 后半部分,含函数地址)仍可写,延迟绑定机制正常工作
- 防御能力有限,仍可被 GOT 劫持攻击
- Full RELRO(完全):需显式开启(-z relro -z now)。在 Partial 基础上:
- 完成所有符号重定位,禁用延迟绑定
- 将整个 GOT(包括 .got.plt)标记为只读
- 攻击者无法通过任意地址写来覆盖 GOT 表项
防御原理
GOT 劫持是常见利用方式:攻击者利用漏洞(如栈溢出)改写某个 GOT 表项,使其指向恶意函数或 shellcode。Full RELRO 将 GOT 设为只读后,此类攻击彻底失效。
对于RELRO来说没有什么主流的绕过方案,所以一般保护开起来都是出题人有目的的 例如没开relro,或者partial relro可能在提示可以改got然后劫持函数。 如果是full的那么就是堵死了改got了,我们一般考虑劫持栈或者数据流执行虚函数等等。
strip¶
严谨来说,strip不是一个保护,只是一个程序名称,用于剔除符号表来减小程序体积或者增加逆向难度。
未strip时,程序的全局变量和函数名都是可以直接在ida中看到的,gdb也可以直接依据函数名下断点等。但是strip后,ida就看不到了,只能看到以sub_xxx的函数,gdb也会提示符号不存在。相当于提高了逆向难度和调试难度。
strip后的题目在ctf中的占比很大,所以需要选手有很强的逆向难度。
NX¶
NX保护是大多数题目开起的保护之一,并且gcc也是默认开启的。 顺带一提,pie在gcc是默认开启的,默认情况下relro是partial relro,canary不是默认开启的
NX的工作原理就是对指定区域,比如栈,将其对应的内存段标记为不可执行, 但是注意了,这里的不可执行,其实是页表标定的不可执行,并不是真正物理页的不可执行。 所以我们通过重映射或者改写页表flag是可以完成将原本不可执行页改成可执行页的。
对于NX的绕过方案,我们使用rop链来绕过或者迁移shellcode到其他页上。
CET¶
这里要讲的影子栈和控制流完整性校验都是属于CET保护方案的。 CET(Control-flow Enforcement Technology)机制是 Intel提出基于硬件的⽤于缓解 ROP/JOP/COP 的新技术。
特别强调下,他是基于硬件⽀持的解决⽅案。从Intel的Tigerlake (11th gen),Alderlake (12th gen)/Sapphire-Rapid起,粗颗粒度地旨在预防前向( call/jmp )和后向( ret )控制流指令劫持来御防ROP的攻击。因此针对防御对象不同,CET技术又分为CET-SS用于针对ROP的ret指令和CET-IBT用于针对JOP/COP的jmp/call指令
CET-SS和CET-IBT 在实现机制上属于CPU内部异常,这里就涉及到中断机制。当执行启动CET发现执行执行流中没有endbr64或函数返回ret和影子栈中shadow stack保存的ret不一致时,CPU内部触发异常,这里CET占用的中断向量21号,触发#CP并归为陷阱执行中断处理程序exc_control_protection(),对CET-SS CET-IBT分情况进行报错。CET-IBT -> "traps: Missing ENDBR: xxx", CET-SS-> #CP(control protect)。
SHSTK(CET-SS)¶
CET 使操作系统能够创建⼀个 shadow stack (影⼦栈)。正常情况下,当执⾏ call 指令时,会将 call 指令后⼀条指令地址压栈。当启⽤了 shadow stack 后,会同时在普通数据栈和 shadow stack 中压⼊返回地址,随后在执⾏ ret 返回时,会将 shadow stack 中的返回地址和普通数据栈中的返回地址做对⽐,如匹配,则正常执⾏,如不匹配,则触发#CP(Controlflow Protection) 异常。
可以理解为canary Plus Pro max。 :)
IBT(CET-IBT)¶
JOP/COP 攻击⼿法与 ROP 类似,只不过是把 ROP 中以 ret 指令做跳板的关键点替换成了 call/jmp 指令。这⼀指令序列,这就是 COP 中的 gadget ,以 call 指令为跳板,也就不需要 ret 指令的辅助了。这种不需要 ret 指令的攻击场景下,前⾯所说的 shadow stack 机制就失效了。这种情况下, CET 的第⼆种机制 IBT(Indirect Branch Tracking) 就应运⽽⽣了。
IBT(Indirect Branch Tracking),间接跳转跟踪"希望能防止攻击者让间接跳转(例如,通过指针变量进行的函数调用)进入一个不应该走到的地方。功守道之中是为了应对JOP/COP的。
IBT 是为了防御面向跳转编程的(jump-oriented programming);工作原理是试图确保每个 indirect branch 的目标确实都是适合作为跳转目标的。IBT 的方法有很多,每一种都有自己的优势和劣势。例如,内核在 5.13 开发周期中支持了编译器实现的 IBT 机制。在这种模式下,编译器通过一个 "jump table, 跳转表" 来完成每一个间接跳转,不仅确保目标是要供间接跳转使用的,而且要确保被调用函数的原型与调用者所期望的一致。这种方法是很有效的,但要增加很多编译、运行时的开销
英特尔的 IBT 方法相当简单,但优点是得到了硬件的支持,因此速度更快.如果 IBT 被启用,那么 CPU 将确保每个间接跳转都落在一条特殊指令(endbr32 或 endbr64)上,该指令执行时跟 no-op 效果一致。如果发现意外,那么处理器将引发一次 control-protection(#CP)exception。
| 维度 | CFI(广义) | IBT(Intel 实现) |
|---|---|---|
| 实现层级 | 软件(编译器插桩) + 可选硬件 | 纯硬件(CPU) |
| 粒度 | 粗粒度(按函数类型)或细粒度(按地址) | 细粒度,基于指令标记 |
| 性能开销 | 较高(软件检查) | 很低(硬件原生执行) |
| 兼容性 | 需要重新编译 | 需要编译器生成 endbr64,操作系统启用 |
要注意的是IBT是intel对CFI的硬件实现 维度 CFI(广义) IBT(Intel 实现) 实现层级 软件(编译器插桩) + 可选硬件 纯硬件(CPU) 粒度 粗粒度(按函数类型)或细粒度(按地址) 细粒度,基于指令标记 性能开销 较高(软件检查) 很低(硬件原生执行) 兼容性 需要重新编译 需要编译器生成 endbr64,操作系统启用
传统的软件 CFI(如 LLVM CFI)会在间接调用前插入检查代码;而 IBT 将这些检查下沉到 CPU 流水线,既降低了开销,也避免了软件实现可能被绕过的问题。
对于CET的绕过我们有:
1.经典绕过:COOP(伪造对象编程)¶
这是目前最被广泛研究的方案。其核心思路是:既然你保护“返回地址”,那我就不返回了。
原理:CET的影子栈主要防御后向边缘(RET指令)。COOP完全不碰返回地址,而是通过劫持C++对象的虚表指针,将程序流导向攻击者精心构造的“假对象”和合法函数中的特定指令序列(vfgadget)。
效果:由于执行流始终通过合法的CALL指令进入函数(带有ENDBR64),且不触发RET校验,因此可以完美绕过Intel CET的硬件防御。
2.硬件特性滥用:Shadow Stack 迁移¶
这是利用 CET 自身的管理指令(RSTORSSP)进行的“降维打击”。
- 原理:如果攻击者能通过漏洞(如任意写)构造一块伪造的影子栈内存,并执行RSTORSSP指令,就能让 CPU 切换使用这块攻击者控制的影子栈。
- 后果:一旦切换到伪造栈,原本的返回地址校验就形同虚设,攻击者可以直接在伪造栈上布置 ROP 链。这属于利用合法硬件指令做非法操作。
3.语义层攻击:全局函数指针劫持¶
这是一种学术界的自动化攻击方案(如工具 Untangle)。
- 原理:CET 只保证跳转目标以ENDBR64开头,但不保证“该函数是否应该在此处被调用”。该方案通过静态分析,自动寻找那些可以被外部调用的全局函数指针。
- 特点:将目标函数指针篡改为满足调用条件的其他合法函数,利用合法函数间的调用关系实现任意代码执行。
4.利用机制缺陷:WRSS 指令¶
- 原理:Intel CET 有一条特殊指令 WRSS,允许在特定模式下直接写入影子栈。虽然正常情况下 WRSS 是被禁用的,但如果系统配置失误或存在结合其他漏洞,攻击者可以直接覆盖影子栈中的返回地址。
总结与现状¶
虽然存在上述绕过方案,但利用门槛依然很高 :( :
- COOP 依赖环境:需要目标程序使用 C++ 且存在复杂的虚函数调用链。
- 硬件特性利用:RSTORSSP 和 WRSS 需要特定的内存布局和权限,实战难度极大。
- 业界现状:目前尚未出现大规模的在野利用,CET 依然极大地提高了漏洞利用的成本。
在CTF中也很少出现关于CET的题目,读者仅需做一个了解即可 qwq
总结¶
常见保护组合与绕过速查表
| 保护 | 作用对象 | 绕过方案 |
|---|---|---|
| Canary | 栈溢出 | 泄露地址,爆破 |
| ASLR | libc,heap地址 | 泄露地址,partial write,爆破 |
| PIE | 程序地址 | 泄露计算,爆破,partial write |
| NX | 内存段 | ROP,mprotect,迁移 shellcode |
| RELRO | 延时绑定技术,.plt.got |
劫持数据流(虚表、栈) |
| CET(SS+IBT) | ret / 间接跳转 | COOP、影子栈迁移(高难度) |
补充说明
GCC默认开启:PIE、NX、Partial RELRO GCC默认不开启:Canary(需 -fstack-protector) ASLR 是系统级设置,与编译选项无关