Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Now, you can install dependencies in [requirements.txt](requirements.txt):

```
usage: rop3.py [-h] [-v] [--depth <bytes>] [--all] [--rop | --no-rop] [--retf | --no-retf] [--jop | --no-jop] [--allow-undeterministic-gadgets] [--allow-complex-memory-ops] [--verbose]
[--binary <file> [<file> ...]] [--badchar <hex> [<hex> ...]] [--keep-canary-address] [--base <hex> [<hex> ...]] [--op <op>] [--dst <reg>] [--src <reg>] [--ropchain <file>]
[--exhaustive | --no-exhaustive]
[--binary <file> [<file> ...]] [--badchar <hex> [<hex> ...]] [--badchar-bytes <hex> [<hex> ...]] [--keep-canary-address] [--base <hex> [<hex> ...]] [--arch <name>] [--symbols]
[--output {text,json,csv}] [--op <op>] [--dst <reg>] [--src <reg>] [--ropchain <file>] [--exhaustive | --no-exhaustive]

This tool allows you to search for gadgets, operations, and ROP chains using a backtracking algorithm in a tree-like structure

Expand All @@ -56,10 +56,16 @@ options:
specify a list of binary path files to analyze
--badchar <hex> [<hex> ...]
specify a list of chars to avoid in gadget address
--badchar-bytes <hex> [<hex> ...]
specify a list of chars to avoid in gadget opcode bytes
--keep-canary-address
do not prefer canary-free addresses (0x00, 0x0a, 0x0d, 0xff) when discarding duplicate gadgets
--base <hex> [<hex> ...]
specify a base address to relocate binary files (it may take a while). When you specify more than one base address, you need to provide one address for each binary
--arch <name> select the architecture slice of a fat Mach-O binary (e.g. x86_64, i386)
--symbols annotate gadgets with the nearest symbol (when the binary is not stripped)
--output {text,json,csv}
output format (default: text)
--op <op> search for operation
--dst <reg> specify a destination register for the operation
--src <reg> specify a source register for the operation
Expand Down
35 changes: 20 additions & 15 deletions rop3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,30 @@ def main():
if args.ropchain:
ropchain = rop3.ropchain.RopChain(finder)
result = ropchain.search_from_files(args.binary, args.ropchain,
base=args.base, badchars=args.badchar)
if args.exhaustive:
for idx, x in enumerate(result, 1):
utils.print_ropchain(x, idx)
else:
utils.print_ropchain(next(iter(result)))
base=args.base, badchars=args.badchar,
badchar_bytes=args.badchar_bytes,
arch=args.arch, symbols=args.symbols)
utils.output_ropchains(result, args.output, exhaustive=args.exhaustive)
elif args.op:

result = finder.find_op(args.binary, args.op, args.dst, args.src, base=args.base, badchars=args.badchar)
result = finder.find_op(args.binary, args.op, args.dst, args.src,
base=args.base, badchars=args.badchar,
badchar_bytes=args.badchar_bytes,
arch=args.arch, symbols=args.symbols)
if result and isinstance(result[0], list):
for chain in result:
for gadget in chain:
utils.print_gadget(gadget)
''' Composite operation: a list of chains '''
if args.output == 'text':
for chain in result:
for gadget in chain:
utils.print_gadget(gadget)
else:
utils.output_ropchains(result, args.output, exhaustive=True)
else:
for gadget in result:
utils.print_gadget(gadget)
utils.output_gadgets(result, args.output)
else:
for gadget in finder.find(args.binary, base=args.base, badchars=args.badchar):
utils.print_gadget(gadget)
gadgets = finder.find(args.binary, base=args.base, badchars=args.badchar,
badchar_bytes=args.badchar_bytes,
arch=args.arch, symbols=args.symbols)
utils.output_gadgets(gadgets, args.output)
except parser.ParserException as exc:
debug.error(str(exc))
except rop3.ropchain.RopChainNotFound as exc:
Expand Down
10 changes: 8 additions & 2 deletions rop3/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ def __init__(self):
self.argparser.add_argument('--verbose', action='store_true', default=False, help='show progress information (gadget counts, combinations)')
self.argparser.add_argument('--binary', type=str, metavar='<file>', nargs='+', help='specify a list of binary path files to analyze')
self.argparser.add_argument('--badchar', type=str, metavar='<hex>', nargs='+', help='specify a list of chars to avoid in gadget address')
self.argparser.add_argument('--badchar-bytes', type=str, metavar='<hex>', nargs='+', help='specify a list of chars to avoid in gadget opcode bytes')
self.argparser.add_argument('--keep-canary-address', action='store_true', default=False, help='do not prefer canary-free addresses (0x00, 0x0a, 0x0d, 0xff) when discarding duplicate gadgets')
self.argparser.add_argument('--base', type=str, metavar='<hex>', nargs='+', help='specify a base address to relocate binary files (it may take a while). When you specify more than one base address, you need to provide one address for each binary')
self.argparser.add_argument('--arch', type=str, metavar='<name>', default=None, help='select the architecture slice of a fat Mach-O binary (e.g. x86_64, i386)')
self.argparser.add_argument('--symbols', action='store_true', default=False, help='annotate gadgets with the nearest symbol (when the binary is not stripped)')
self.argparser.add_argument('--output', choices=['text', 'json', 'csv'], default='text', help='output format (default: text)')
self.argparser.add_argument('--op', type=str, metavar='<op>', help='search for operation')
self.argparser.add_argument('--dst', type=str, metavar='<reg>', help='specify a destination register for the operation')
self.argparser.add_argument('--src', type=str, metavar='<reg>', help='specify a source register for the operation')
Expand Down Expand Up @@ -108,8 +112,10 @@ def _check_args(self, args):
for baddr in args.base:
self._check_int_value(baddr)

if args.badchar:
for badchar in args.badchar:
for option in (args.badchar, args.badchar_bytes):
if not option:
continue
for badchar in option:
value = self._check_int_value(badchar)
if value < 0x00 or value > 0xff:
debug.error(f'{badchar}: bad char must be one byte (range 0x00-0xff)')
Expand Down
14 changes: 14 additions & 0 deletions rop3/binaries/elf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io

from elftools.elf.elffile import ELFFile, ELFError
from elftools.elf.sections import SymbolTableSection

import rop3.binary as binary

Expand Down Expand Up @@ -74,6 +75,19 @@ def get_exec_sections(self):
})
return ret

def get_symbols(self):
''' Function/object symbols from .symtab and .dynsym, rebased by the
same delta as the sections. Stripped binaries yield none. '''
ret = []
for sec in self._elf.iter_sections():
if not isinstance(sec, SymbolTableSection):
continue
for sym in sec.iter_symbols():
addr = sym['st_value']
if sym.name and addr:
ret.append((addr + self._base_delta, sym.name))
return ret

def get_arch(self):
return self._arch

91 changes: 73 additions & 18 deletions rop3/binaries/macho.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@

import capstone
import io
import struct

from macholib.MachO import MachO as _MachO
from macholib.mach_o import (
LC_SEGMENT, LC_SEGMENT_64,
CPU_TYPE_NAMES,
LC_SEGMENT, LC_SEGMENT_64, LC_SYMTAB,
CPU_TYPE_NAMES, N_STAB,
S_ATTR_PURE_INSTRUCTIONS, S_ATTR_SOME_INSTRUCTIONS
)

Expand All @@ -32,8 +33,14 @@
VM_PROT_EXECUTE = 0x04
S_INSTRUCTION_ATTRS = S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS

# Mach-O architecture name -> (rop3 architecture class)
SUPPORTED_ARCHS = {
'x86_64': X64_Architecture,
'i386': X86_Architecture,
}

class MachO:
def __init__(self, data, base):
def __init__(self, data, base, arch=None):
# Dirty way to initialize the class, since it only supports reading
# from a filename
try:
Expand All @@ -46,24 +53,42 @@ def __init__(self, data, base):
self._macho.load(self._file)
except Exception as exc:
raise binary.BinaryException(str(exc)) from exc
# Fat binaries carry several headers; pick the first supported slice
# (do not commit to a header until its architecture is recognised)
self._header = None
self._arch = None
for header in self._macho.headers:
arch = CPU_TYPE_NAMES.get(header.header.cputype)
if arch == "x86_64":
self._header, self._arch = header, X64_Architecture()
break
elif arch == "i386":
self._header, self._arch = header, X86_Architecture()
break
if not self._header:
raise binary.BinaryException(
'Mach-O: No supported architectures were found')

self._header, self._arch = self._select_slice(arch)

self._base_delta = self._base_delta_for(base)

def _select_slice(self, arch):
'''
Fat binaries carry several headers. With --arch, pick that slice; else
pick the first supported slice in file order. Do not commit to a header
until its architecture is recognized.
'''
available = {} # arch name -> header (first occurrence)
for header in self._macho.headers:
name = CPU_TYPE_NAMES.get(header.header.cputype)
if name is not None:
available.setdefault(name, header)

if arch is not None:
if arch not in SUPPORTED_ARCHS:
raise binary.BinaryException(
f'Mach-O: unsupported --arch {arch} '
f'(choose from {", ".join(sorted(SUPPORTED_ARCHS))})')
if arch not in available:
present = ", ".join(sorted(available)) or "none"
raise binary.BinaryException(
f'Mach-O: arch {arch} not present in binary (available: {present})')
return available[arch], SUPPORTED_ARCHS[arch]()

for header in self._macho.headers:
name = CPU_TYPE_NAMES.get(header.header.cputype)
if name in SUPPORTED_ARCHS:
return header, SUPPORTED_ARCHS[name]()

raise binary.BinaryException(
'Mach-O: No supported architectures were found')

def _image_base(self):
''' Link-time base of the image: the __TEXT segment vmaddr (fall back
to the lowest segment vmaddr) '''
Expand Down Expand Up @@ -102,6 +127,36 @@ def get_exec_sections(self):
})
return ret

def get_symbols(self):
''' Symbols from the LC_SYMTAB symbol table, rebased by the same delta
as the sections. macholib does not expand the nlist array, so it is
parsed here from the file. Stripped binaries yield none. '''
ret = []
is64 = self._arch.mode == capstone.CS_MODE_64
entry_fmt = '<IBBHQ' if is64 else '<IBBhI' # nlist_64 / nlist
entry_size = struct.calcsize(entry_fmt)
slice_off = self._header.offset

for (lc, cmd, data) in self._header.commands:
if lc.cmd != LC_SYMTAB:
continue
self._file.seek(slice_off + cmd.stroff)
strtab = self._file.read(cmd.strsize)
self._file.seek(slice_off + cmd.symoff)
entries = self._file.read(cmd.nsyms * entry_size)
for i in range(cmd.nsyms):
n_strx, n_type, n_sect, n_desc, n_value = struct.unpack_from(
entry_fmt, entries, i * entry_size)
if n_type & N_STAB: # debug (STABS) entry
continue
if not n_value: # undefined / no address
continue
end = strtab.find(b'\x00', n_strx)
name = strtab[n_strx:end].decode('utf-8', 'replace')
if name:
ret.append((n_value + self._base_delta, name))
return ret

def get_arch(self):
return self._arch

16 changes: 16 additions & 0 deletions rop3/binaries/pe.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ def get_exec_sections(self):
})
return ret

def get_symbols(self):
''' Best-effort symbols from the export table (names + ordinals),
relative to the (possibly relocated) image base. '''
ret = []
self._pe.parse_data_directories()
export_dir = getattr(self._pe, 'DIRECTORY_ENTRY_EXPORT', None)
if export_dir is None:
return ret
image_base = self._pe.OPTIONAL_HEADER.ImageBase
for exp in export_dir.symbols:
if exp.address:
name = exp.name.decode('utf-8', 'replace') if exp.name \
else f'ordinal_{exp.ordinal}'
ret.append((image_base + exp.address, name))
return ret

def get_arch(self):
return self._arch

16 changes: 12 additions & 4 deletions rop3/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ class Binary:
'''
Interface to access binary file details
'''
def __init__(self, filename, base):
def __init__(self, filename, base, arch=None):
self.filename = os.path.realpath(filename)
self.raw_data = self._read_binary()
self._binary = self._load_binary(base)
self._binary = self._load_binary(base, arch)

def _read_binary(self):
try:
Expand All @@ -38,7 +38,7 @@ def _read_binary(self):
except (IOError, FileNotFoundError):
debug.error(f'{self.filename}: Unable to read file')

def _load_binary(self, base):
def _load_binary(self, base, arch=None):
# MS-DOS Stub (PE)
if self.raw_data[:2] == b'\x4d\x5a': # MZ
return pe.PE(self.raw_data, base)
Expand All @@ -53,7 +53,7 @@ def _load_binary(self, base):
b'\xfe\xed\xfa\xcf', b'\xcf\xfa\xed\xfe', # 64-bit
b'\xca\xfe\xba\xbe', # Fat header
]:
return macho.MachO(self.raw_data, base)
return macho.MachO(self.raw_data, base, arch)

else:
raise BinaryException(f'{self.filename}: Format file not supported')
Expand All @@ -71,5 +71,13 @@ def get_arch(self):
'''
return self._binary.get_arch()

def get_symbols(self):
'''
@returns a list of (address, name) tuples for the binary's symbols,
or an empty list when the format/loader has none (e.g. stripped)
'''
getter = getattr(self._binary, 'get_symbols', None)
return getter() if getter else []

class BinaryException(Exception):
pass
Loading
Loading