├── .github └── workflows │ ├── publish.yml │ ├── python-app.yml │ └── test_master.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── angrcli ├── __init__.py ├── ast │ ├── __init__.py │ └── rendering.py ├── full │ └── __init__.py ├── interaction │ ├── __init__.py │ └── explore.py ├── plugins │ ├── ContextView │ │ ├── __init__.py │ │ ├── colors.py │ │ ├── context_view.py │ │ └── disassemblers.py │ ├── __init__.py │ ├── explore.py │ └── watches.py └── py.typed ├── example ├── .gdb_history ├── .zshrc ├── MarsAnalytica ├── mars_auto.py ├── mars_state1 ├── mars_state2 ├── morph ├── morph_auto.py └── morph_cli.py ├── images ├── ast_rendering.png └── context_view_demo.png ├── setup.py └── tests ├── base.py ├── simproc_demo.elf ├── sym_exec.elf ├── test_code_printing.py ├── test_derefs.py ├── test_interactive_explore.py ├── test_morph.py └── test_simprocs.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Tagged Commit to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | 9 | jobs: 10 | build-n-publish: 11 | name: Build and publish Python Package 12 | runs-on: ubuntu-18.04 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Set up Python 3.10 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: "3.10" 19 | 20 | 21 | - name: Install pypa/build 22 | run: >- 23 | python -m 24 | pip install 25 | build 26 | --user 27 | - name: Build a binary wheel and a source tarball 28 | run: >- 29 | python -m 30 | build 31 | --sdist 32 | --wheel 33 | --outdir dist/ 34 | 35 | 36 | - name: Publish distribution 📦 to PyPI 37 | uses: pypa/gh-action-pypi-publish@master 38 | with: 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | 41 | 42 | - name: Release on GitHub 43 | uses: softprops/action-gh-release@v1 44 | with: 45 | files: ./dist/* 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests with angr from PyPI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: "0 6 * * 1" 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python 3.10 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: "3.10" 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 pytest 31 | pip install ./ 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /.github/workflows/test_master.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests with angr docker container 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: "0 6 * * *" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup Python 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: "3.10" 25 | - name: Install angr 26 | shell: bash 27 | run: | 28 | which python 29 | pip install -U pip wheel setuptools pyinstaller unicorn==2.0.1 30 | pip install git+https://github.com/eliben/pyelftools#egg=pyelftools 31 | pip install git+https://github.com/angr/archinfo.git#egg=archinfo 32 | pip install git+https://github.com/angr/pyvex.git#egg=pyvex 33 | pip install git+https://github.com/angr/cle.git#egg=cle 34 | pip install git+https://github.com/angr/claripy.git#egg=claripy 35 | pip install git+https://github.com/angr/ailment.git#egg=ailment 36 | pip install --no-build-isolation git+https://github.com/angr/angr.git#egg=angr 37 | - name: Install angr-cli 38 | shell: bash 39 | run: | 40 | pip install pytest 41 | pip install ./ 42 | # su --login - angr -c "cd /home/angr/angr-dev; source /home/angr/.virtualenvs/angr/bin/activate; echo 'N' | ./extremely-simple-setup.sh" 43 | - name: Test with pytest 44 | run: | 45 | pytest $PWD 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Florian Magin, Alexander Druffel 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude * __pycache__ 2 | recursive-exclude tests * 3 | recursive-exclude example * 4 | include LICENSE README.md requirements.txt 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angr CLI 2 | 3 | 4 | ![Tests with angr from PyPI](https://github.com/fmagin/angr-cli/workflows/Tests%20with%20angr%20from%20PyPI/badge.svg) 5 | 6 | This Python package is a collection of modules to allow easier interactive use of angr for learning and prototyping angr. 7 | 8 | All features are designed for the use of angr in an interactive environment like an IPython shell or a Jupyter environment (both CLI and Notebook), but some will still work in a simple Python shell or script. 9 | 10 | Using a font that supports Ligatures like [JetBrains Mono](https://www.jetbrains.com/lp/mono/) is recommended to make 11 | the output more pleasant to read. 12 | 13 | ## Install 14 | 15 | ### PyPi 16 | 17 | A stable version is available on PyPi. 18 | ```sh 19 | pip install angrcli 20 | ``` 21 | 22 | ### Dev 23 | 24 | In case you want a development install of this, run this in a folder of your choice (e.g. your `angr-dev` repo) after activating your angr virtual environment 25 | 26 | ```sh 27 | git clone https://github.com/fmagin/angr-cli.git 28 | cd angr-cli 29 | pip install -e ./ 30 | ``` 31 | 32 | ## General Usage 33 | 34 | To import and setup all features: 35 | 36 | ```python 37 | import angrcli.full 38 | ``` 39 | 40 | This will take care of importing and registering the plugins. 41 | 42 | ## Core Features 43 | 44 | ### State View Plugin 45 | 46 | The Context View plugin allows rendering of a state in a view similiar to that provided by GDB plugins like GEF or pwndbg. 47 | 48 | 49 | #### Usage 50 | 51 | ```python 52 | import angr 53 | # This line registers the plugin and makes it available on each state 54 | import angrcli.plugins.ContextView 55 | proj = angr.Project("/bin/ls", load_options={"auto_load_libs":False}) 56 | state = proj.factory.entry_state() 57 | 58 | # Print the state 59 | state.context_view.pprint() 60 | ``` 61 | ![Context View](./images/context_view_demo.png) 62 | 63 | 64 | ### Interactive Exploration 65 | 66 | The Interactive Exploration is a [Python CMD](https://pymotw.com/2/cmd/) wrapper around a Simulation Manager that provides shortcuts for various common operations, like stepping blocks, running until a symbolic branch or manually selecting successors. 67 | 68 | This can either be used in a script, or inside an IPython shell. The latter allows rapid switching between the wrapper to access the shortcuts and the IPython shell for more complex operations. 69 | 70 | 71 | #### Usage 72 | ```python 73 | import angr 74 | import angrcli.plugins.ContextView 75 | from angrcli.interaction.explore import ExploreInteractive 76 | proj = angr.Project("/bin/ls", load_options={"auto_load_libs":False}) 77 | state = proj.factory.entry_state() 78 | # For quick but less flexible access (state isn't modified) 79 | state.explore() 80 | 81 | # state.explore() basically just does the following on each call 82 | e = ExploreInteractive(proj, state) 83 | e.cmdloop() 84 | 85 | ``` 86 | 87 | #### Demo 88 | 89 | [![asciicast](https://asciinema.org/a/256289.svg)](https://asciinema.org/a/256289) 90 | 91 | ## Misc 92 | 93 | ### AST Preview 94 | 95 | `angrcli.ast.rendering` provides `render_ast` which uses graphviz to generate a SVG representation of an AST which can be displayed instead of the `__repr__` method of the AST object. 96 | 97 | #### Example 98 | 99 | 100 | ```python 101 | import claripy 102 | from angrcli.ast.rendering import render_ast 103 | from claripy.ast.bv import BV 104 | BV._repr_svg_ = lambda self: render_ast(self)._repr_svg_() 105 | x = claripy.BVS('x', 32) 106 | y = claripy.BVS('y', 32) 107 | ``` 108 | 109 | ![AST Rendering](./images/ast_rendering.png) 110 | -------------------------------------------------------------------------------- /angrcli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/angrcli/__init__.py -------------------------------------------------------------------------------- /angrcli/ast/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/angrcli/ast/__init__.py -------------------------------------------------------------------------------- /angrcli/ast/rendering.py: -------------------------------------------------------------------------------- 1 | from graphviz import Digraph # type: ignore 2 | import random 3 | 4 | import claripy 5 | 6 | 7 | def render_ast(ast: claripy.ast.bv.BV) -> Digraph: 8 | graph = Digraph() 9 | 10 | def render_rec(graph: Digraph, ast: claripy.ast.bv.BV) -> str: 11 | if type(ast) in [int]: 12 | rand_ident = str(random.randint(1, 2 ** 32)) 13 | graph.node(rand_ident, label=hex(ast)) 14 | return rand_ident 15 | if ast.op == "BVS" or ast.op == "BVV": 16 | rand_ident = str(random.randint(1, 2 ** 32)) 17 | graph.node(rand_ident, label=ast.__str__()) 18 | return rand_ident 19 | op_node_ident = str(random.randint(1, 2 ** 32)) 20 | graph.node(op_node_ident, label=ast.op) 21 | for arg in ast.args: 22 | argnode = render_rec(graph, arg) 23 | graph.edge(op_node_ident, argnode) 24 | return op_node_ident 25 | 26 | render_rec(graph, ast) 27 | return graph 28 | 29 | 30 | def ast_to_svg(ast: claripy.ast.bv.BV) -> str: 31 | g = render_ast(ast) 32 | return g._repr_svg_() 33 | -------------------------------------------------------------------------------- /angrcli/full/__init__.py: -------------------------------------------------------------------------------- 1 | from angrcli.plugins.watches import Watches 2 | from angrcli.plugins.ContextView import ContextView 3 | from angrcli.plugins.explore import ExplorePlugin 4 | -------------------------------------------------------------------------------- /angrcli/interaction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/angrcli/interaction/__init__.py -------------------------------------------------------------------------------- /angrcli/interaction/explore.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import cmd2 4 | 5 | from angr import Project, SimState, SimulationManager 6 | from angrcli.plugins.ContextView.colors import Color, ColoredString 7 | 8 | 9 | class GUICallbackBaseClass: 10 | def update_ip(self, ip: int) -> None: 11 | pass 12 | 13 | 14 | class BinjaCallback(GUICallbackBaseClass): 15 | def __init__(self, bv: Any): 16 | self.bv = bv 17 | 18 | def update_ip(self, ip: int) -> None: 19 | self.bv.file.navigate(self.bv.file.view, ip) 20 | 21 | 22 | class ExploreInteractive(cmd2.Cmd): 23 | 24 | intro: ColoredString = Color.redify("[!] Dropping into angr shell\n") 25 | intro += Color.redify( 26 | "Available Commands: print, (p)ick, (r)un, (s)tep, stepi, (q)uit" 27 | ) # type: ignore 28 | prompt: ColoredString = Color.redify(">>> ") 29 | 30 | def __init__( 31 | self, 32 | proj: Project, 33 | state: SimState, 34 | gui_callback_object: GUICallbackBaseClass = GUICallbackBaseClass(), 35 | ): 36 | super(ExploreInteractive, self).__init__(allow_cli_args=False) 37 | self.proj = proj 38 | self.simgr = proj.factory.simulation_manager(state) # type: SimulationManager 39 | if "deferred" not in self.simgr.stashes: 40 | self.simgr.stashes["deferred"] = [] 41 | self.gui_cb = gui_callback_object 42 | 43 | @property 44 | def state(self) -> SimState: 45 | """ 46 | Alias to `self.simgr.one_active` 47 | :return: 48 | """ 49 | return self.simgr.one_active 50 | 51 | def _clearScreen(self) -> None: 52 | print("\033[H\033[J") 53 | 54 | def do_quit(self, args: str) -> bool: 55 | """Quits the cli.""" 56 | print(Color.redify("Exiting cmd-loop")) 57 | return True 58 | 59 | def do_q(self, args: str) -> bool: 60 | self.do_quit(args) 61 | return True 62 | 63 | def do_print(self, arg: str) -> None: 64 | """ 65 | print [state_number] 66 | Prints a state 67 | state_number optionally specifies the state to print if multiple are available 68 | """ 69 | if not arg: 70 | arg = "0" 71 | 72 | pick = int(arg) 73 | active = len(self.simgr.active) 74 | if pick >= active: 75 | print( 76 | Color.redify("Only {} active state(s), indexed from 0".format(active)) 77 | ) 78 | else: 79 | self.simgr.active[pick].context_view.pprint() 80 | self.gui_cb.update_ip(self.simgr.active[pick].addr) 81 | 82 | def do_stepi(self, args: str) -> None: 83 | """ 84 | stepi 85 | Steps one instruction 86 | """ 87 | if len(self.simgr.active) == 1: 88 | self.simgr.step(num_inst=1) 89 | self._clearScreen() 90 | if len(self.simgr.active) == 0: 91 | print(Color.redify("State terminated")) 92 | self._handle_state_termination() 93 | else: 94 | self.simgr.one_active.context_view.pprint(linear_code=True) 95 | self.gui_cb.update_ip(self.simgr.one_active.addr) 96 | elif len(self.simgr.active) > 1: 97 | for idx, state in enumerate(self.simgr.active): 98 | print(state.context_view._pstr_branch_info(idx)) 99 | 100 | def do_step(self, args: str) -> None: 101 | """ 102 | step 103 | Steps the current state one basic block 104 | """ 105 | if len(self.simgr.active) == 1: 106 | self.simgr.step() 107 | self._clearScreen() 108 | if len(self.simgr.active) == 0: 109 | print(Color.redify("State terminated")) 110 | self._handle_state_termination() 111 | else: 112 | self.simgr.one_active.context_view.pprint() 113 | self.gui_cb.update_ip(self.simgr.one_active.addr) 114 | elif len(self.simgr.active) > 1: 115 | for idx, state in enumerate(self.simgr.active): 116 | print(state.context_view._pstr_branch_info(idx)) 117 | 118 | def do_s(self, args: str) -> None: 119 | self.do_step(args) 120 | 121 | def do_run(self, args: str) -> None: 122 | """ 123 | run [state_number] 124 | Runs until a branch is encountered 125 | state_number optionally picks a state if multiple are available 126 | """ 127 | if len(self.simgr.active) > 1 and args: 128 | self.do_pick(args) 129 | if len(self.simgr.active) == 1: 130 | self.simgr.run(until=lambda s: len(s.active) != 1) 131 | if self.simgr.active: 132 | self.gui_cb.update_ip(self.simgr.one_active.addr) 133 | 134 | if len(self.simgr.active) > 0: 135 | for i, state in enumerate(self.simgr.active): 136 | print(state.context_view._pstr_branch_info(i)) 137 | else: 138 | print(Color.redify("STATE FINISHED EXECUTION")) 139 | self._handle_state_termination() 140 | 141 | def do_r(self, args: str) -> None: 142 | self.do_run(args) 143 | 144 | def do_pick(self, arg: str) -> None: 145 | """ 146 | pick 147 | Selects a state to continue if multiple are available, the other state is saved 148 | """ 149 | try: 150 | pick = int(arg) 151 | ip = self.simgr.active[pick].regs.ip 152 | except: 153 | print( 154 | "Invalid Choice: " 155 | + Color.redify("{}".format(arg)) 156 | + ", for {}".format(self.simgr) 157 | ) 158 | return 159 | print(Color.redify("Picking state with ip: " + (str(ip)))) 160 | self.simgr.move( 161 | from_stash="active", 162 | to_stash="deferred", 163 | filter_func=lambda x: x.solver.eval(ip != x.regs.ip), 164 | ) 165 | self.simgr.step() 166 | self._clearScreen() 167 | self.simgr.one_active.context_view.pprint() 168 | 169 | def do_p(self, args: str) -> None: 170 | self.do_pick(args) 171 | 172 | def do_EOF(self, args: str) -> bool: 173 | self.do_quit(args) 174 | return True 175 | 176 | def _handle_state_termination(self) -> None: 177 | self.simgr.deadended[-1].context_view.pprint() 178 | if len(self.simgr.stashes["deferred"]) == 0: 179 | print(Color.redify("No states left to explore")) 180 | else: # DFS-style like 181 | state = self.simgr.stashes["deferred"].pop() 182 | print( 183 | Color.redify("Other side of last branch with jumpguard ") 184 | + Color.greenify(str(state.solver.simplify(state.history.jump_guard))) 185 | + Color.redify(" has been added to {}".format(self.simgr)) 186 | ) 187 | self.simgr.stashes["active"].append(state) 188 | -------------------------------------------------------------------------------- /angrcli/plugins/ContextView/__init__.py: -------------------------------------------------------------------------------- 1 | from .context_view import ContextView 2 | -------------------------------------------------------------------------------- /angrcli/plugins/ContextView/colors.py: -------------------------------------------------------------------------------- 1 | from typing import NewType, cast 2 | 3 | ColoredString = NewType("ColoredString", str) 4 | 5 | 6 | class Color: 7 | """Used to colorify terminal output. 8 | Taken nearly verbatim from gef, https://github.com/hugsy/gef/blob/ecd6f8ff638d34043045df169ca6062b2fb28819/gef.py#L366-L421 9 | 10 | gef is distributed under the MIT License (MIT) 11 | Copyright (c) 2013-2019 crazy rabbidz 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | """ 31 | 32 | colors = { 33 | "normal": "\033[0m", 34 | "gray": "\033[1;38;5;240m", 35 | "red": "\033[31m", 36 | "green": "\033[32m", 37 | "yellow": "\033[33m", 38 | "blue": "\033[34m", 39 | "pink": "\033[35m", 40 | "cyan": "\033[36m", 41 | "bold": "\033[1m", 42 | "underline": "\033[4m", 43 | "underline_off": "\033[24m", 44 | "highlight": "\033[3m", 45 | "highlight_off": "\033[23m", 46 | "blink": "\033[5m", 47 | "blink_off": "\033[25m", 48 | } 49 | 50 | disable_colors = False 51 | 52 | @staticmethod 53 | def redify(msg: str) -> ColoredString: 54 | return Color.colorify(msg, "red") 55 | 56 | @staticmethod 57 | def greenify(msg: str) -> ColoredString: 58 | return Color.colorify(msg, "green") 59 | 60 | @staticmethod 61 | def blueify(msg: str) -> ColoredString: 62 | return Color.colorify(msg, "blue") 63 | 64 | @staticmethod 65 | def yellowify(msg: str) -> ColoredString: 66 | return Color.colorify(msg, "yellow") 67 | 68 | @staticmethod 69 | def grayify(msg: str) -> ColoredString: 70 | return Color.colorify(msg, "gray") 71 | 72 | @staticmethod 73 | def pinkify(msg: str) -> ColoredString: 74 | return Color.colorify(msg, "pink") 75 | 76 | @staticmethod 77 | def cyanify(msg: str) -> ColoredString: 78 | return Color.colorify(msg, "cyan") 79 | 80 | @staticmethod 81 | def boldify(msg: str) -> ColoredString: 82 | return Color.colorify(msg, "bold") 83 | 84 | @staticmethod 85 | def underlinify(msg: str) -> ColoredString: 86 | return Color.colorify(msg, "underline") 87 | 88 | @staticmethod 89 | def highlightify(msg: str) -> ColoredString: 90 | return Color.colorify(msg, "highlight") 91 | 92 | @staticmethod 93 | def blinkify(msg: str) -> ColoredString: 94 | return Color.colorify(msg, "blink") 95 | 96 | @staticmethod 97 | def colorify(text: str, attrs: str) -> ColoredString: 98 | """Color text according to the given attributes. 99 | :param str text: 100 | :param 101 | :return str: 102 | """ 103 | if Color.disable_colors is True: 104 | return cast(ColoredString, text) 105 | 106 | colors = Color.colors 107 | msg = [colors[attr] for attr in attrs.split() if attr in colors] 108 | msg.append(text) 109 | if colors["highlight"] in msg: 110 | msg.append(colors["highlight_off"]) 111 | if colors["underline"] in msg: 112 | msg.append(colors["underline_off"]) 113 | if colors["blink"] in msg: 114 | msg.append(colors["blink_off"]) 115 | msg.append(colors["normal"]) 116 | return cast(ColoredString, "".join(msg)) 117 | -------------------------------------------------------------------------------- /angrcli/plugins/ContextView/context_view.py: -------------------------------------------------------------------------------- 1 | from angr import SimEngineError, SimProcedure, SimState 2 | from angr.knowledge_plugins import Function 3 | from angr.sim_type import * 4 | 5 | import angr # type annotations; pylint: disable=unused-import 6 | import claripy 7 | from claripy.ast.bv import BV 8 | from claripy.annotation import UninitializedAnnotation 9 | from angr.calling_conventions import SimCC, SimFunctionArgument 10 | from typing import Optional, Tuple, Any, cast, List, Union, Dict 11 | 12 | from angrcli.plugins.ContextView.disassemblers import ( 13 | AngrCapstoneDisassembler, 14 | DisassemblerInterface, 15 | ) 16 | from archinfo import RegisterName 17 | from .colors import Color, ColoredString 18 | 19 | from angr.state_plugins import SimStatePlugin, SimSolver 20 | 21 | l = logging.getLogger("angr.state_plugins.context_view") 22 | 23 | from pygments import highlight # type: ignore 24 | from pygments.lexers.asm import NasmLexer # type: ignore 25 | from pygments.formatters.terminal import TerminalFormatter # type: ignore 26 | 27 | MAX_AST_DEPTH = 5 28 | 29 | PrettyString = Union[str, ColoredString] 30 | 31 | class ContextView(SimStatePlugin): 32 | # Class variable to specify disassembler 33 | _disassembler = AngrCapstoneDisassembler() # type: DisassemblerInterface 34 | 35 | def __init__( 36 | self, 37 | use_only_linear_disasm: bool = False, 38 | disable_linear_disasm_fallback: bool = True, 39 | ): 40 | super(ContextView, self).__init__() 41 | self.use_only_linear_disasm = use_only_linear_disasm 42 | self.disable_linear_disasm_fallback = disable_linear_disasm_fallback 43 | 44 | def set_state(self, state: SimState) -> None: 45 | super(ContextView, self).set_state(state) 46 | self.stack = Stack(self.state) 47 | 48 | @SimStatePlugin.memo 49 | def copy(self, memo: Any) -> "ContextView": 50 | return ContextView( 51 | self.use_only_linear_disasm, self.disable_linear_disasm_fallback 52 | ) 53 | 54 | def print_legend(self) -> None: 55 | s = "LEGEND: " 56 | s += Color.greenify("SYMBOLIC") 57 | s += " | " + Color.grayify("UNINITIALIZED") 58 | s += " | " + Color.yellowify("STACK") 59 | s += " | " + Color.blueify("HEAP") 60 | s += " | " + Color.redify("CODE R-X") 61 | s += " | " + Color.pinkify("DATA R*-") 62 | s += " | " + Color.underlinify("RWX") 63 | s += " | RODATA" 64 | print(s) 65 | 66 | def pprint(self, linear_code: bool = False) -> str: 67 | """ 68 | Pretty print an entire state 69 | :param bool linear_code: 70 | :return str: Should always be the empty string to allow monkey patching as __repr__ method 71 | """ 72 | """Pretty context view similiar to the context view of gdb plugins (peda and pwndbg)""" 73 | headerWatch = "[ ──────────────────────────────────────────────────────────────────── Watches ── ]" 74 | headerBacktrace = "[ ────────────────────────────────────────────────────────────────── BackTrace ── ]" 75 | headerCode = "[ ─────────────────────────────────────────────────────────────────────── Code ── ]" 76 | headerFDs = "[ ──────────────────────────────────────────────────────────── FileDescriptors ── ]" 77 | headerStack = "[ ────────────────────────────────────────────────────────────────────── Stack ── ]" 78 | headerRegs = "[ ────────────────────────────────────────────────────────────────── Registers ── ]" 79 | 80 | # Disable the warnings about accessing uninitialized memory/registers so they don't break printing 81 | self.state.options.add(angr.sim_options.SYMBOL_FILL_UNCONSTRAINED_MEMORY) 82 | self.state.options.add(angr.sim_options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS) 83 | self.print_legend() 84 | 85 | print(Color.blueify(headerRegs)) 86 | self.print_registers_pane() 87 | 88 | print(Color.blueify(headerCode)) 89 | self.print_code_pane(linear_code) 90 | 91 | if [b"", b"", b""] != [self.state.posix.dumps(x) for x in self.state.posix.fd]: 92 | print(Color.blueify(headerFDs)) 93 | self.print_fds_pane() 94 | 95 | print(Color.blueify(headerStack)) 96 | self.print_stack_pane() 97 | 98 | print(Color.blueify(headerBacktrace)) 99 | self.print_backtrace_pane() 100 | 101 | try: 102 | self.state.watches 103 | except AttributeError: 104 | l.warning("No watches plugin loaded, unable to print watches") 105 | else: 106 | print(Color.blueify(headerWatch)) 107 | self.print_watches_pane() 108 | 109 | # Reenable the warnings about accessing uninitialized memory/registers so they don't break printing 110 | self.state.options.remove(angr.sim_options.SYMBOL_FILL_UNCONSTRAINED_MEMORY) 111 | self.state.options.remove(angr.sim_options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS) 112 | 113 | # This is a hack to allow this method to be monkey patched as the __repr__ method of a state 114 | return "" 115 | 116 | def print_registers_pane(self) -> None: 117 | for reg in self.default_registers(): 118 | register_number, size = self.state.arch.registers[reg] 119 | print( 120 | self._pstr_register( 121 | reg, 122 | self.state.registers.load( 123 | register_number, inspect=False, disable_actions=True, size=size 124 | ), 125 | ) 126 | ) 127 | 128 | def print_code_pane(self, linear_code: bool = False) -> None: 129 | print(self._pstr_code(linear_code)) 130 | 131 | def print_fds_pane(self) -> None: 132 | for fd in self.state.posix.fd: 133 | print("fd " + str(fd), ":", repr(self.state.posix.dumps(fd))) 134 | 135 | def print_stack_pane(self) -> None: 136 | stackdepth = 8 137 | # Not sure if that can happen, but if it does things will break 138 | if not self.state.regs.sp.concrete: 139 | print("STACK POINTER IS SYMBOLIC: " + str(self.state.regs.sp)) 140 | return 141 | for o in range(stackdepth): 142 | print(self._pstr_stack_element(o)) 143 | 144 | def print_backtrace_pane(self) -> None: 145 | print(self._pstr_backtrace()) 146 | 147 | def print_watches_pane(self) -> None: 148 | try: 149 | self.state.watches 150 | except AttributeError: 151 | l.warning( 152 | "Tried accessing watches plugin, but the state doesn't have this registered" 153 | ) 154 | return 155 | for name, w in self.state.watches.eval: 156 | print("%s:\t%s" % (name, w)) 157 | return None 158 | 159 | def __BVtoREG(self, bv: BV) -> str: 160 | """ 161 | 162 | :param claripy.ast.BV bv: 163 | :return: str 164 | """ 165 | if type(bv) == str: 166 | return bv 167 | if "reg" in str(bv): 168 | replname = str(bv) 169 | for v in self.state.solver.describe_variables(bv): 170 | if "reg" in v: 171 | ridx = v[1] 172 | regname = self.state.arch.register_names[ridx] 173 | replname = replname.replace("reg_" + hex(ridx)[2:], regname) 174 | return replname 175 | return str(bv) 176 | 177 | def _color_code_ast(self, bv: claripy.ast.bv.BV) -> Tuple[ColoredString, bool]: 178 | """ 179 | Converts a bitvector into a string representation that is colored depending on it's type/value and returns 180 | 181 | Colors: 182 | Uninitialized: Gray 183 | Symbolic: Green 184 | Stack: Yellow 185 | Heap: Blue 186 | Code: Red 187 | :param claripy.ast.BV bv: 188 | :return Tuple[str, bool]: 189 | """ 190 | if bv.symbolic: 191 | if bv.has_annotation_type(UninitializedAnnotation): 192 | return Color.grayify(self.__BVtoREG(bv)), False 193 | return Color.greenify(self.__BVtoREG(bv)), False 194 | # its concrete 195 | value: int = self.state.solver.eval(bv, cast_to=int) 196 | 197 | if ( 198 | self.state.solver.eval(self.state.regs.sp) 199 | <= value 200 | <= self.state.arch.initial_sp 201 | ): 202 | return Color.yellowify(hex(value)), False 203 | if self.state.heap.heap_base <= value <= self.state.heap.heap_base + self.state.heap.heap_size: 204 | return Color.blueify(hex(value)), False 205 | 206 | try: 207 | perm = self.state.memory.permissions(value) 208 | if not perm.symbolic: 209 | perm = self.state.solver.eval(perm, cast_to=int) 210 | if perm: 211 | PROT_EXEC = 4 212 | if perm & 4: 213 | descr = " <%s>" % self.state.project.loader.describe_addr(value) 214 | if descr == " ": 215 | return Color.redify(hex(value)), True 216 | return Color.redify(hex(value) + descr), True 217 | else: 218 | descr = " <%s>" % self.state.project.loader.describe_addr(value) 219 | if descr == " ": 220 | return Color.pinkify(hex(value)), False 221 | return Color.pinkify(hex(value) + descr), False 222 | except: 223 | pass 224 | 225 | return cast(ColoredString, hex(value)), False 226 | 227 | def __color_code_ast(self, bv: claripy.ast.bv.BV) -> ColoredString: 228 | """ 229 | 230 | :param claripy.ast.BV bv: 231 | :return str: 232 | """ 233 | 234 | return self._color_code_ast(bv)[0] 235 | 236 | def _pstr_backtrace(self) -> PrettyString: 237 | """ 238 | Generates the backtrace of stackframes. 239 | Example: 240 | Frame 0: PLT.rand+0x401 in morph (0xb99) => 0xc0080176, sp = 0x7fffffffffefed8 241 | Frame 1: __libc_start_main.after_init+0x0 in extern-address space (0x98) => PLT.rand+0x2de in morph (0xa76), sp = 0x7fffffffffeff18 242 | Frame 2: PLT.rand+0x8 in morph (0x7a0) => __libc_start_main+0x0 in extern-address space (0x18), sp = 0x7fffffffffeff28 243 | Frame 3: 0x0 => 0x0, sp = 0xffffffffffffffff 244 | 245 | :return str: 246 | """ 247 | result = [] 248 | for i, f in enumerate(self.state.callstack): 249 | if self.state.project.loader.find_object_containing(f.call_site_addr): 250 | call_site_addr = self.state.project.loader.describe_addr( 251 | f.call_site_addr 252 | ) 253 | else: 254 | call_site_addr = "%#x" % f.call_site_addr 255 | if self.state.project.loader.find_object_containing(f.func_addr): 256 | func_addr = self.state.project.loader.describe_addr(f.func_addr) 257 | else: 258 | func_addr = "%#x" % f.func_addr 259 | 260 | frame = "Frame %d: %s => %s, sp = %#x" % ( 261 | i, 262 | call_site_addr, 263 | func_addr, 264 | f.stack_ptr, 265 | ) 266 | 267 | result.append(frame) 268 | return "\n".join(result) 269 | 270 | def _pstr_code(self, linear_code: bool =False) -> PrettyString: 271 | """ 272 | 273 | :param bool linear_code: Whether the code will be printed as linear or block based disassembly 274 | :return str: the string of the code pane with colored assembly 275 | """ 276 | result = [] 277 | if not self.use_only_linear_disasm and not linear_code: 278 | previos_block: str = self._pstr_previous_codeblock() 279 | if previos_block: 280 | result.append(previos_block) 281 | result.append( 282 | "\t|\t" 283 | + self.__color_code_ast( 284 | self.state.solver.simplify(self.state.history.jump_guard) 285 | ) 286 | + "\n\tv" 287 | ) 288 | result.append(self._pstr_current_codeblock(linear_code)) 289 | return "\n".join(result) 290 | 291 | def _pstr_previous_codeblock(self) -> PrettyString: 292 | """ 293 | Example: 294 | main+0x0 in sym_exec.elf (0x1149) 295 | 0x401149: push rbp 296 | 0x40114a: mov rbp, rsp 297 | 0x40114d: sub rsp, 0x20 298 | 0x401151: mov dword ptr [rbp - 0x14], edi 299 | 0x401154: mov qword ptr [rbp - 0x20], rsi 300 | 0x401158: cmp dword ptr [rbp - 0x14], 1 301 | 0x40115c: jg 0x401183 302 | 303 | :return str: The string form of the previous code block including annotations like location 304 | """ 305 | result = [] # type: List[PrettyString] 306 | try: 307 | prev_ip = self.state.history.bbl_addrs[-1] 308 | except IndexError: 309 | return cast(ColoredString, "") 310 | 311 | # Print the location (like main+0x10) if possible 312 | descr = self.state.project.loader.describe_addr(prev_ip) 313 | if descr != "not part of a loaded object": 314 | result.append(descr) 315 | 316 | if not self.state.project.is_hooked(prev_ip): 317 | code = self._pstr_codeblock(prev_ip) 318 | if code == None: 319 | if self.disable_linear_disasm_fallback: 320 | code = Color.redify( 321 | "No code at current ip. Please specify self_modifying code " 322 | ) 323 | else: 324 | raise Exception() # skip print of previous 325 | code_lines = code.split("\n") # type: List[PrettyString] 326 | 327 | # if it is longer than MAX_DISASS_LENGTH, only print the first lines 328 | if len(code_lines) >= self._disassembler.MAX_DISASS_LENGHT: 329 | result.append("TRUNCATED BASIC BLOCK") 330 | result.extend(code_lines[-self._disassembler.MAX_DISASS_LENGHT :]) 331 | else: 332 | result.extend(code_lines) 333 | 334 | else: 335 | hook = self.state.project.hooked_by(prev_ip) 336 | result.append(str(hook)) 337 | 338 | return "\n".join(result) 339 | 340 | def _pstr_current_codeblock(self, linear_code: bool = False) -> PrettyString: 341 | """ 342 | Example: 343 | main+0x15 in sym_exec.elf (0x115e) 344 | 0x40115e: mov rax, qword ptr [rbp - 0x20] 345 | 0x401162: mov rax, qword ptr [rax] 346 | 0x401165: mov rsi, rax 347 | 0x401168: lea rdi, [rip + 0xe95] 348 | 0x40116f: mov eax, 0 349 | 0x401174: call 0x401040 350 | 351 | :param bool linear_code: Whether the code will be printed as linear or block based disassembly 352 | :return str: The colored string form of the current code block including annotations like location, either in block or linear form 353 | """ 354 | 355 | result = [] 356 | current_ip = self.state.solver.eval(self.state.regs.ip) 357 | 358 | # Print the location (like main+0x10) if possible 359 | descr = self.state.project.loader.describe_addr(current_ip) 360 | if descr != "not part of a loaded object": 361 | result.append(descr) 362 | 363 | # Check if we are hooked or at the start of a known function to maybe pretty print the arguments 364 | cc = None # type: Optional[SimCC] 365 | target: Union[SimProcedure, Function, None] = None 366 | if current_ip in self.state.project.kb.functions: 367 | target: Function = self.state.project.kb.functions[current_ip] 368 | cc = target.calling_convention 369 | 370 | if self.state.project.is_hooked(current_ip): 371 | hook = self.state.project.hooked_by( 372 | current_ip 373 | ) # type: Optional[SimProcedure] 374 | # Technically we can be sure that hook isn't None, because we checked with is_hooked, 375 | # but mypy doesn't know this 376 | # But by not guarding this with an is_hooked, we get annoying warnings, so we do both 377 | if hook is not None: 378 | target = hook 379 | cc = target.cc 380 | 381 | if target and target.prototype: 382 | result.extend(self._pstr_call_info(self.state, cc, target.prototype)) 383 | 384 | # Get the current code block about to be executed as pretty disassembly 385 | if not self.state.project.is_hooked(current_ip): 386 | if self.use_only_linear_disasm or linear_code: 387 | code = self._pstr_codelinear(current_ip) 388 | else: 389 | code = self._pstr_codeblock(current_ip) 390 | if code == None: 391 | if self.disable_linear_disasm_fallback: 392 | code = Color.redify( 393 | "No code at current ip. Please specify self_modifying code " 394 | ) 395 | else: 396 | code = self._pstr_codelinear( 397 | current_ip 398 | ) # do fallback to Capstone 399 | code = code.split("\n") 400 | 401 | # if it is longer than MAX_DISASS_LENGTH, only print the first lines 402 | if len(code) >= self._disassembler.MAX_DISASS_LENGHT: 403 | result.extend(code[: self._disassembler.MAX_DISASS_LENGHT]) 404 | result.append("TRUNCATED BASIC BLOCK") 405 | else: 406 | result.extend(code) 407 | else: 408 | hook = self.state.project.hooked_by(current_ip) 409 | result.append(str(hook)) 410 | 411 | return "\n".join(result) 412 | 413 | def _pstr_codeblock(self, ip: int) -> Optional[List[PrettyString]]: 414 | """ 415 | Example: 416 | 0x40115e: mov rax, qword ptr [rbp - 0x20] 417 | 0x401162: mov rax, qword ptr [rax] 418 | 0x401165: mov rsi, rax 419 | 0x401168: lea rdi, [rip + 0xe95] 420 | 0x40116f: mov eax, 0 421 | 0x401174: call 0x401040 422 | 423 | :param int ip: Address of the start of the block (typically the instruction pointer, thus ip) 424 | :return Optional[str]: If a code block could be generated returns the colored assembly, else None 425 | 426 | """ 427 | try: 428 | block = self.state.project.factory.block(ip, backup_state=self.state) 429 | code = self._disassembler.block_disass(block, self) 430 | if not Color.disable_colors: 431 | return highlight(code, NasmLexer(), TerminalFormatter()) 432 | else: 433 | return code 434 | except SimEngineError as e: 435 | l.info("Got exception %s, returning None" % e) 436 | return None 437 | 438 | def _pstr_codelinear(self, ip: int) -> PrettyString: 439 | """ 440 | Example: 441 | 0x401154: mov qword ptr [rbp - 0x20], rsi 442 | 0x401158: cmp dword ptr [rbp - 0x14], 1 443 | 0x40115c: jg 0x401183 444 | 0x40115e: mov rax, qword ptr [rbp - 0x20] 445 | --> 0x401162: mov rax, qword ptr [rax] 446 | 0x401165: mov rsi, rax 447 | 0x401168: lea rdi, [rip + 0xe95] 448 | 0x40116f: mov eax, 0 449 | 0x401174: call 0x401040 450 | 0x401179: mov eax, 0xffffffff 451 | 0x40117e: jmp 0x401222 452 | 453 | :param int ip: Address around the instructon that should be disassembled 454 | :return str: The colored string of the instructions around the ip, with the ip instruction prefixed with "-->" 455 | """ 456 | code = self._disassembler.linear_disass(ip, self) 457 | if not Color.disable_colors: 458 | return highlight(code, NasmLexer(), TerminalFormatter()) 459 | else: 460 | return code 461 | 462 | def _pstr_stack_element(self, offset: int) -> PrettyString: 463 | """ 464 | Format: 465 | "IDX:OFFSET| ADDRESS --> CONTENT": 466 | Example 467 | 00:0x00| sp 0x7fffffffffeff10 --> 0x7fffffffffeff60 --> 0x7fffffffffeff98 --> 0x6d662f656d6f682f 468 | 469 | :param int offset: 470 | :return str: One line for the stack element being prettified 471 | """ 472 | """print(stack element in the form """ 473 | l = "%s:" % ("{0:#02d}".format(offset)) 474 | l += "%s| " % ("{0:#04x}".format(offset * self.state.arch.bytes)) 475 | try: 476 | stackaddr, stackval = self.stack[offset] 477 | except IndexError: 478 | return "" 479 | 480 | if self.state.solver.eval(stackaddr) == self.state.solver.eval( 481 | self.state.regs.sp, cast_to=int 482 | ): 483 | l += "sp" 484 | elif self.state.solver.eval(stackaddr) == self.state.solver.eval( 485 | self.state.regs.bp, cast_to=int 486 | ): 487 | l += "bp" 488 | else: 489 | l += " " 490 | l += " " 491 | 492 | l += "%s " % self.__color_code_ast(stackaddr) 493 | l += " --> %s" % self._pstr_ast(stackval) 494 | return l 495 | 496 | def _pstr_register(self, reg: RegisterName, value: claripy.ast.bv.BV) -> str: 497 | """ 498 | 499 | :param str reg: Name of the register 500 | :param claripy.ast.BV value: Value of the register 501 | :return str: 502 | """ 503 | repr = reg.upper() + ":\t" 504 | repr += self._pstr_ast(value) 505 | return repr 506 | 507 | def __deref_addr(self, addr: int) -> Optional[claripy.ast.bv.BV]: 508 | """ 509 | 510 | :param int addr: 511 | :param int depth: 512 | :return Optional[claripy.ast.BV]: 513 | """ 514 | if addr in self.state.memory: 515 | deref = self.state.memory.load(addr, 1, inspect=False, disable_actions=True) 516 | if deref.op == "Extract": 517 | deref = deref.args[2] 518 | else: 519 | deref = self.state.mem[addr].uintptr_t.resolved 520 | return deref 521 | return None 522 | 523 | def _pstr_ast( 524 | self, ast: claripy.ast.bv.BV, ty: Optional[SimType] = None, depth: int=0 525 | ) -> str: 526 | """Return a pretty string for an AST including a description of the derefed value if it makes sense (i.e. if 527 | the ast is concrete and the derefed value is not uninitialized 528 | More complex rendering is possible if type information is supplied 529 | :param claripy.ast.BV ast: The AST to be pretty printed 530 | :param angr.sim_type.SimType ty: Optional Type information 531 | :return str: Pretty colored string 532 | """ 533 | if depth > MAX_AST_DEPTH: 534 | return str(ast) 535 | 536 | # Type handling 537 | if isinstance(ty, SimTypePointer): 538 | if ast.concrete: 539 | cc_ast, ast_is_code_ptr = self._color_code_ast(ast) 540 | if ast_is_code_ptr: 541 | return cc_ast 542 | if isinstance(ty.pts_to, SimTypePointer): 543 | return 544 | try: 545 | tmp = "%s --> %s" % ( 546 | cc_ast, 547 | repr(self.state.mem[ast].string.concrete), 548 | ) 549 | except ValueError: 550 | deref = self.state.memory.load( 551 | ast, 1, inspect=False, disable_actions=True 552 | ) 553 | if deref.op == "Extract": 554 | return "%s --> %s" % ( 555 | cc_ast, 556 | self.__color_code_ast(deref.args[2]), 557 | ) 558 | elif deref.has_annotation_type(UninitializedAnnotation): 559 | return "%s --> UNINITIALIZED" % (cc_ast) 560 | else: 561 | return "%s --> COMPLEX SYMBOLIC STRING" % (cc_ast) 562 | else: 563 | return tmp 564 | else: 565 | return "%s %s" % ( 566 | Color.redify("WARN: Symbolic Pointer"), 567 | self.__color_code_ast(ast), 568 | ) 569 | if isinstance(ty, SimTypeChar) and ast.concrete: 570 | return "'%s'" % chr(self.state.solver.eval(ast)) 571 | elif isinstance(ty, SimTypeInt): 572 | if ast.concrete: 573 | return "%#x" % self.state.solver.eval(ast, cast_to=int) 574 | else: 575 | return str(ast) 576 | 577 | if ast.concrete: 578 | value: int = self.state.solver.eval(ast) 579 | cc_ast, ast_is_code_ptr = self._color_code_ast(ast) 580 | deref = self.__deref_addr(value) 581 | if (deref != None) and not ast_is_code_ptr and ast.op == "BVV": 582 | pretty_deref = self._pstr_ast(deref, depth=depth + 1) 583 | return "%s --> %s" % (cc_ast, pretty_deref) 584 | else: 585 | return cc_ast 586 | else: 587 | if ast.depth > MAX_AST_DEPTH: 588 | # AST is probably too large to render 589 | return Color.greenify( 590 | "" 591 | % (ast.depth, ast.variables, ast.__hash__()) 592 | ) 593 | return self.__color_code_ast(ast) 594 | 595 | def default_registers(self) -> List[RegisterName]: 596 | """ 597 | The list of the registers that are printed by default in the register pane 598 | Either some custom set for common architectures or a default set generated from the arch specification 599 | :return List[str]: 600 | """ 601 | custom = { 602 | "X86": cast(List[RegisterName], ["eax", "ebx", "ecx", "edx", "esi", "edi", "ebp", "esp", "eip"]), 603 | "AMD64": cast(List[RegisterName], [ 604 | "rax", 605 | "rbx", 606 | "rcx", 607 | "rdx", 608 | "rsi", 609 | "rdi", 610 | "rbp", 611 | "rsp", 612 | "rip", 613 | "r8", 614 | "r9", 615 | "r10", 616 | "r11", 617 | "r12", 618 | "r13", 619 | "r14", 620 | "r15", 621 | ]), 622 | } # type: Dict[str, List[RegisterName]] 623 | if self.state.arch.name in custom: 624 | return custom[self.state.arch.name] 625 | else: 626 | l.warning("No custom register list implemented, using fallback") 627 | return ( 628 | self.state.arch.default_symbolic_registers 629 | + [self.state.arch.register_names[self.state.arch.ip_offset]] 630 | + [self.state.arch.register_names[self.state.arch.sp_offset]] 631 | + [self.state.arch.register_names[self.state.arch.bp_offset]] 632 | ) 633 | 634 | def _pstr_branch_info(self, idx: Optional[int] = None) -> PrettyString: 635 | """ 636 | Return the information about the state concerning the last branch as a pretty string 637 | :param Optional[int] idx: 638 | :return str: 639 | """ 640 | str_ip = self._pstr_ast(self.state.regs.ip) 641 | simplified_jump_guard = self.state.solver.simplify( 642 | self.state.history.jump_guard 643 | ) 644 | str_jump_guard = self._pstr_ast(simplified_jump_guard) 645 | vars = cast(BV, self.state.history.jump_guard).variables 646 | 647 | return "%sIP: %s\tCond: %s\n\tVars: %s\n" % ( 648 | str(idx) + ":\t" if type(idx) is int else "", 649 | str_ip, 650 | str_jump_guard, 651 | vars, 652 | ) 653 | 654 | def _pstr_call_info(self, state: SimState, cc: SimCC, prototype: SimTypeFunction) -> List[PrettyString]: 655 | """ 656 | 657 | :param angr.SimState state: 658 | :param SimCC cc: 659 | :return List[str]: 660 | """ 661 | return [self._pstr_call_argument(*arg) for arg in cc.get_arg_info(state, prototype)] 662 | 663 | def _pstr_call_argument(self, ty: SimType, name: str, location: SimFunctionArgument, value: claripy.ast.bv.BV) -> PrettyString: 664 | """ 665 | The input should ideally be the unpacked tuple from one of the list entries of calling_convention.get_arg_info(state) 666 | :param angr.sim_type.SimType ty: 667 | :param str name: 668 | :param angr.calling_conventions.SimFunctionArgument location: 669 | :param claripy.ast.BV value: 670 | :return str: 671 | """ 672 | return "%s %s@%s: %s" % (ty, name, location, self._pstr_ast(value, ty=ty)) 673 | 674 | 675 | class Stack: 676 | def __init__(self, state: SimState): 677 | self.state = state # type: angr.SimState 678 | 679 | def __getitem__(self, offset: int) -> Tuple[int, claripy.ast.bv.BV]: 680 | """Returns a tuple of a stack element as (addr, content)""" 681 | addr = self.state.regs.sp + offset * self.state.arch.bytes 682 | if self.state.solver.eval(addr >= self.state.arch.initial_sp): 683 | raise IndexError 684 | return ( 685 | addr, 686 | self.state.memory.load( 687 | addr, 688 | size=self.state.arch.bytes, 689 | endness=self.state.arch.memory_endness, 690 | inspect=False, 691 | disable_actions=True, 692 | ), 693 | ) 694 | 695 | 696 | ContextView.register_default("context_view") 697 | -------------------------------------------------------------------------------- /angrcli/plugins/ContextView/disassemblers.py: -------------------------------------------------------------------------------- 1 | from typing import List, TYPE_CHECKING 2 | 3 | import angr 4 | 5 | 6 | class DisassemblerInterface: 7 | """ 8 | Interface that contains the options for the Disassembler abstraction used and the methods that need to be implemented 9 | """ 10 | 11 | MAX_DISASS_LENGHT = 30 12 | MAX_CAP_DIS_LENGHT = 10 13 | NB_INSTR_PREV = 4 14 | 15 | def block_disass(self, block: angr.block.Block, ctx_view: 'ContextView') -> List[str]: 16 | raise NotImplemented 17 | 18 | def linear_disass(self, ip: int, ctx_view: 'ContextView') -> List[str]: 19 | raise NotImplemented 20 | 21 | 22 | import capstone # type: ignore 23 | import claripy 24 | 25 | 26 | class AngrCapstoneDisassembler(DisassemblerInterface): 27 | def block_disass(self, block: angr.block.Block, ctx_view: 'ContextView') -> List[str]: 28 | """ 29 | 30 | :param angr.block.Block block: 31 | :param angrcli.plugins.context_view.ContextView ctx_view: 32 | :return: 33 | """ 34 | return str(block.capstone) 35 | 36 | def linear_disass(self, ip: int, ctx_view: 'ContextView') -> List[str]: 37 | """ 38 | 39 | When doing a fallback to Capstone we cannot disassemble by blocks so we 40 | procede in a GEF style: 41 | print NB_INSTR_PREV instruction before the current, 42 | print the current with an arrow, 43 | print (MAX_CAP_DIS_LENGHT - NB_INSTR_PREV -1) isntructions after current 44 | :param int ip: 45 | :param angrcli.plugins.context_view.ContextView ctx_view: 46 | :return: 47 | """ 48 | md = capstone.Cs( 49 | ctx_view.state.project.arch.cs_arch, ctx_view.state.project.arch.cs_mode 50 | ) 51 | 52 | disasm_start = ip 53 | for i in range(15 * self.NB_INSTR_PREV, 0, -1): 54 | 55 | mem = ctx_view.state.memory.load( 56 | ip - i, i + 15, inspect=False, disable_actions=True 57 | ) 58 | if mem.symbolic: 59 | break 60 | 61 | mem = ctx_view.state.solver.eval(mem, cast_to=bytes) 62 | 63 | cnt = 0 64 | last_instr = None 65 | for instr in md.disasm(mem, ip - i): 66 | if cnt == self.NB_INSTR_PREV: 67 | last_instr = instr 68 | break 69 | cnt += 1 70 | 71 | if last_instr is not None and last_instr.address == ip: 72 | disasm_start = ip - i 73 | break 74 | 75 | code = "" 76 | mem = ctx_view.state.memory.load(disasm_start, self.MAX_CAP_DIS_LENGHT * 15) 77 | if mem.symbolic: 78 | if isinstance(mem.args[0], claripy.ast.bv.BV) and not mem.args[0].symbolic: 79 | mem = mem.args[0] 80 | else: 81 | return "Instructions are symbolic!" 82 | 83 | mem = ctx_view.state.solver.eval(mem, cast_to=bytes) 84 | 85 | cnt = 0 86 | md = capstone.Cs( 87 | ctx_view.state.project.arch.cs_arch, ctx_view.state.project.arch.cs_mode 88 | ) 89 | for instr in md.disasm(mem, disasm_start): 90 | if instr.address == ip: 91 | code += " --> " 92 | else: 93 | code += " " 94 | code += "0x%x:\t%s\t%s\n" % (instr.address, instr.mnemonic, instr.op_str) 95 | if cnt == self.MAX_CAP_DIS_LENGHT: 96 | break 97 | cnt += 1 98 | return code 99 | 100 | 101 | class GhidraDisassembler(DisassemblerInterface): # noqa 102 | """ 103 | This classes uses ghidra_bridge to query the disassembly from Ghidra which automatically resolves structure and variable references 104 | ghidra_bridge is a giant hack, don't be confused that this uses variables that shouldn't exist and probably messes with the namespace in weird ways 105 | """ 106 | 107 | def __init__(self) -> None: 108 | import ghidra_bridge 109 | 110 | namespace = {} 111 | self._bridge = ghidra_bridge.GhidraBridge(namespace) 112 | self._cuf = ghidra.program.model.listing.CodeUnitFormat.DEFAULT # noqa: F821 113 | self._diss = ghidra.app.util.PseudoDisassembler(currentProgram) # noqa: F821 114 | 115 | def disass_line(self, addr): 116 | codeUnit = self._diss.disassemble(currentAddress.getNewAddress(addr)) # noqa: F821 117 | return "0x%x: %s\n" % (addr, self._cuf.getRepresentationString(codeUnit)) 118 | 119 | def block_disass(self, block: angr.block.Block, ctx_view: 'ContextView') -> List[str]: 120 | """ 121 | 122 | :param angr.block.Block block: 123 | :return: 124 | """ 125 | result = "" 126 | for a in block.instruction_addrs: 127 | codeUnit = self._diss.disassemble(currentAddress.getNewAddress(a)) # noqa: F821 128 | result += "0x%x: %s\n" % (a, self._cuf.getRepresentationString(codeUnit)) 129 | return result 130 | 131 | def linear_disass(self, ip: int, ctx_view: 'ContextView') -> List[str]: 132 | """ 133 | 134 | :param int ip: 135 | :param angrcli.plugins.context_view.ContextView ctx_view: 136 | :return: 137 | """ 138 | raise NotImplemented # TODO 139 | 140 | if TYPE_CHECKING: 141 | from angrcli.plugins.ContextView.context_view import ContextView 142 | -------------------------------------------------------------------------------- /angrcli/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/angrcli/plugins/__init__.py -------------------------------------------------------------------------------- /angrcli/plugins/explore.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | 3 | from angr import SimStatePlugin, SimState 4 | from angrcli.interaction.explore import ExploreInteractive 5 | 6 | 7 | class ExplorePlugin(SimStatePlugin): 8 | 9 | def __init__( 10 | self, 11 | explorer: Optional[ExploreInteractive] = None, 12 | ): 13 | super(ExplorePlugin, self).__init__() 14 | self._explorer = explorer 15 | 16 | def set_state(self, state: SimState) -> None: 17 | super(ExplorePlugin, self).set_state(state) 18 | 19 | @SimStatePlugin.memo 20 | def copy(self, memo: Any) -> "ExplorePlugin": 21 | return ExplorePlugin(self._explorer) 22 | 23 | def __call__(self): 24 | self._explorer = ExploreInteractive(self.state.project, self.state) 25 | self._explorer.cmdloop() 26 | return self._explorer.simgr 27 | 28 | 29 | ExplorePlugin.register_default("explore") -------------------------------------------------------------------------------- /angrcli/plugins/watches.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any, Dict, List, Optional 2 | 3 | from angr import SimStatePlugin, SimState 4 | from claripy.ast.bv import BV 5 | 6 | Watch = Callable[[SimState], Any] 7 | 8 | class Watches(SimStatePlugin): 9 | def __init__(self, watches: Dict[str, Watch]={}): 10 | """ 11 | :param watches: a list of lambdas that map a state to some AST to evaluate an arbitrary expression inside the state and keep track in the view 12 | """ 13 | super(Watches, self).__init__() 14 | self._watches = watches 15 | 16 | def set_state(self, state: SimState) -> None: 17 | super(Watches, self).set_state(state) 18 | 19 | def add_watch(self, watch: Watch, name: str) -> None: 20 | self._watches[name] = watch 21 | 22 | def watch_bv(self, bv: BV, cast_to: Optional[Any] =None) -> Watch: 23 | w: Watch = lambda state: state.solver.eval(bv, cast_to=cast_to) 24 | self.add_watch(w, bv.args[0].split('_')[0]) 25 | return w 26 | 27 | def __getitem__(self, key: str) -> Any: 28 | return self._watches[key](self.state) 29 | 30 | @SimStatePlugin.memo 31 | def copy(self, memo: object) -> 'Watches': 32 | return Watches(watches=self._watches) 33 | 34 | @property 35 | def eval(self) -> List[Any]: 36 | results = [] 37 | for name, watch in self._watches.items(): 38 | try: 39 | results.append((name, watch(self.state))) 40 | except Exception as e: 41 | results.append((name, e)) 42 | return results 43 | 44 | 45 | 46 | 47 | 48 | Watches.register_default("watches") -------------------------------------------------------------------------------- /angrcli/py.typed: -------------------------------------------------------------------------------- 1 | partial -------------------------------------------------------------------------------- /example/.gdb_history: -------------------------------------------------------------------------------- 1 | start 2 | r 3 | r asdfasdfasdfasdf 4 | c 5 | lq 6 | q 7 | start 8 | s 9 | q 10 | start 11 | s 12 | q 13 | start 14 | vmmap 15 | ls 16 | q 17 | -------------------------------------------------------------------------------- /example/.zshrc: -------------------------------------------------------------------------------- 1 | source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh 2 | 3 | -------------------------------------------------------------------------------- /example/MarsAnalytica: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/example/MarsAnalytica -------------------------------------------------------------------------------- /example/mars_auto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import angr 4 | import logging 5 | import pickle 6 | import IPython 7 | import claripy 8 | import angrcli.plugins.ContextView 9 | 10 | from angrcli.interaction.explore import ExploreInteractive 11 | 12 | logging.getLogger("angr.engines.vex.engine").setLevel(logging.ERROR) 13 | logging.getLogger("angr.state_plugins.symbolic_memory").setLevel(logging.ERROR) 14 | logging.getLogger("angr.sim_manager").setLevel(logging.INFO) 15 | 16 | testfile = "./MarsAnalytica" 17 | 18 | p = angr.Project(testfile) 19 | 20 | string = """ # run this code to generate the original states 21 | flag = claripy.BVS("flag", 8*19) 22 | s = p.factory.entry_state(stdin=flag, add_options=angr.options.unicorn) 23 | for b in flag.chop(8): 24 | s.solver.add(b > 0x20) 25 | s.solver.add(b < 0x7f) 26 | sm = p.factory.simulation_manager(s) 27 | print("Simulating until first branch") 28 | 29 | sm.run(until=lambda lpg: len(lpg.active) > 1) 30 | print("Reached first branch!") 31 | f1 = open("mars_state1", "wb") 32 | f2 = open("mars_state2", "wb") 33 | f1.write(pickle.dumps(sm.active[0], -1)) 34 | f1.close() 35 | f2.write(pickle.dumps(sm.active[1], -1)) 36 | f2.close() 37 | print("States stored!") 38 | """ 39 | f1 = open("mars_state1", "rb") 40 | f2 = open("mars_state2", "rb") 41 | s1 = pickle.loads(f1.read()) 42 | s2 = pickle.loads(f2.read()) 43 | f1.close() 44 | f2.close() 45 | sm = p.factory.simgr([s1, s2]) 46 | 47 | print("starting exploring") 48 | # Strategy: Explore from this point. Most constrained is good! 49 | # active[0].history.jump_guard: 0x317 == mul(__reverse(...)) 50 | # active[1].history.jump_guard: 0x317 != mul(__reverse(...)) 51 | while len(sm.deadended) == 0: 52 | print(sm) 53 | # Drop every state from 'active', except the first one (which is the most constrained one) 54 | sm.drop(stash='active', filter_func=lambda s: s != sm.active[0]) 55 | print(sm.one_active.posix.dumps(0)) 56 | # Continue to next branch 57 | sm.run(until=lambda lpg: len(lpg.deadended) > 1 or len(lpg.active) > 1) 58 | 59 | IPython.embed() 60 | print("Done!") 61 | 62 | s.context_view.pprint()# noqa: F821 63 | e = ExploreInteractive(p, s)# noqa: F821 64 | e.cmdloop() 65 | 66 | import IPython; IPython.embed() 67 | -------------------------------------------------------------------------------- /example/mars_state1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/example/mars_state1 -------------------------------------------------------------------------------- /example/mars_state2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/example/mars_state2 -------------------------------------------------------------------------------- /example/morph: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/example/morph -------------------------------------------------------------------------------- /example/morph_auto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | import angr 5 | import claripy 6 | 7 | logging.getLogger("angr.sim_manager").setLevel(logging.INFO) 8 | 9 | class NotVeryRand(angr.SimProcedure): 10 | def run(self, return_values=None): 11 | rand_idx = self.state.globals.get('rand_idx', 0) % len(return_values) 12 | out = return_values[rand_idx] 13 | self.state.globals['rand_idx'] = rand_idx + 1 14 | return out 15 | 16 | winStr = b'What are you waiting for, go submit that flag!' 17 | testfile = "./morph" 18 | argv = claripy.BVS('argv1', 8 * 0x17) 19 | 20 | p = angr.Project(testfile, 21 | support_selfmodifying_code=True, 22 | load_options={"auto_load_libs":False}) 23 | 24 | p.hook_symbol('time', NotVeryRand(return_values=[0])) 25 | p.hook_symbol('rand', NotVeryRand(return_values=[0])) 26 | 27 | s = p.factory.entry_state(args=[p.filename, argv]) 28 | 29 | simgr = p.factory.simgr(s)#, save_unsat=True) 30 | 31 | 32 | simgr.explore(find=lambda s: winStr in s.posix.dumps(1)) # winStr in stdout 33 | print("Exploration done! Result:", str(simgr)) 34 | if len(simgr.stashes["found"]) > 0: 35 | print(simgr.one_found.solver.eval(argv, cast_to=bytes)) 36 | 37 | -------------------------------------------------------------------------------- /example/morph_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import angr 4 | from angrcli.interaction.explore import ExploreInteractive 5 | import angrcli.plugins.ContextView 6 | import claripy 7 | import logging 8 | logging.getLogger("angr.engines.vex.engine").setLevel(logging.ERROR) 9 | logging.getLogger("angr.state_plugins.symbolic_memory").setLevel(logging.ERROR) 10 | 11 | testfile = "./morph" 12 | p = angr.Project(testfile, support_selfmodifying_code=True, 13 | load_options={"auto_load_libs":False, 14 | #load_options={"auto_load_libs":True, 15 | 'main_opts': { 16 | 'base_addr': 0x555555554000 # To match gdb 17 | } 18 | }) 19 | argv = claripy.BVS('argv1', 8 * 0x17) 20 | s = p.factory.entry_state(args=[p.filename, argv]) 21 | 22 | 23 | #s.register_plugin("context_view", cv()) 24 | 25 | class NotVeryRand(angr.SimProcedure): 26 | def run(self, return_values=None): 27 | rand_idx = self.state.globals.get('rand_idx', 0) % len(return_values) 28 | out = return_values[rand_idx] 29 | self.state.globals['rand_idx'] = rand_idx + 1 30 | return out 31 | 32 | p.hook_symbol('time', NotVeryRand(return_values=[0])) 33 | p.hook_symbol('rand', NotVeryRand(return_values=[0])) 34 | 35 | s.watches.add_watch(lambda state: state.solver.eval(argv, cast_to=bytes), "argv[1]") 36 | 37 | simgr = p.factory.simgr(s, save_unsat=True) 38 | 39 | s.context_view.pprint() 40 | e = ExploreInteractive(p, s) 41 | e.cmdloop() 42 | 43 | print("Done! e.simgr has the simgr from your session") 44 | 45 | -------------------------------------------------------------------------------- /images/ast_rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/images/ast_rendering.png -------------------------------------------------------------------------------- /images/context_view_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/images/context_view_demo.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='angrcli', 5 | version='1.2.0', 6 | packages=find_packages(exclude=['tests.*', 'tests', 'example.*', 'example']), 7 | include_package_data=True, 8 | license='MIT', 9 | long_description='none', 10 | python_requires='>=3.6', 11 | url='https://github.com/fmagin/angr-cli', 12 | install_requires=['angr', 'Pygments', 'cmd2'], 13 | package_data={'angrcli': ["py.typed"]}, 14 | ) 15 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import angr 2 | import angrcli.plugins.ContextView 3 | from angrcli.interaction.explore import ExploreInteractive 4 | import os 5 | location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sym_exec.elf") 6 | 7 | proj = angr.Project(location, load_options={'auto_load_libs': False}) 8 | 9 | state = proj.factory.entry_state() 10 | e = ExploreInteractive(proj, state) 11 | -------------------------------------------------------------------------------- /tests/simproc_demo.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/tests/simproc_demo.elf -------------------------------------------------------------------------------- /tests/sym_exec.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmagin/angr-cli/c41a0d8c8db88b9616b264cac501d3f75f3678cd/tests/sym_exec.elf -------------------------------------------------------------------------------- /tests/test_code_printing.py: -------------------------------------------------------------------------------- 1 | import angr 2 | import angrcli.plugins.ContextView 3 | 4 | from angrcli.plugins.ContextView.colors import Color 5 | 6 | from angrcli.interaction.explore import ExploreInteractive 7 | 8 | Color.disable_colors = True 9 | import os 10 | 11 | location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sym_exec.elf") 12 | 13 | proj = angr.Project(location, load_options={'auto_load_libs': False}) 14 | 15 | 16 | def test_linear_disass_valid_code(): 17 | state = proj.factory.entry_state() 18 | assert "--> 0x401050: endbr64 " in state.context_view._pstr_current_codeblock( 19 | linear_code=True), "'--> 0x401050: endbr64 ' not in code" 20 | 21 | 22 | def test_linear_disass_symbolic_code(): 23 | state = proj.factory.blank_state(addr=0x1337) 24 | assert state.context_view._pstr_current_codeblock(linear_code=True) == "Instructions are symbolic!" 25 | 26 | 27 | def test_entry_print(): 28 | state = proj.factory.entry_state() 29 | state.context_view: angrcli.plugins.ContextView.context_view.ContextView 30 | assert state.context_view._pstr_current_codeblock().split("\n")[1] == "0x401050: endbr64\t", "First code line not as expected" 31 | assert state.context_view._pstr_current_codeblock() == state.context_view._pstr_code(), "Code pane must be equal to current codeblock on entry state" 32 | 33 | 34 | def test_interactive(): 35 | state = proj.factory.entry_state() 36 | e = ExploreInteractive(proj, state) 37 | 38 | 39 | if __name__ == "__main__": 40 | test_linear_disass_symbolic_code() 41 | test_linear_disass_valid_code() 42 | -------------------------------------------------------------------------------- /tests/test_derefs.py: -------------------------------------------------------------------------------- 1 | import angr 2 | from angrcli.plugins.ContextView.colors import Color 3 | 4 | proj = angr.Project("/bin/ls", load_options={'auto_load_libs': False}) 5 | 6 | Color.disable_colors = True 7 | 8 | 9 | def test_max_depth(): 10 | state = proj.factory.blank_state() 11 | state.regs.rax = 0x2000 12 | state.mem[0x2000].uintptr_t = 0x3000 13 | state.mem[0x3000].uintptr_t = 0x4000 14 | state.mem[0x4000].uintptr_t = 0x5000 15 | state.mem[0x5000].uintptr_t = 0x6000 16 | state.mem[0x6000].uintptr_t = 0x7000 17 | state.mem[0x7000].uintptr_t = 0x8000 18 | state.mem[0x8000].uintptr_t = 0xa000 19 | assert state.context_view._pstr_register("RAX", 20 | state.regs.rax) == 'RAX:\t0x2000 --> 0x3000 --> 0x4000 --> 0x5000 --> 0x6000 --> 0x7000 --> ' 21 | 22 | 23 | def test_loop(): 24 | state = proj.factory.blank_state() 25 | state.regs.rax = 0x1337 26 | state.mem[0x1337].uintptr_t = 0x4242 27 | state.mem[0x4242].uintptr_t = 0x1337 28 | assert state.context_view._pstr_register("RAX", state.regs.rax) == "RAX:\t0x1337 --> 0x4242 --> 0x1337 --> 0x4242 --> 0x1337 --> 0x4242 --> " 29 | 30 | def test_cc(): 31 | state = proj.factory.blank_state() 32 | state.regs.rax = 0x2000 33 | colored_ast = state.context_view._color_code_ast(state.regs.rax)[0] 34 | assert colored_ast == '0x2000' 35 | 36 | 37 | if __name__ == "__main__": 38 | test_max_depth() 39 | test_loop() 40 | test_cc() 41 | -------------------------------------------------------------------------------- /tests/test_interactive_explore.py: -------------------------------------------------------------------------------- 1 | import os 2 | import angr 3 | import claripy 4 | from angrcli.interaction.explore import ExploreInteractive 5 | 6 | from angrcli.plugins.ContextView.colors import Color 7 | 8 | Color.disable_colors = True 9 | location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sym_exec.elf") 10 | 11 | proj = angr.Project(location, load_options={'auto_load_libs': False}) 12 | 13 | 14 | # cfg = proj.analyses.CFGFast() 15 | 16 | 17 | def test_proper_termination(): 18 | state = proj.factory.entry_state() 19 | e = ExploreInteractive(proj, state) 20 | 21 | for _ in range(0, 20): 22 | e.do_step("") 23 | 24 | # Final step 25 | e.do_step("") 26 | 27 | # One state should be deadended 28 | assert len(e.simgr.deadended) == 1 29 | 30 | # Stepping without active states should not throw an exception 31 | e.do_step("") 32 | 33 | 34 | def test_branching(): 35 | argv1 = claripy.BVS("argv1", 8 * 16) 36 | state = proj.factory.entry_state(args=[proj.filename, argv1]) 37 | e = ExploreInteractive(proj, state) 38 | 39 | e.do_run("") 40 | state = e.state 41 | 42 | # Try stepping and check that the state has not been changed 43 | e.do_step("") 44 | assert state == e.state, "State is not equal anymore" 45 | 46 | # Check that branch info is as expected. Bit hacky because the generated name of the variable might change during testing e.g. to argv1_51_128 instead of argv1_0_128 47 | assert e.state.context_view._pstr_branch_info() == "IP: 0x40119a \tCond: \n\tVars: frozenset({'%s'})\n" % (argv1.args[0], argv1.args[0]), "Branch info not as expected" 48 | 49 | s1, s2 = e.simgr.active 50 | # Pick wrong branch 51 | e.do_run("1") 52 | 53 | # One state should be deadended 54 | assert len(e.simgr.deadended) == 1, "Incorrect number of deadended states" 55 | 56 | # The other state of the last branch should now be the active one 57 | assert e.state == s1 58 | 59 | for _ in range(0, 8): 60 | e.do_run("0") 61 | 62 | assert b'PASSWORD' in e.simgr.deadended[1].solver.eval(argv1, cast_to=bytes) 63 | 64 | 65 | if __name__ == "__main__": 66 | test_proper_termination() 67 | test_branching() 68 | -------------------------------------------------------------------------------- /tests/test_morph.py: -------------------------------------------------------------------------------- 1 | import os 2 | import angr 3 | import claripy 4 | import angrcli.plugins.ContextView.context_view 5 | from angrcli.interaction.explore import ExploreInteractive 6 | 7 | import angrcli.plugins.watches 8 | 9 | from angrcli.plugins.ContextView.colors import Color 10 | 11 | Color.disable_colors = True 12 | morph_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "example", "morph") 13 | 14 | proj = angr.Project(morph_location, load_options={'auto_load_libs': False}, support_selfmodifying_code=True) 15 | 16 | 17 | 18 | class NotVeryRand(angr.SimProcedure): 19 | def run(self, return_values=None): 20 | rand_idx = self.state.globals.get('rand_idx', 0) % len(return_values) 21 | out = return_values[rand_idx] 22 | self.state.globals['rand_idx'] = rand_idx + 1 23 | return out 24 | 25 | 26 | argv = claripy.BVS('argv1', 8 * 0x17) 27 | state = proj.factory.entry_state(args=[proj.filename, argv]) 28 | 29 | state.watches.add_watch(lambda state: state.solver.eval(argv, cast_to=bytes), "argv[1]") 30 | 31 | proj.hook_symbol('time', NotVeryRand(return_values=[0])) 32 | proj.hook_symbol('rand', NotVeryRand(return_values=[0])) 33 | 34 | e = ExploreInteractive(proj, state) 35 | 36 | def test_morph(): 37 | # Run until first branch 38 | e.do_run("") 39 | 40 | # Select correct Strlen result 41 | e.do_run("0") 42 | # Check that some code is being printed 43 | assert "No code at current ip" not in e.state.context_view._pstr_current_codeblock(), "Code not being printed correctly" 44 | 45 | for _ in range(0,23): 46 | e.do_run("0") 47 | 48 | assert e.simgr.one_deadended.watches['argv[1]'] == b'34C3_M1GHTY_M0RPh1nG_g0', "Invalid watch result %s" % e.simgr.one_deadended.watches['argv[1]'] 49 | 50 | # if __name__ == "__main__": 51 | # # test_morph() 52 | -------------------------------------------------------------------------------- /tests/test_simprocs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import angr 3 | import claripy 4 | import angrcli.plugins.ContextView.context_view 5 | from angrcli.interaction.explore import ExploreInteractive 6 | 7 | from angrcli.plugins.ContextView.colors import Color 8 | 9 | Color.disable_colors = True 10 | location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "simproc_demo.elf") 11 | 12 | proj = angr.Project(location, load_options={'auto_load_libs': False}) 13 | 14 | cfg = proj.analyses.CFGFast() 15 | 16 | state = proj.factory.entry_state() 17 | e = ExploreInteractive(proj, state) 18 | 19 | 20 | def test_sims(): 21 | e.do_step("") 22 | 23 | lines = e.state.context_view._pstr_current_codeblock().split('\n') 24 | r = ['__libc_start_main+0x0 in extern-address space (0x0)', 25 | 'char* unknown@: 0x401159 ', 26 | # The following line is volatile and also wrong, because __libc_start_main has no proper prototype in angr defined yet 27 | # All the arguments default to char*, which is correct enough for all except argc, which is just an int 28 | 'char* unknown@: WARN: Symbolic Pointer ', 29 | "char* unknown@: 0x7fffffffffeff60 --> b'\\x98\\xff\\xfe\\xff\\xff\\xff\\xff\\x07'", 30 | 'char* unknown@: 0x4011b0 <__libc_csu_init+0x0 in simproc_demo.elf (0x11b0)>', 31 | 'char* unknown@: 0x401220 <__libc_csu_fini+0x0 in simproc_demo.elf (0x1220)>', 32 | ''] 33 | assert lines[0] == r[0], "Incorrect location for __libc_start_main" 34 | assert lines[1] == r[1], "Incorrect main argument for __libc_start_main" 35 | assert lines[4] == r[4], "Incorrect init argument for __libc_start_main" 36 | assert lines[5] == r[5], "Incorrect fini argument for __libc_start_main" 37 | assert lines[6] == r[6], "Incorrect code for __libc_start_main" 38 | 39 | for _ in range(0, 14): 40 | e.do_step("") 41 | 42 | # Should be at call puts 43 | lines = e.state.context_view._pstr_current_codeblock().split('\n') 44 | assert lines[0] == 'puts+0x0 in extern-address space (0x10)', "Incorrect location for puts" 45 | assert lines[1] == "char* s@: 0x402004 <_IO_stdin_used+0x4 in simproc_demo.elf (0x2004)> --> b'SimProc Demo'", "Incorrect arguments rendered for puts" 46 | assert lines[2] == '', "Incorrect code for puts" 47 | 48 | for _ in range(0, 3): 49 | e.do_step("") 50 | 51 | lines = e.state.context_view._pstr_current_codeblock().split('\n') 52 | assert lines[0] == 'malloc+0x0 in extern-address space (0x18)', "Incorrect location for malloc" 53 | assert lines[1] == "unsigned long (64 bits) size@: 0x100", "Incorrect arguments rendered for malloc" 54 | assert lines[2] == '', "Incorrect code for malloc" 55 | 56 | for _ in range(0, 3): 57 | e.do_step("") 58 | 59 | lines = e.state.context_view._pstr_current_codeblock().split('\n') 60 | assert lines[0] == 'strcpy+0x0 in extern-address space (0x8)', "Incorrect location for strcpy" 61 | assert lines[1] == "char* to@: 0xc0000f40 --> UNINITIALIZED", "Incorrect first argument rendered for strcpy" 62 | assert lines[2] == "char* from@: 0x%x --> b'%s'" % (e.state.regs.rsi.args[0], proj.filename), "Incorrect sencond argument rendered for strcpy" 63 | assert lines[3] == '', "Incorrect code for strcpy" 64 | 65 | 66 | if __name__ == "__main__": 67 | test_sims() 68 | --------------------------------------------------------------------------------