Skip to content

Commit

Permalink
Merge pull request #7 from reverseame/dev
Browse files Browse the repository at this point in the history
Major refactor, v1.0.0: closes #1, #2, #3, #4, #6
  • Loading branch information
duroz authored Oct 6, 2021
2 parents 42b9803 + ca2b406 commit f893c58
Show file tree
Hide file tree
Showing 66 changed files with 1,162 additions and 1,343 deletions.
82 changes: 41 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,61 +33,61 @@ Now, you can install dependencies in [requirements.txt](requirements.txt):
## Usage

```
usage: rop3.py [-h] [-v] [--depth <bytes>] [--all] [--nojop] [--noretf]
[--binary <file> [<file> ...]] [--base <ImageBase>]
[--badchar <hex> [<hex> ...]] [--ins <mnemonic>] [--op <op>]
[--dst <reg/imm>] [--src <reg/imm>] [--ropchain <file>]
usage: rop3.py [-h] [-v] [--depth <bytes>] [--all] [--nojop] [--noretf] [--nosides] [--silent]
[--binary <file> [<file> ...]] [--badchar <hex> [<hex> ...]] [--base <hex> [<hex> ...]]
[--op <op>] [--dst <reg>] [--src <reg>] [--ropchain <file>]
This tool allows you to search for gadgets, operations, and ROP chains using a
backtracking algorithm in a tree-like structure
This tool allows you to search for gadgets, operations, and ROP chains using a backtracking algorithm
in a tree-like structure
optional arguments:
-h, --help show this help message and exit
-v, --version display rop3.py's version and exit
--depth <bytes> depth for search engine (default to 5 bytes)
--all disables the removal of duplicate gadgets
--nojop disables JOP gadgets
--noretf disables gadgets terminated in a far return (retf)
--all show the same gadget in different addresses
--nojop do not search for JOP gadgets
--noretf do not search for gadgets terminated in a far return (retf)
--nosides eliminate gadgets with side-effects
--silent eliminate side-effects warnings
--binary <file> [<file> ...]
specify a list of binary path files to analyze
--base <ImageBase> specify a ImageBase address to relocate binary files (it may
take a while)
--badchar <hex> [<hex> ...]
specify a list of chars to avoid in gadget address
--ins <mnemonic> search for instruction mnemonic
--op <op> search for operation. Available: add, and, eqc, gfc, jmp,
lc, ld, lsd, ltc, mov, neg, not, or, spa, sps, st, sub, xor
--dst <reg/imm> specify a destination reg/imm to instruction/operation
--src <reg/imm> specify a source reg/imm to instruction/operation
--ropchain <file> plain text file with rop chains
--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
--op <op> search for operation. Available: add, and, eqc, gcf, jmp, lc, ld, lsd, ltc,
mov, neg, not, or, spa, sps, st, sub, xor
--dst <reg> specify a destination register for the operation
--src <reg> specify a source register for the operation
--ropchain <file> plain text file with a ROP chain
```

In the work that we presented in [15th IEEE Workshop on Offensive Technologies (WOOT21)](https://www.ieee-security.org/TC/SP2021/SPW2021/WOOT21/), we used rop3 to evaluate the executional power of Return Oriented Programming in a [subset of most common Windows DLLs](https://drive.google.com/file/d/1gOxUolzrw-xlaW6K-fhzZ7Z-sqxiaZeZ/view?usp=sharing>). Check the [paper](https://drive.google.com/file/d/1sPOmjqTmUfgm0iSSYJCvUAHfC10TNBAn/view) for further details.

```
$ python3 rop3.py --nojop --noretf --binary ~/dlls/win10x64/kernel32.dll --op mov --dst eax
[kernel32.dll @ 0x180007874]: mov eax, ebx ; ret
[kernel32.dll @ 0x180023dc2]: mov eax, ecx ; pop rbp ; ret
[kernel32.dll @ 0x180002258]: mov eax, ecx ; ret
[kernel32.dll @ 0x180003988]: mov eax, edx ; ret
[kernel32.dll @ 0x180003987]: mov eax, r10d ; ret
[kernel32.dll @ 0x180007873]: mov eax, r11d ; ret
[kernel32.dll @ 0x180006064]: mov eax, r8d ; ret
[kernel32.dll @ 0x180002257]: mov eax, r9d ; ret
[kernel32.dll @ 0x18000234d]: xchg eax, ebp ; ret
[kernel32.dll @ 0x18002ed9d]: xchg eax, ebp ; ret 0x1589
[kernel32.dll @ 0x180067d26]: xchg eax, ebp ; ret 0x1deb
[kernel32.dll @ 0x180014af2]: xchg eax, ebp ; ret 0xc283
[kernel32.dll @ 0x1800275ec]: xchg eax, ebp ; ret 0xe1e8
[kernel32.dll @ 0x18004bddf]: xchg eax, ebp ; ret 3
[kernel32.dll @ 0x180027849]: xchg eax, ebx ; ret
[kernel32.dll @ 0x180049aa9]: xchg eax, edx ; ret
[kernel32.dll @ 0x18005d83a]: xchg eax, edx ; ret 1
[kernel32.dll @ 0x18000222d]: xchg eax, esi ; ret
[kernel32.dll @ 0x18001dcf7]: xchg eax, esi ; ret 0x1389
[kernel32.dll @ 0x18000223a]: xchg eax, esi ; ret 0xfa83
[kernel32.dll @ 0x18001f642]: xchg eax, esp ; ret
[kernel32.dll @ 0x180011564]: xchg eax, esp ; ret 0xf741
```Shell
$ python3 rop3.py --nojop --noretf --binary ~/dlls/win10x86/SHELL32.dll --op mov --dst eax --src ecx
[SHELL32.dll @ 0x69b8a61b]: mov eax, ecx ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x698dc8c8]: mov eax, ecx ; pop ebx ; leave ; ret (x5) (modifies esp)
[SHELL32.dll @ 0x6991a2b1]: mov eax, ecx ; pop ebx ; ret (x4) (modifies esp)
[SHELL32.dll @ 0x6995e30b]: mov eax, ecx ; pop edi ; ret (x2) (modifies esp)
[SHELL32.dll @ 0x69c3c483]: mov eax, ecx ; pop esi ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x699670c1]: mov eax, ecx ; pop esi ; pop ebp ; ret (modifies esp)
[SHELL32.dll @ 0x6992d289]: mov eax, ecx ; pop esi ; ret (x11) (modifies esp)
[SHELL32.dll @ 0x698a474c]: mov eax, ecx ; ret (x97)
[SHELL32.dll @ 0x6991ea9b]: xchg eax, ecx ; add al, 0 ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x698b4804]: xchg eax, ecx ; add dword ptr fs:[eax], eax ; ret
[SHELL32.dll @ 0x69b0eae2]: xchg eax, ecx ; in eax, 0xff ; leave ; ret (modifies dst=eax, esp)
[SHELL32.dll @ 0x69c8eb40]: xchg eax, ecx ; int 0xff ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x69b2eade]: xchg eax, ecx ; jecxz 0x69b2eae0 ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x698965e0]: xchg eax, ecx ; mov al, 0x1c ; outsb dx, byte ptr [esi] ; ret
[SHELL32.dll @ 0x698dea7e]: xchg eax, ecx ; or byte ptr [eax], al ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x698feaa6]: xchg eax, ecx ; push es ; add cl, cl ; ret
[SHELL32.dll @ 0x6984d4c9]: xchg eax, ecx ; ret (x3)
[SHELL32.dll @ 0x69cd4cee]: xchg eax, ecx ; sahf ; ret
[SHELL32.dll @ 0x69c3eb09]: xchg eax, ecx ; sar bh, cl ; leave ; ret (modifies esp)
[SHELL32.dll @ 0x6995541d]: xchg eax, ecx ; test byte ptr [ecx - 0x75], ch ; ret
```

## Licence
Expand Down
52 changes: 19 additions & 33 deletions rop3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,31 @@
along with rop3. If not, see <https://www.gnu.org/licenses/>.
'''

import os
import sys

import rop3.args as args
import rop3.debug as debug
import rop3.args
import rop3.utils as utils
import rop3.gadfinder as finder
import rop3.ropchain as ropchain
import rop3.ropchain
import rop3.gadfinder as gadfinder

def main():
argis = args.parse_args(sys.argv[1:])
args = rop3.args.ArgumentParser().parse_args(sys.argv[1:])

if argis.version:
if args.version:
utils.show_version()
sys.exit(0)
elif argis.binary:
gadfinder = finder.Finder(depth=argis.depth, flags=argis.flags)

if argis.ropchain:
rop = ropchain.RopChain(gadfinder, argis.ropchain)
result = rop.search(argis.binary)

if result:
for i, chain in enumerate(result):
print('=' * 80)
print('Ropchain {0:n}'.format(i+1))
print('=' * 80)
for item in chain:
print(utils.format_op_ropchain(item['op']))
for gad in item['gads']:
print('\t{0}'.format(utils.format_op_gadget(gad)))
print('')
else:
debug.warning('Unable to find ropchain, consider to either change operations or add more libraries')
elif argis.op:
for gadgets in gadfinder.find_op_iter(argis.binary, op=argis.op, dst=argis.dst, src=argis.src, base=argis.base):
for gad in gadgets:
print(utils.format_op_gadget(gad))
elif args.binary:
finder = gadfinder.GadFinder(args.depth, args.flags)

if args.ropchain:
ropchain = rop3.ropchain.RopChain(finder)
result = ropchain.search(args.binary, args.ropchain, base=args.base, badchars=args.badchar)
utils.print_ropchains(result, args.silent, args.nosides)
elif args.op:
for filename, base in zip(args.binary, args.base):
for gadget in finder.find_op(filename, args.op, args.dst, args.src, base=base, badchars=args.badchar):
utils.print_gadget(gadget, args.silent, args.nosides)
else:
for gadgets in gadfinder.find_iter(argis.binary, base=argis.base):
for gad in gadgets:
print(utils.format_gadget(gad))
for filename, base in zip(args.binary, args.base):
for gadget in finder.find(filename, base=base, badchars=args.badchar):
utils.print_gadget(gadget)
158 changes: 98 additions & 60 deletions rop3/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,71 +15,109 @@
along with rop3. If not, see <https://www.gnu.org/licenses/>.
'''

import os
import argparse

import rop3.debug as debug
import rop3.utils as utils
import rop3.parser as parser
import rop3.gadfinder as gadfinder
import rop3.template as template

def parse_args(arguments):
description = 'This tool allows you to search gadgets, operations formed by gadgets and generate automatic ROP chains in Portable Executable (PE)'
parser = argparse.ArgumentParser(description=description)

parser.add_argument('-v', '--version', action='store_true', help='display {0}\'s version and exit'.format(utils.TOOL_NAME))
parser.add_argument('--depth', type=int, metavar='<bytes>', default=gadfinder.DEPTH, help='depth for search engine (default to {0} bytes)'.format(gadfinder.DEPTH))
parser.add_argument('--all', action='store_true', help='disables the removal of duplicate gadgets')
parser.add_argument('--nojop', action='store_true', help='disables JOP gadgets')
parser.add_argument('--noretf', action='store_true', help='disables gadgets terminated in a far return (retf)')
parser.add_argument('--binary', type=str, metavar='<file>', nargs='+', help='specify a list of binary path files to analyze')
parser.add_argument('--base', type=str, metavar='<ImageBase>', default='', help='specify a ImageBase address to relocate binary files (it may take a while)')
ops = ', '.join(sorted(utils.get_ops().keys()))
parser.add_argument('--op', type=str, metavar='<op>', help='search for operation. Available: {{{0}}}'.format(ops))
parser.add_argument('--dst', type=str, metavar='<reg/imm>', default='', help='specify a destination reg/imm to operation')
parser.add_argument('--src', type=str, metavar='<reg/imm>', default='', help='specify a source reg/imm to operation')
# TODO
# parser.add_argument('--fit', action='store_true', help='search smallest operation gadgets based in number of bytes')
parser.add_argument('--ropchain', type=str, metavar='<file>', help='plain text file with rop chains')

args = parser.parse_args(arguments)
args = _convert_flags(args)

check_args(args)

return args

def check_args(args):
if not (args.version or args.binary):
debug.error('You need to provide a binary (--binary or --help)')

if args.op:
try:
template.TemplateOp(args.op)
except template.OperationError as exc:
debug.error(str(exc))

if (args.dst or args.src) and not args.op:
debug.error('You need to provide an operation to specify a register (--op or --help)')

if args.base:
class ArgumentParser:
def __init__(self):
self.parser = parser.Parser()
description = 'This tool allows you to search for gadgets, operations, and ROP chains using a backtracking algorithm in a tree-like structure'
self.argparser = argparse.ArgumentParser(description=description)
self.argparser.add_argument('-v', '--version', action='store_true', help=f'display {utils.TOOL_NAME}\'s version and exit')
self.argparser.add_argument('--depth', type=int, metavar='<bytes>', default=gadfinder.DEPTH, help=f'depth for search engine (default to {gadfinder.DEPTH} bytes)')
self.argparser.add_argument('--all', action='store_true', help='show the same gadget in different addresses')
self.argparser.add_argument('--nojop', action='store_true', help='do not search for JOP gadgets')
self.argparser.add_argument('--noretf', action='store_true', help='do not search for gadgets terminated in a far return (retf)')
self.argparser.add_argument('--nosides', action='store_true', help='eliminate gadgets with side-effects')
self.argparser.add_argument('--silent', action='store_true', help='eliminate side-effects warnings')
# self.argparser.add_argument('--verbose', action='store_true', help='full description of side-effects warnings')
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('--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('--ins', type=str, metavar='<mnemonic>', help='search for an instruction mnemonic')
self.argparser.add_argument('--op', type=str, metavar='<op>', help=f'search for operation. Available: {", ".join(sorted([op.name for op in self.parser.get_ops()]))}')
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')
self.argparser.add_argument('--ropchain', type=str, metavar='<file>', help='plain text file with a ROP chain')

def parse_args(self, arguments):
args = self.argparser.parse_args(arguments)

self._check_args(args)

args = self._convert_flags(args)
args = self._convert_base(args)

return args

def _convert_flags(self, args):
'''
Transform user provided options to bit flags
'''
namespace = vars(args)
flags = 0

if args.all:
flags |= gadfinder.KEEP_DUPLICATES
if args.nojop:
flags |= gadfinder.NO_JOP
if args.noretf:
flags |= gadfinder.NO_RETF

namespace['flags'] = flags

return args

def _convert_base(self, args):
''' Replicate one base for all binaries '''
if not args.version:
base_addresses = args.base
if not base_addresses: # None
base_addresses = [args.base] * len(args.binary)
elif len(args.base) == 1: # Just one base
base_addresses = args.base * len(args.binary)

args.base = base_addresses

return args

def _check_args(self, args):
if not (args.version or args.binary):
debug.error('You need to provide a binary (--binary or --help)')

if args.base:
if len(args.binary) != len(args.base):
if len(args.base) != 1:
debug.error(f'Number of binaries ({len(args.binary)}) does not match number of addresses ({len(args.base)}) (--help)')

for baddr in args.base:
self._check_int_value(baddr)

if args.badchar:
for badchar in args.badchar:
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)')

if args.op:
try:
self.parser.get_op(args.op)
except parser.ParserException:
debug.error(f'{args.op}: Operation not found (--help)')

if args.ropchain:
ropchain_filename = os.path.abspath(args.ropchain)
if not os.path.isfile(ropchain_filename):
debug.error(f'{ropchain_filename}: File not found (--help)')

def _check_int_value(self, value):
try:
base = int(args.base, 0)
namespace = vars(args)
namespace['base'] = base
''' With base 0, it tries to infer the integer type based in prefix '''
return int(value, 0)
except ValueError:
debug.error('\'{0}\': Unable to infer number value, try to use prefix (v.g. \'0xCACAFEA\')'.format(args.base))

def _convert_flags(args):
namespace = vars(args)
flags = 0

if args.all:
flags |= gadfinder.SKIP_DUPLICATES
if args.nojop:
flags |= gadfinder.NO_JOP
if args.noretf:
flags |= gadfinder.NO_RETF

namespace['flags'] = flags

return args
debug.error(f'{value}: value not recognized')
Loading

0 comments on commit f893c58

Please sign in to comment.