(1)本文及后续文章均使用IDA Pro 9+进行软件逆向,IDA Pro 9之前版本的静态栈视图可能与文中截图有略微差别,但不影响分析使用。如需安装IDA Pro 9.1,可参考前面的文章。
(2)程序中绘制的栈布局图片使用draw.io软件完成。
(3)学习之前需要掌握C语言的基础知识,可参考前面的C语言系列文章。
(5)PWN环境的搭建、逆向基础知识等内容本系列文章进行了省略,可自行从网上其他地方学习获得。
(6)本文附件地址(“基础知识”文件夹)。
一、GCC编译参数
1、查看默认的gcc编译参数:
|
|
2、编译生成32位的程序
|
|
3、有关Canary保护的选项
|
|
4、有关NX保护(No-eXecute)(DEP)的选项
|
|
5、有关PIE的选项
|
|
6、有关RELRO(ReLocation Read-Only)的选项
|
|
二、x86程序栈布局
1、C语言函数调用栈
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
编译器利用栈帧,使得函数参数和函数中局部变量的分配与释放对程序员透明。编译器将控制权移交函数本身之前,插入特定代码将函数参数压入栈帧中,并分配足够的内存空间用于存放函数中的局部变量。使用栈帧的一个好处是使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。
栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
更具描述性,以下称EBP为帧基指针, ESP为栈顶指针,并在引用汇编代码时分别记为%ebp和%esp。
函数调用栈的典型内存布局如下:
图中给出主调函数(Caller)(紫色部分)和被调函数(Callee)(蓝色部分)的栈帧布局。函数可以没有参数和局部变量,故图中”Argument(参数)“和”Local Variable(局部变量)“不是函数栈帧结构的必需部分。
函数调用时入栈顺序为:
实参N~1 → 主调函数返回地址 → 主调函数帧基指针EBP → 被调函数局部变量1~N
主调函数将函数参数(实参)按照调用约定依次入栈(图中传参调用约定为从右到左);然后将指令指针EIP入栈以保存主调函数的返回地址(下一条待执行指令的地址);进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。
此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值。本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。如此递归便形成函数调用栈。
2、main函数栈布局
一个典型的 x86 程序 main 函数栈的布局如下图所示:
(1)栈增长方向
栈在内存中是“从高地址向低地址增长”的,符合图中自上而下,从高地址→低地址的视图结构。
(2)栈布局说明
-
envp(环境变量指针)
-
类型:
const char **envp
-
含义:传递给 main 函数的环境变量数组(如 PATH=/usr/bin 等)
-
-
argv(参数数组)
-
类型:
const char **argv
-
含义:命令行参数数组,例如程序名和参数(./prog abc def)
-
-
argc(参数个数)
-
类型:
int argc
-
含义:命令行参数的数量
-
-
Return Address(返回地址)
-
含义:函数返回后跳转到调用者的位置
-
压栈方式:由 call 指令自动压栈(call main → 返回地址 push 到栈上)
-
-
Saved Registers(保存的寄存器)
-
通常为:old EBP,也存在其他寄存器状态需要保存的情况。
-
含义:保存调用者的栈基址(使得 ebp 能恢复),一般在函数开头:push ebp
-
-
Local Variable(局部变量区)
-
向下分配空间:
sub esp, xx
为局部变量留空间 -
变量如:char buf[100], int tmp 等
-
这是最可能被溢出攻击利用的区域
-
(3)程序栈示例
我们用到的示例程序(Function)的代码:
|
|
buffer[12]
是一个局部变量,在栈中分配空间;- 调用
gets()
时,用户输入的数据被写入栈上的 buffer 区域; puts()
只是把 buffer 打印出来;- 该函数在调用时会构造栈帧:包括参数(
envp
、argc
、argv
)、返回地址、旧的 EBP 以及 buffer 所在的局部变量区。
Linux下编译该程序:
|
|
使用IDA Pro分析该程序。
建议将IDA Pro识别出的传给 gets 函数的缓存区变量s,直接重命名为buffer,这会使代码更容易理解。
双击 buffer 变量的位置,即可跳转到IDA Pro的静态栈视图。
在静态栈视图中,我们可以看到该函数调用时的基本栈布局信息,比如buffer
、saved registers
、return address
、argc
、argv
、envp
是怎么布局的。但需要注意的是IDA Pro静态栈视图是从低地址到高地址表示的,习惯上我们一般会按照自上而下,从高地址→低地址的顺序去制图,所以,需要我们去画一下栈布局。
需要注意的是,本程序中保存到栈上的寄存器不仅只有ebp,还有ebx,ecx。我们需要根据实际的栈布局情况去绘图。
|
|
项目 | 大小 | 位于栈上的偏移范围(相对 EBP) |
---|---|---|
envp |
4 字节 | [ebp+10h] |
argv |
4 字节 | [ebp+0Ch] |
argc |
4 字节 | [ebp+08h] |
返回地址 | 4 字节 | [ebp+04h] |
调用者的 EBP(old) | 4 字节 | [ebp+00h] |
保存的 EBX | 4 字节 | [ebp-04h] |
保存的 ECX | 4 字节 | [ebp-08h] |
char buffer[12] |
12 字节 | [ebp-14h] 到 [ebp-09h] (从低地址向上) |
在调用 main 函数之前,程序会将函数参数envp、argc、argv依次压栈。
接着,程序调用call指令或其他方式把返回地址压栈。
call指令有两个功能:第一个是执行call指令后,程序会跳转到指定的要调用的函数地址去执行;第二个是会将该函数调用结束后要执行的指令(也就是返回地址)保存到栈上。
最后,程序会在 main 函数中使用push、sub等指令完成对栈的布局。
在 main 函数中,我们重点关注导致函数栈发生变化的几条指令:
.text:08049180 000 push ebp
.text:08049181 004 mov ebp, esp
.text:08049183 004 push ebx
.text:08049184 008 push ecx
.text:08049185 00C sub esp, 10h
.text:08049188 01C call __x86_get_pc_thunk_bx
push ebp 指令
作用:保存旧栈基址,以便后续能恢复;
将调用者的 EBP
值压入栈中(4字节);栈指针 ESP = ESP - 4
,IDA 视图中显示为000
到004
。
mov ebp, esp 指令
作用:建立当前函数的局部变量/参数访问基准;
用当前 ESP 值作为新栈帧基址;不会影响栈内容,但会让程序以 EBP
为基准来布局栈帧。
push ebx 指令
作用:保存调用者的 EBX 寄存器内容(callee-saved 寄存器);
栈又下移 4 字节;栈指针 ESP = ESP - 4
,IDA 视图中显示为004
到008
。
push ecx 指令
作用:保存 ECX 的值;
栈再下移 4 字节;栈指针 ESP = ESP - 4
,IDA 视图中显示为008
到00C
。
sub esp, 10h 指令
作用:为局部变量预留 0x10 字节(16 字节)空间;
栈再下移 16 字节;栈指针 ESP = ESP - 16
,IDA 视图中显示为008
到00C
。
至此,在x86程序中,一个典型的 main 函数调用过程中的栈布局介绍完成。
三、Linux程序保护机制
使用命令checksec
可以查看程序开启的保护机制:
pip3 install pwntools 后即可使用附加工具 checksec
1、Canary保护机制
Stack Canary(栈金丝雀)
是一种重要的计算机安全机制,旨在防止缓冲区溢出攻击,特别是栈溢出攻击。其名称源于矿工在矿井中携带金丝雀,以便在有毒气体泄漏时,金丝雀比人类更早感知并发出警告。同理,栈金丝雀在程序栈中充当“早期预警系统”,用于检测栈溢出行为。
在启用栈保护的程序中,每当函数被调用时,编译器会在栈帧中插入一个特殊的值(即“金丝雀”)。当函数返回时,程序会检查该值是否被篡改。如果发现金丝雀值被修改,程序会认为发生了栈溢出攻击,立即终止执行,以防止攻击者利用溢出覆盖返回地址等关键数据。
在启用栈保护的情况下,x86 程序的 main 函数栈结构如下图所示:
|
|
__stack_chk_guard
或 __security_cookie
是由编译器随机生成的一个值。
Canary 值位于返回地址前面,局部变量后面。攻击者若要覆盖返回地址,必须首先覆盖 Canary 值,从而触发栈保护机制。
Canary 保护在Linux下被称为Stack Canaries 保护
,在Windows被称为GS 保护
。
根据生成方式的不同,可以将 Stack Canaries 分为三类:Terminator Canary(终止符金丝雀)
、Random Canary(随机金丝雀)
、Random XOR Canary(随机异或金丝雀)
-
Terminator Canary(终止符金丝雀):这种 Canary 是一个固定的值,通常是0或者一些字符串终止符(如0x00, 0xFF, 0x0A, 0xFF)。这种 Canary 的优点是可以防止一些基于字符串操作的缓冲区溢出攻击,因为这些操作会在遇到终止符时停止。但该方式容易被攻击者预测和绕过。
-
Random Canary(随机金丝雀): 为防止 Canary 的值被攻击者猜到,这种方式的 Canary 是一个随机生成的值,会在程序初始化时随机生成,增加了攻击者猜测的难度。通常使用
/dev/urandom
或当前时间的哈希值生成。 -
Random XOR Canary(随机异或金丝雀):在随机生成的 Canary 值的基础上,与栈帧中的控制数据(如返回地址、帧指针等)进行异或操作。这种方式增加了攻击的复杂度,即使攻击者获取了 Canary 值,也难以利用。
一般情况下,Stack Canaries/GS 保护机制会在函数开始执行的时候先往栈底插入一个 cookie 值,这个 cookie 值就被称为 Canary。用于检测缓冲区溢出,以及防止攻击者利用缓冲区溢出攻击,从而执行恶意代码。
(1)Windows GS保护示例
|
|
如下所示,示例为32位Windows应用程序(Windows-GS.exe)在_main
函数中的GS保护。
在_main
函数中,程序会从__security_cookie
全局变量中获取随机数,然后将其赋值给 EAX 寄存器。接着将其与此时 EBP 的值进行异或运算,并将得到的结果存放到 EAX 寄存器中。最后,将 EAX 中的值存放到栈上的 cookie(原var_4) 变量中。
双击变量cookie,就可以跳转到IDA Pro的静态栈视图进行查看(可结合前面提到的Canary保护机制视图对比查看)
|
|
其中伪C代码中的索引[ebp+cookie]
就是cookie变量在栈中[ebp-04h]
的位置。
程序会在_mian
函数代码执行完返回前,对 cookie 的值进行校验。首先会将当前 cookie 的值从栈上取出并存放到 ECX 寄存器中,然后将 ECX 中的值与此时 EBP 的值(EBP的值在当前_main函数内始终不变)进行异或运算(还原cookie值),得到的结果存放到 ECX 中。接着会调用 __security_check_cookie
函数对 cookie 的值进行校验。
在__security_check_cookie
函数中,如果此时 ECX 中保存的 Cookie 值和最初的 __security_cookie
全局变量中获取的随机数相同,就返回正常;如果不同就会跳转到___report_gsfailure
导致程序异常退出。
上面连续出现了5条汇编指令:
mov ecx, [ebp+cookie]
add esp, 8
xor ecx, ebp ; StackCookie
xor eax, eax
call @__security_check_cookie@4 ; __security_check_cookie(x)
其中有关 cookie 校验的关键指令为前面图片中标红的指令:
mov ecx, [ebp+cookie]
xor ecx, ebp ; StackCookie
call @__security_check_cookie@4 ; __security_check_cookie(x)
针对这5条汇编指令解释:
mov ecx, [ebp+cookie]
作用:从栈中取出原先保存的 canary 值(进入函数时写入的);将其保存到 ecx
中,准备做检查。
add esp, 8
作用:恢复栈空间,因为程序前面通过 call 调用了gets
和 puts
函数,调用这两个函数前都push eax
(参数)了一次,一次就会占用栈空间 4 字节,所以总共占用栈空间 8 字节;这一步就是清理参数,调整 esp
回到调用gets
和 puts
函数前的状态。
xor ecx, ebp
作用:取出栈上的 cookie 后,还原原始的 cookie 值ecx = (cookie_on_stack) ^ ebp
。因为函数开始时存的是 cookie ^ ebp
,所以现在用 ecx ^ ebp
恢复原始值;同时,为下个函数 __security_check_cookie(ecx)
做准备。
xor eax, eax
作用:将 eax
置 0;有些调用约定要求:eax
为 0 表示正常返回;在这里不关键,但多数是为了约定或语义明确。
call @__security_check_cookie@4
作用:调用实际的栈保护检查函数。cookie值如果一致 → 安全,继续执行;cookie值如果不一致 → 说明栈被篡改,程序终止,通常调用__report_gsfailure
或直接崩溃。
(2)Linux Stack Canaries 保护示例
|
|
如下所示,示例为32位Linux应用程序(Linux-Canaries)在_main
函数中的GS保护。
在main
函数中,程序首先通过mov eax, large gs:14h
指令,从段寄存器GS
的偏移0x14
处获取全局的stack canary
值(这是线程局部存储中的栈保护值),将其存入 EAX
寄存器。随后,将该值保存在当前函数栈帧的局部变量 [ebp+canary]
(原[ebp+var_C]变量)中,用作进入函数前对 canary 值的保存。
其中,gets
函数和puts
传递的变量s已被重命名为buffer。
双击变量canary,就可以跳转到IDA Pro的静态栈视图进行查看(可结合前面提到的Canary保护机制视图对比查看)
|
|
在函数执行完毕,即将返回前。程序会从当前栈的[ebp+canary]
处读取最初保存的 canary 值并保存到edx寄存器中,接着从gs:14h
处重新取出当前程序 canary 的值,并与edx
中获得的栈上的 canary 值相减;若两值一致(sub
结果为 0),说明栈未被破坏,继续执行返回流程;若不一致(即跳转未发生),程序会调用__stack_chk_fail_local
函数;__stack_chk_fail_local
函数会进一步调用___stack_chk_fail
,导致程序异常终止。
2、ASLR保护机制
ASLR(Address Space Layout Randomization)地址空间布局随机化机制,ASLR 是现代操作系统中广泛采用的一种内存安全保护机制,旨在通过随机化程序内存空间的关键区域地址,增加漏洞利用难度,防止攻击者准确定位关键结构(如 shellcode、libc 函数地址、返回地址等)。
ASLR 生效依赖两个条件:
- 程序需启用 PIE(-fPIE -pie)编译/链接选项,即代码段可被加载到任意地址;
- 系统启用了 ASLR 内核策略,否则加载地址仍然是固定的。
Linux 中通过 /proc/sys/kernel/randomize_va_space
控制 ASLR 的启用方式,其含义如下:
值 | 含义 |
---|---|
0 | 关闭 ASLR,所有地址固定(栈、堆、映射段、共享库)。适用于调试/开发 |
1 | 基础 ASLR,栈、mmap 映射、共享库地址随机化,但堆基址固定 |
2 | 增强 ASLR(默认),在模式 1 的基础上,对堆地址也进行随机化 |
查看当前设置:
|
|
临时修改设置:
|
|
在关闭 ASLR 或未启用 PIE 的情况下,攻击者能准确预测目标地址(如返回地址、libc 函数),易于构造攻击;开启 ASLR + PIE 后,攻击者必须首先泄露地址信息(信息泄露漏洞),否则无法完成精确利用。
3、NX保护机制
NX(No-eXecute)是一种内存执行权限控制机制,用于防止将数据页当作代码执行,从而阻止攻击者通过栈/堆注入 ShellCode 的方式劫持程序流程。
- 在 Linux 平台下,称为 NX(No-eXecute)保护;
- 在 Windows 平台下,称为 DEP(Data Execution Prevention,数据执行保护)。
NX/DEP 的核心思想是:将 数据存储区域(如栈、堆、BSS 段)标记为不可执行。当攻击者试图将恶意代码注入栈或堆,并试图通过函数返回或跳转指令执行这一段 ShellCode 时,CPU 会检测到当前内存页无执行权限,从而抛出异常,终止程序运行,防止攻击得逞。
ShellCode本质是在有限的局部变量空间(比如Buffer)中,构造的一个可以执行系统函数调用的一连串指令,ShellCode所在的位置必须有可执行权限才能成功被利用。
NX 防止”执行注入”,而不是“注入本身”。
关于NX保护需要注意几点:
- ShellCode 执行失败 ≠ 攻击失败:攻击者仍可以利用返回到合法代码区段的方式进行攻击,如 ROP。
- 代码执行区绕过:若程序中存在执行权限的后门函数或 gadget 片段,攻击者可跳转到这些合法区域绕过 NX 保护。
- NX 保护仅作用于数据页:对
.text
、.plt
等代码段没有限制。
4、RELRO保护机制
(1)什么是GOT表?
GOT表(Global Offset Table)(全局偏移表)是一个数据结构,位于.got
或.got.plt
段中,用于存储外部函数或全局变量在运行时的实际地址。
- 作用:在程序运行时,通过 GOT 表项获取外部函数或变量的真实地址,实现位置无关代码(PIC)的支持。
- 特点:
- 在程序启动时,GOT 表中的某些项尚未填充,待首次调用时由动态链接器解析并填充。
- GOT 表通常是可读写的,这也是某些攻击(如 GOT 劫持)的潜在目标。
(2)什么是PLT表?
PLT表(Procedure Linkage Table)(过程链接表)是一段代码,位于.plt
段中,用于实现对外部函数的调用。
- 作用:当程序调用外部函数时,实际跳转到 PLT 表中的对应入口,PLT 再通过 GOT 获取函数的真实地
- 特点:
- PLT 表中的每个入口对应一个外部函数,包含跳转指令和辅助指令。
- PLT 表的首项(通常称为 PLT0)用于支持延迟绑定机制。
(3)延迟绑定技术
延迟绑定是一种优化机制,只有在函数首次被调用时,才解析其真实地址,从而减少程序启动时间。
在 ELF中,GOT表与PLT表是实现动态链接和延迟绑定(Lazy Binding)的关键机制。
- 在延迟绑定模式下,程序首次调用某个外部函数(如
printf()
)时,实际调用的是对应的 PLT 段; - PLT 段会触发动态链接器解析该函数的真实地址,并将其写入对应的 GOT 表项;
- 下次调用该函数时,程序将直接从 GOT 表中取出函数地址,无需再解析。
第一次调用外部函数的流程:
- 程序调用外部函数(例如
printf
),实际跳转到printf@plt
,这是 PLT 表中的对应入口。 - 执行 PLT 表中的第一条指令:
printf@plt
中的跳转指令jmp *printf@got
,会跳转到 GOT 表中printf
的项。 - 首次调用时的 GOT 表项:
printf@got
中存储的是指向printf@plt+6
的地址,即 PLT 表中printf
项的第二条指令。 - 执行 PLT 表中的第二条指令:这条指令会将
printf
在重定位表(.rel.plt
或.rela.plt
)中的索引压入栈中。 - 跳转到 PLT0:随后跳转到 PLT 表的首项
PLT0
,它会调用动态链接器的解析函数(如_dl_runtime_resolve
)。 - 动态链接器解析符号:动态链接器根据提供的索引解析
printf
的真实地址,并将其写入printf@got
表项。 - 返回并执行真实函数:解析完成后,控制权返回,程序继续执行
printf
函数。
调用 printf
│
▼
printf@plt ──▶ jmp *printf@got
│
▼
[首次调用] printf@got → printf@plt+6
│
▼
push 重定位索引
│
▼
jmp PLT0
│
▼
PLT0 → 调用 _dl_runtime_resolve
│
▼
解析并填充 printf@got → printf 实际地址
│
▼
跳转到 printf 实际地址
第二次调用外部函数的流程:
- 程序再次调用
printf
,实际跳转到printf@plt
,这是 PLT 表中的对应入口。 - 执行 PLT 表中的第一条指令:
printf@plt
中的跳转指令jmp *printf@got
,会跳转到 GOT 表中printf
的项。 printf@got
中已存有printf
的真实地址,程序直接跳转,无需再次解析。
call printf@plt
│
▼
jmp *printf@got
│
▼
跳转到 printf 实际地址
在 ELF 文件中,PLT 表的首项(通常称为PLT0
)是一个特殊的入口,用于支持延迟绑定机制。
- PLT0 的作用:当程序首次调用某个外部函数时,PLT 表中的对应入口会跳转到 PLT0,PLT0 再调用动态链接器的解析函数(如
_dl_runtime_resolve
)来解析该函数的真实地址。 - 是否每次都使用 PLT0:是的,在延迟绑定模式下,所有外部函数的首次调用都会通过各自的 PLT 表项跳转到 PLT0,由 PLT0 统一调用动态链接器进行解析。
(4)Glibc库
glibc(GNU C Library)是 Linux 系统中最核心的 C 标准库,实现了诸如 printf()
、system()
、malloc()
等常用函数的底层逻辑。
- 它是 ELF 可执行文件中大多数外部函数的真正实现位置。
- 通常作为动态库
libc.so.6
链接进程序,在运行时由动态链接器(ld-linux.so
)加载。
glibc 和动态链接之间的关系
在动态链接模型下,glibc
并不会被编译进最终程序中,而是在程序运行时通过.plt
和.got
机制“延迟”加载:程序中调用的printf()
函数,并不直接调用glibc
中的实现,而是通过printf@plt
→ printf@got
→ libc.so.6:printf
来间接跳转。
结合这里的 glibc 的知识,我们完善下前面提到的调用外部函数的过程。
假设某 ELF 程序调用了 printf("hello world\n")
,以下是流程概览:
编译时(生成 ELF 文件):
编译器不会将 printf() 的实现代码直接编译进程序,而是生成一个对 printf@plt 的调用。
链接时(程序还没运行):
链接器添加 .plt 和 .got.plt 结构,生成 printf@plt,并设置其间接跳转到 printf@got。
程序运行,首次调用 printf():
call printf@plt
│
▼
jmp *printf@got ← GOT 表中尚未解析,跳转到 plt+6
│
▼
push 重定位索引 ← 用于告诉动态链接器解析哪个符号
jmp plt0 ← 跳转到 PLT0,调用解析函数
│
▼
_dl_runtime_resolve (由 ld-linux.so 提供)
│
▼
解析 printf → libc.so.6:printf;
将 libc.so.6:printf 写入 printf@got;
跳转执行真正的 printf();
程序运行,再次调用 printf():
call printf@plt
│
▼
jmp *printf@got ← 这次 GOT 表中是 libc 的地址
│
▼
libc.so.6:printf ← 直接执行真正的 printf 实现
glibc
是程序中外部函数如printf
、system
的实际实现所在。在 ELF 程序中,通过 PLT 和 GOT 实现延迟绑定,首次调用这些函数时会触发动态链接器解析其真实地址并写入 GOT 表,后续调用直接跳转到glibc
中的实现。
概念 | 描述 |
---|---|
glibc | 提供printf() 的真实实现,是目标地址 |
PLT 表 | 生成在可执行文件中,起跳板作用 |
GOT 表 | 运行时动态链接器写入函数实际地址 |
动态链接器 | _dl_runtime_resolve() 负责首次解析 |
(5)RELRO保护
由于.got.plt
段在程序运行期间需保持可写,攻击者可能通过漏洞(如栈溢出、格式化字符串等)篡改 GOT 表项,将其重定向到恶意函数(如system()
),从而实现控制流劫持。
RELRO(RELocation Read-Only)是由 Red Hat 工程师 Jakub Jelinek 于 2004 年引入的一种安全机制,旨在防止 GOT 表被修改,从根本上缓解延迟绑定机制带来的劫持风险。
RELRO 的两种模式:
模式 | 描述 |
---|---|
Partial RELRO | 使.got 和.dynamic 等段在程序初始化后变为只读(via mprotect ),但保留延迟绑定机制(.got.plt 仍可写)。默认由-Wl,-z,relro 启用 |
Full RELRO | 禁用延迟绑定,程序启动前即完成所有符号解析,.got.plt 也变为只读,彻底防止 GOT 劫持。由 -Wl,-z,relro -Wl,-z,now 同时开启。 |
关于RELRO保护需要注意几点:
- Full RELRO 会略微增加启动时间,因为程序需在启动前完成所有动态符号解析;
- 如果程序未开启 Full RELRO,攻击者仍可能通过 GOT 表实现如
ret2got
、got overwrite
等攻击; - Full RELRO + PIE + NX + Canary + ASLR 是 Linux 环境下现代 ELF 程序的推荐安全编译配置。