ELF 文件的常见保护机制及绕过思路

在 Pwn题目中,ELF(Executable and Linkable Format)文件通常会开启多种保护机制,以抵御常见的漏洞利用手段。理解这些保护机制的原理、检测方法及绕过思路,是成功解题的核心前提。本文将系统梳理 PWN 题目中主流的 ELF 保护机制,并详细分析对应的绕过方案。

前置须知

  • system("sh")system("/bin/sh")execve("/bin/sh", 0, 0)等函数可以用来获取 shell 权限。得到shell权限之后就可以cat flag了。

检测方式

1
2
3
checksec pwn  # pwn为目标ELF文件
# 如果报错可以使用
checksec --file=pwn

输入以上代码会在终端里显示出ELF的保护机制。

主要保护机制及绕过思路

一、NX(No-eXecute)保护:栈 / 堆不可执行

1. 保护原理

NX(No-eXecute,不可执行)是最基础的 ELF 保护机制之一,其核心作用是标记栈、堆等数据区域为 “不可执行”。当程序尝试将数据区域(如栈上的 shellcode)当作代码执行时,CPU 会触发异常,直接终止程序运行,从而阻止攻击者通过注入 shellcode 并执行来获取权限。

2. 检测方法

  • 工具检测:使用checksec,若输出中包含NX: NX enabled,则表示开启 NX 保护;若为NX: NX disabled,则未开启。

3. 绕过思路

NX 保护的核心是 “阻止数据区域执行”,因此绕过思路围绕 “不依赖数据区域执行代码” 展开,主流方案如下:

1.Ret2Libc:利用系统库函数

  • 原理:程序运行时会加载libc(C 标准库),而libc中包含system("/bin/sh")execve等可直接获取 shell 的函数。通过漏洞(如栈溢出)控制程序的返回地址,将其指向libc中的system函数地址,并构造参数(如"/bin/sh"的地址),让程序执行system("/bin/sh"),从而绕过 NX。

  • 适用场景:程序泄露了库函数地址,通过该地址可以计算出libc库的base(基址),从而构造指向libc的payload.

2.Ret2Shellcode(仅特定场景)

  • 原理:写入shellcode,计算机会执行机器码。
  • 适用场景:若程序存在 “可执行的数据区域”(如自定义的rwx权限段、未开启 NX 的部分区域),可将 shellcode 写入该区域,再通过漏洞控制返回地址跳转到 shellcode 执行。

3.Ret2DLOPEN/Ret2DLRESOLVE:动态链接利用

  • 原理:利用 ELF 的动态链接机制,通过构造dlopen(加载动态库)和dlsym(获取库中函数地址)的调用流程,动态加载libc并执行system函数,无需提前泄露libc基地址。

  • 适用场景:程序未开启 Full RELRO 保护(GOT 表可写),且无法直接泄露libc地址时。

二、ASLR(Address Space Layout Randomization):地址空间随机化

1. 保护原理

ASLR 是操作系统级的保护机制,其作用是在程序每次运行时,随机化 ELF 文件的代码段、数据段、堆、栈以及libc等动态库的加载地址。攻击者无法提前预测关键地址(如system函数、"/bin/sh"字符串地址),从而阻止 Ret2Libc 等依赖固定地址的利用方式。

2. 绕过思路

ASLR 的核心是 “地址随机”,绕过思路围绕 “获取随机地址的具体值” 或 “利用地址固定的区域” 展开:

1.信息泄露漏洞:获取关键地址

  • 原理:通过程序中的信息泄露漏洞(如格式化字符串漏洞、栈溢出导致的内存泄漏、UAF 漏洞),读取内存中已加载的关键地址(如 GOT 表中libc函数的真实地址、栈地址、堆地址),再根据地址偏移计算出目标函数(如system)或字符串(如"/bin/sh")的地址。

  • 常见泄露场景

    • 格式化字符串漏洞:通过%p输出栈上的libc地址或栈地址;

    • 栈溢出泄露:覆盖返回地址前,先泄露栈上保存的libc函数地址(如__libc_start_main的返回地址);

    • GOT 表泄露:通过漏洞读取 GOT 表中已解析的libc函数地址(如puts的 GOT 地址)。

2.地址爆破:小范围随机地址尝试

  • 原理:若 ASLR 随机化的地址空间较小(如仅低 12 位随机,对应 4KB 内存页),可通过循环发送 payload,尝试所有可能的地址组合,直到命中正确地址。

  • 适用场景

    • 栈地址随机化范围小(如仅栈基址低 12 位随机);

    • 程序可重复运行且无次数限制(如远程题目未限制连接次数)。

3.利用静态地址区域

  • 原理:若程序未开启 PIE 保护(代码段地址固定),则 ELF 自身的代码段、数据段地址固定,可利用这些区域中的 gadget(代码片段)或字符串构造利用链,无需依赖libc地址。

  • 适用场景:未开启 PIE 保护的程序,且程序自身存在可利用的 gadget 和字符串。

三、PIE(Position-Independent Executable):PIE偏移

1. 保护原理

PIE 是 ELF 文件级的保护机制,其作用是让 ELF 文件的代码段、数据段、BSS 段等在加载时使用相对地址,而非固定地址。结合 ASLR 后,程序每次运行时,整个 ELF 文件的加载基地址会随机化,导致攻击者无法提前预测代码段中的 gadget 地址、数据段中的字符串地址等。

2. 检测方法

  • 使用checksec,若输出中包含PIE: PIE enabled(或PIE: 0x400000等具体基址,代表部分随机),则开启 PIE;若为PIE: PIE disabled,则未开启。

3. 绕过思路

PIE 的核心是 “ELF 自身地址随机”,绕过思路与 ASLR 类似,需先获取 ELF 的加载基地址:

  • 泄露 ELF 基地址

    • 原理:通过漏洞泄露 ELF 代码段或数据段中的某个已知偏移地址(如main函数地址、某个全局变量地址),再根据该地址与 ELF 基地址的偏移,计算出 ELF 的加载基地址。

    • 示例:若泄露的main函数真实地址为0x55a3b7201120ida显示main的偏移为0x1120,则 ELF 基地址为0x55a3b7201120 - 0x1120 = 0x55a3b7200000

四、RELRO(Relocation Read-Only):重定位表只读保护

1. 保护原理

RELRO 通过限制 ELF 的 GOT(Global Offset Table,全局偏移表)和 PLT(Procedure Linkage Table,过程链接表)的可写性,防止攻击者通过修改 GOT 表来劫持函数执行流程(如将puts的 GOT 表项修改为system地址)。RELRO 分为两种级别:

保护级别 原理 防护强度
Partial RELRO(部分 RELRO) 仅将 GOT 表的部分区域设为只读,GOT 表的前半部分(.got)仍可写,后半部分(.got.plt)只读 较弱,可修改.got 区域
Full RELRO(完全 RELRO) 将整个 GOT 表设为只读,且在程序启动时提前解析所有动态链接函数(关闭延迟绑定),GOT 表无任何可写区域 较强,无法修改 GOT 表

2. 检测方法

  • 工具检测:使用checksec,若输出中包含RELRO: Full RELRO,则开启完全 RELRO;若为RELRO: Partial RELRO,则开启部分 RELRO;若为RELRO: No RELRO,则未开启。

3. 绕过思路

RELRO 的核心是 “GOT 表只读”,绕过思路需根据保护级别区分:

1.Partial RELRO:修改可写 GOT 区域

  • 原理:Partial RELRO 下,GOT 表的.got区域(存储已解析的函数地址)仍可写,攻击者可通过栈溢出、格式化字符串等漏洞修改该区域的函数地址(如将__libc_start_main的 GOT 地址修改为system地址),实现函数劫持。

  • 示例:通过格式化字符串漏洞,利用%n__libc_start_main的 GOT 表项修改为system地址,当程序再次调用__libc_start_main时,实际执行system

2.Full RELRO:放弃 GOT 表,寻找其他漏洞

  • 原理:Full RELRO 下 GOT 表完全只读,无法修改,因此需放弃 “GOT 表劫持” 思路,转而利用其他类型的漏洞或代码段中的 gadget:

    • 利用栈溢出 + Ret2Gadget:通过栈溢出控制返回地址,跳转到代码段或libc中的 gadget 组合(如pop retpop rdi ret),构造system("/bin/sh")的调用流程。

    • 利用堆漏洞:若程序存在堆漏洞(如 UAF、Double Free、Off-by-One),可通过篡改堆块元数据(如fdbk指针),实现任意地址写或控制程序执行流,无需依赖 GOT 表。

五、Stack Canary(栈金丝雀):栈溢出检测

1. 保护原理

Stack Canary(栈金丝雀)是一种栈溢出检测机制,其核心逻辑是:在程序启动时,生成一个随机的 “金丝雀” 值,存储在栈帧的 rbp下方(栈溢出的必经之路)。函数返回前,程序会检查金丝雀值是否被篡改,若被篡改(说明发生栈溢出),则立即终止程序

2. 检测方法

  • 工具检测:使用checksec,若输出中包含Canary: Canary found,则开启 Stack Canary;若为Canary: No canary found,则未开启。

3. 绕过思路

Stack Canary 的核心是 “随机值保护栈帧”,绕过思路围绕 “获取金丝雀值” 展开:

  • 泄露金丝雀值

    • 原理:通过漏洞(如格式化字符串漏洞、栈溢出导致的部分内存泄露)读取栈上存储的金丝雀值,在构造栈溢出 payload 时,将泄露的金丝雀值填充到对应的位置,避免触发__stack_chk_fail

    • 常见泄露场景

      • 格式化字符串漏洞:栈上的金丝雀值可通过%p直接输出(需确定金丝雀在栈上的偏移);
    • 注意事项:金丝雀值在程序每次运行时随机(结合 ASLR),因此需在同一次程序运行中完成 “泄露金丝雀 + 构造 payload 溢出”。

gdb调试

调试验证:通过gdb动态调试,获取关键地址,从而构造payload。

vmmap:查看进程的内存映射情况,获取各个段的地址信息。
reg rsp:查看当前栈顶指针的值。(system函数调用前会检查rsp向0x10对齐)