The MemWriter class builds ROP chains that write arbitrary data to memory addresses. It handles badbytes, chunk optimization, and uses memory arithmetic operations when needed.
Overview
Accessed through the ROP instance as rop.write_to_mem(), MemWriter automatically:
- Finds optimal memory write gadgets
- Handles data with badbytes using arithmetic transformations
- Writes data in efficient chunks (1, 2, 4, or 8 bytes)
- Manages address and data register dependencies
Class Definition
Located in angrop/chain_builder/mem_writer.py
Public Method
write_to_mem
write_to_mem(addr, data, preserve_regs=None, fill_byte=b"\xff") -> RopChain
Builds a ROP chain that writes data to a memory address.
Target memory address where data will be written.
Data to write (must be a bytes object).
Set of register names that must not be modified.
Single byte used to pad data if necessary. Must not be a badbyte.
Returns: A RopChain that writes the data to memory.
Raises:
RopException if data cannot be written
RopException if fill_byte is a badbyte
RopException if addr is symbolic
ROP Instance Method
When you call rop.write_to_mem(), it invokes MemWriter.write_to_mem() internally:
rop.write_to_mem(addr, data, preserve_regs=None, fill_byte=b"\xff")
Implementation Details
Memory Write Gadgets
MemWriter requires gadgets with specific properties:
- Self-contained: No dependencies on initial state
- Single memory write: Only one symbolic write operation
- Independent addr/data: Address and data controlled by different registers
From source code (mem_writer.py:358-375):
def _get_all_mem_write_gadgets(gadgets):
possible_gadgets = set()
for g in gadgets:
if not g.self_contained:
continue
if len(g.mem_writes) != 1:
continue
for m_access in g.mem_writes:
if (m_access.addr_controllable() and
m_access.data_controllable() and
m_access.addr_data_independent()):
possible_gadgets.add(g)
return possible_gadgets
Gadget Examples
; Good memory write gadgets
mov [rax], rbx; ret ; addr=rax, data=rbx, independent
mov qword ptr [rdi], rsi; ret
mov dword ptr [rcx], edx; ret
; Bad gadgets
mov [rax], rax; ret ; addr and data not independent
mov [rax], 0x42; ret ; data not controllable
Badbyte Handling
When data contains badbytes, MemWriter uses multiple strategies:
Transforms safe bytes into target bytes using arithmetic:
From source code (mem_writer.py:213-250):
def _find_chunk_transforms(target_bytes, badbytes, preferred_init):
ops = ("xor", "or", "and", "add")
for op in ops:
init_bytes = []
arg_bytes = []
for tb in target_bytes:
solved = _solve_byte_pair(tb, op, badbytes, preferred_init)
if solved is None:
break
ib, ab = solved
init_bytes.append(ib)
arg_bytes.append(ab)
if found:
# Write init_bytes, then apply op with arg
yield (init_blob, op, arg_val)
Example:
# To write 0x0a (badbyte):
# Method 1: Write 0x0b, then subtract 1
# Method 2: Write 0x00, then OR 0x0a (if 0x00 is safe)
# Method 3: Write 0xff, then AND 0x0a
2. Per-Byte Operations
Handles each byte with different operations:
# Byte 0: XOR 0x41 with 0x4b -> 0x0a
# Byte 1: ADD 0x00 with 0x20 -> 0x20
# Byte 2: OR 0x00 with 0xff -> 0xff
3. Write Plans
From source code (mem_writer.py:607-629):
def _badbyte_mem_write_plans(data_size):
"""Generate plans using different chunk sizes"""
chunk_sizes = (1, 2, 4, 8)
def dfs(plans, total):
if total >= data_size:
yield (total, list(plans))
return
for cz in chunk_sizes:
plans.append((total, cz))
yield from dfs(plans, total+cz)
plans.pop()
MemWriteChain Caching
MemWriter caches chain templates for efficiency:
From source code (mem_writer.py:18-132):
class MemWriteChain:
"""Cached memory writing chain template"""
def __init__(self, builder, gadget, preserve_regs):
# Build template with symbolic addr/data
self.addr_bv = claripy.BVS("addr", mem_write.addr_size)
self.data_bv = claripy.BVS("data", mem_write.data_size)
self.chain = self._build_chain()
def concretize(self, addr_val, data):
# Replace symbolic values with concrete ones
chain = self.chain.copy()
# ... replace addr_bv and data_bv ...
return chain
Usage Examples
Basic Memory Write
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.find_gadgets()
# Write string to memory
chain = rop.write_to_mem(0x61b100, b"/bin/sh\x00")
chain.pp()
Writing Binary Data
# Write shellcode
shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff"
chain = rop.write_to_mem(0x61b000, shellcode)
Writing with Register Preservation
# Write data without modifying rax
chain = rop.write_to_mem(0x61b100, b"data", preserve_regs={'rax'})
Building Execve Chain
# Write "/bin/sh" to memory
chain = rop.write_to_mem(0x61b100, b"/bin/sh\x00")
# Call execve with the string address
chain += rop.func_call("execve", [0x61b100, 0, 0])
# Or use convenience method
chain = rop.execve() # Automatically writes and calls
Writing File Paths
import os
# Write file path
chain = rop.write_to_mem(0x61b100, b"/home/ctf/flag\x00")
# Open file
chain += rop.func_call("open", [0x61b100, os.O_RDONLY])
# Read into buffer
chain += rop.func_call("read", [3, 0x61b200, 0x100])
# Write to stdout
chain += rop.func_call("write", [1, 0x61b200, 0x100])
Writing with Badbytes
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.set_badbytes([0x00, 0x0a])
rop.find_gadgets()
# Data contains badbytes
data = b"\x00\x0a\x0d\xff"
chain = rop.write_to_mem(0x61b100, data)
# MemWriter automatically:
# 1. Writes safe initial values
# 2. Uses mem_xor/mem_add/etc to construct target bytes
Building Data Structures
import struct
struct_addr = 0x61b100
# Write struct fields
chain = rop.write_to_mem(struct_addr + 0, struct.pack('<Q', 0x1234))
chain += rop.write_to_mem(struct_addr + 8, struct.pack('<Q', 0x5678))
chain += rop.write_to_mem(struct_addr + 16, b"name\x00")
Writing Pointer Arrays
# Build argv array for execve
argv_addr = 0x61b200
arg0_addr = 0x61b100
# Write the string
chain = rop.write_to_mem(arg0_addr, b"/bin/sh\x00")
# Write pointer array
chain += rop.write_to_mem(argv_addr, struct.pack('<Q', arg0_addr))
chain += rop.write_to_mem(argv_addr + 8, struct.pack('<Q', 0)) # NULL
# Call execve
chain += rop.func_call("execve", [arg0_addr, argv_addr, 0])
Write Size Optimization
MemWriter automatically chooses optimal chunk sizes:
# For 8-byte data:
mov qword ptr [rax], rbx; ret # 1 write of 8 bytes
# For 3-byte data:
mov word ptr [rax], bx; ret # 1 write of 2 bytes
mov byte ptr [rax+2], bl; ret # 1 write of 1 byte
# Prefers larger writes for efficiency
Error Handling
”Fail to write data to memory :(”
Raised when no suitable gadgets are found.
Solutions:
- Use
fast_mode=False when initializing ROP
- Check if binary has memory write gadgets
- Try alternative addresses or data
”fill_byte is a bad byte!”
Raised when fill_byte contains a badbyte.
Solution: Choose a different fill_byte that’s not in badbytes.
”cannot write to a symbolic address”
Raised when addr parameter is symbolic.
Solution: Use a concrete address value.
”data is not a byte string”
Raised when data is not bytes type.
Solution: Convert to bytes: data.encode() or bytes([...])
Gadget Requirements
For memory writes to work:
- Controllable address: Can set address register to any value
- Controllable data: Can set data register to any value
- Independence: Address and data registers are different
- Self-contained: Gadget doesn’t require special initial state
Example verification from source code (mem_writer.py:413-422):
mem_write = gadget.mem_writes[0]
dep_regs = mem_write.addr_dependencies | mem_write.data_dependencies
if not dep_regs.issubset(can_set_regs):
# Can't set required registers
continue
- Writing large data may generate long chains
- Badbyte handling adds overhead (arithmetic operations)
- Caching reduces overhead for repeated writes
- Chunk size optimization minimizes total gadgets used
See Also