├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── feature-request.yml │ └── question.yml └── workflows │ ├── ci.yml │ ├── nightly-ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── angrop ├── __init__.py ├── arch.py ├── chain_builder │ ├── __init__.py │ ├── builder.py │ ├── func_caller.py │ ├── mem_changer.py │ ├── mem_writer.py │ ├── pivot.py │ ├── reg_mover.py │ ├── reg_setter.py │ ├── shifter.py │ └── sys_caller.py ├── common.py ├── errors.py ├── gadget_finder │ ├── __init__.py │ └── gadget_analyzer.py ├── rop.py ├── rop_block.py ├── rop_chain.py ├── rop_gadget.py ├── rop_utils.py └── rop_value.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── test_backend.py ├── test_badbytes.py ├── test_chainbuilder.py ├── test_find_gadgets.py ├── test_gadgets.py ├── test_rop.py ├── test_ropblock.py └── test_ropchain.py /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 2 | description: Report a bug in angrop 3 | labels: [bug,needs-triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to submit this bug report! 9 | 10 | Before submitting this bug report, please check the following, which may resolve your issue: 11 | * Have you checked that you are running the latest versions of angr and its components? angr is rapidly-evolving! 12 | * Have you [searched existing issues](https://github.com/angr/angrop/issues?q=is%3Aopen+is%3Aissue+label%3Abug) to see if this bug has been reported before? 13 | * Have you checked the [documentation](https://docs.angr.io/)? 14 | * Have you checked the [FAQ](https://docs.angr.io/introductory-errata/faq)? 15 | 16 | **Important:** If this bug is a security vulnerability, please submit it privately. See our [security policy](https://github.com/angr/angr/blob/master/SECURITY.md) for more details. 17 | 18 | **Please note: This repo is effectively unmaintained. While we appreciate bug reports and feature requests, we cannot commit to a timely response.** For more real-time help with angr, from us and the community, join our [Slack](https://angr.io/invite/). 19 | 20 | - type: textarea 21 | attributes: 22 | label: Description 23 | description: Brief description of the bug, with any relevant log messages. 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: Steps to reproduce the bug 30 | description: | 31 | If appropriate, include both a **script to reproduce the bug**, and if possible **attach the binary used**. 32 | 33 | **Tip:** You can attach files to the issue by first clicking on the textarea to select it, then dragging & dropping the file onto the textarea. 34 | - type: textarea 35 | attributes: 36 | label: Environment 37 | description: Many common issues are caused by problems with the local Python environment. Before submitting, double-check that your versions of all modules in the angr suite (angr, cle, pyvex, ...) are up to date and include the output of `python -m angr.misc.bug_report` here. 38 | 39 | - type: textarea 40 | attributes: 41 | label: Additional context 42 | description: Any additional context about the problem. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Join our Slack community 4 | url: https://angr.io/invite/ 5 | about: For questions and help with angr, you are invited to join the angr Slack community 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Request a feature 2 | description: Request a new feature for angrop 3 | labels: [enhancement,needs-triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to submit this feature request! 9 | 10 | Before submitting this feature request, please check the following: 11 | * Have you checked that you are running the latest versions of angr and its components? angr is rapidly-evolving! 12 | * Have you checked the [documentation](https://docs.angr.io/) to see if this feature exists already? 13 | * Have you [searched existing issues](https://github.com/angr/angrop/issues?q=is%3Aissue+label%3Aenhancement+) to see if this feature has been requested before? 14 | 15 | **Please note: This repo is effectively unmaintained. While we appreciate bug reports and feature requests, we cannot commit to a timely response.** For more real-time help with angr, from us and the community, join our [Slack](https://angr.io/invite/). 16 | 17 | - type: textarea 18 | attributes: 19 | label: Description 20 | description: | 21 | Brief description of the desired feature. If the feature is intended to solve some problem, please clearly describe the problem, including any relevant binaries, etc. 22 | 23 | **Tip:** You can attach files to the issue by first clicking on the textarea to select it, then dragging & dropping the file onto the textarea. 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: Alternatives 30 | description: Possible alternative solutions or features that you have considered. 31 | 32 | - type: textarea 33 | attributes: 34 | label: Additional context 35 | description: Any other context or screenshots about the feature request. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Ask a question 2 | description: Ask a question about angrop 3 | labels: [question,needs-triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | If you have a question about angrop, that is not a bug report or a feature request, you can ask it here. For more real-time help with angrop, from us and the community, join our [Slack](https://angr.io/invite/). 9 | 10 | Before submitting this question, please check the following, which may answer your question: 11 | * Have you checked the [documentation](https://docs.angr.io/)? 12 | * Have you checked the [FAQ](https://docs.angr.io/introductory-errata/faq)? 13 | * Have you checked our library of [examples](https://github.com/angr/angr-doc/tree/master/examples)? 14 | * Have you [searched existing issues](https://github.com/angr/angrop/issues?q=is%3Aissue+label%3Aquestion) to see if this question has been answered before? 15 | * Have you checked that you are running the latest versions of angr and its components. angr is rapidly-evolving! 16 | 17 | **Please note: This repo is effectively unmaintained. While we appreciate bug reports and feature requests, we cannot commit to a timely response.** For more real-time help with angr, from us and the community, join our [Slack](https://angr.io/invite/). 18 | 19 | - type: textarea 20 | attributes: 21 | label: Question 22 | description: 23 | validations: 24 | required: true 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ci: 12 | uses: angr/ci-settings/.github/workflows/angr-ci.yml@master 13 | -------------------------------------------------------------------------------- /.github/workflows/nightly-ci.yml: -------------------------------------------------------------------------------- 1 | name: Nightly CI 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | ci: 10 | uses: angr/ci-settings/.github/workflows/angr-ci.yml@master 11 | with: 12 | nightly: true 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v**" 7 | 8 | jobs: 9 | release: 10 | name: Create Release 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | - name: Setup Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | - name: Install pypa/build 20 | run: pip install build 21 | - name: Build sdist and wheel 22 | run: python -m build 23 | - name: Upload artifacts to pipeline 24 | uses: actions/upload-artifact@v3 25 | with: 26 | name: release_artifacts 27 | path: dist/ 28 | - name: Create github release 29 | run: gh release create --generate-notes dist/* 30 | env: 31 | GH_TOKEN: ${{ github.token }} 32 | - name: Publish distribution to PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | with: 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.egg-info 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, The Regents of the University of California 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angrop 2 | ====== 3 | 4 | angrop is a rop gadget finder and chain builder 5 | 6 | ## Overview 7 | angrop is a tool to automatically generate rop chains. 8 | 9 | It is built on top of angr's symbolic execution engine, and uses constraint solving for generating chains and understanding the effects of gadgets. 10 | 11 | angrop should support all the architectures supported by angr, although more testing needs to be done. 12 | 13 | Typically, it can generate rop chains (especially long chains) faster than humans. 14 | 15 | It includes functions to generate chains which are commonly used in exploitation and CTF's, such as setting registers, and calling functions. 16 | 17 | ## Architectures 18 | Supported architectures: 19 | * x86/x64 20 | * ARM 21 | * MIPS 22 | * AARCH64 23 | 24 | It should be relatively easy to support other architectures that are supported by `angr`. 25 | If you'd like to use `angrop` on other architectures, please create an issue and we will look into it :) 26 | 27 | ## Usage 28 | 29 | The ROP analysis finds rop gadgets and can automatically build rop chains. 30 | 31 | ```python 32 | >>> import angr, angrop 33 | >>> p = angr.Project("/bin/ls") 34 | >>> rop = p.analyses.ROP() 35 | >>> rop.find_gadgets() 36 | >>> chain = rop.set_regs(rax=0x1337, rbx=0x56565656) 37 | >>> chain.payload_str() 38 | b'\xb32@\x00\x00\x00\x00\x007\x13\x00\x00\x00\x00\x00\x00\xa1\x18@\x00\x00\x00\x00\x00VVVV\x00\x00\x00\x00' 39 | >>> chain.print_payload_code() 40 | chain = b"" 41 | chain += p64(0x410b23) # pop rax; ret 42 | chain += p64(0x1337) 43 | chain += p64(0x404dc0) # pop rbx; ret 44 | chain += p64(0x56565656) 45 | ``` 46 | 47 | ## Chains 48 | ```python 49 | # angrop includes methods to create certain common chains 50 | 51 | # setting registers 52 | chain = rop.set_regs(rax=0x1337, rbx=0x56565656) 53 | 54 | # moving registers 55 | chain = rop.move_regs(rax='rdx') 56 | 57 | # writing to memory 58 | # writes "/bin/sh\0" to address 0x61b100 59 | chain = rop.write_to_mem(0x61b100, b"/bin/sh\0") 60 | 61 | # calling functions 62 | chain = rop.func_call("read", [0, 0x804f000, 0x100]) 63 | 64 | # adding values to memory 65 | chain = rop.add_to_mem(0x804f124, 0x41414141) 66 | 67 | # shifting stack pointer like add rsp, 0x8; ret (this gadget shifts rsp by 0x10) 68 | chain = rop.shift(0x10) 69 | 70 | # generating ret-sled chains like ret*0x10, but works for ARM/MIPS as well 71 | chain = rop.retsled(0x40) 72 | 73 | # bad bytes can be specified to generate chains with no bad bytes 74 | rop.set_badbytes([0x0, 0x0a]) 75 | chain = rop.set_regs(eax=0) 76 | 77 | # chains can be added together to chain operations 78 | chain = rop.write_to_mem(0x61b100, b"/home/ctf/flag\x00") + rop.func_call("open", [0x61b100,os.O_RDONLY]) + ... 79 | 80 | # chains can be printed for copy pasting into exploits 81 | >>> chain.print_payload_code() 82 | chain = b"" 83 | chain += p64(0x410b23) # pop rax; ret 84 | chain += p64(0x74632f656d6f682f) 85 | chain += p64(0x404dc0) # pop rbx; ret 86 | chain += p64(0x61b0f8) 87 | chain += p64(0x40ab63) # mov qword ptr [rbx + 8], rax; add rsp, 0x10; pop rbx; ret 88 | ... 89 | 90 | ``` 91 | 92 | ## Gadgets 93 | 94 | Gadgets contain a lot of information: 95 | 96 | For example look at how the following code translates into a gadget 97 | 98 | ```asm 99 | 0x403be4: and ebp,edi 100 | 0x403be6: mov QWORD PTR [rbx+0x90],rax 101 | 0x403bed: xor eax,eax 102 | 0x403bef: add rsp,0x10 103 | 0x403bf3: pop rbx 104 | 0x403bf4: ret 105 | ``` 106 | 107 | ```python 108 | >>> print(rop.rop_gadgets[0]) 109 | Gadget 0x403be4 110 | Stack change: 0x20 111 | Changed registers: set(['rbx', 'rax', 'rbp']) 112 | Popped registers: set(['rbx']) 113 | Register dependencies: 114 | rbp: [rdi, rbp] 115 | Memory write: 116 | address (64 bits) depends on: ['rbx'] 117 | data (64 bits) depends on: ['rax'] 118 | ``` 119 | 120 | 121 | The dependencies describe what registers affect the final value of another register. 122 | In the example above, the final value of rbp depends on both rdi and rbp. 123 | Dependencies are analyzed for registers and for memory actions. 124 | All of the information is stored as properties in the gadgets, so it is easy to iterate over them and find gadgets which fit your needs. 125 | 126 | ```python 127 | >>> for g in rop.rop_gadgets: 128 | if "rax" in g.popped_regs and "rbx" not in g.changed_regs: 129 | print(g) 130 | Gadget 0x4032b3 131 | Stack change: 0x10 132 | Changed registers: set(['rax']) 133 | Popped registers: set(['rax']) 134 | Register dependencies: 135 | ``` 136 | 137 | ## TODO's 138 | Allow strings to be passed as arguments to func_call(), which are then written to memory and referenced. 139 | 140 | Add a function for open, read, write (for ctf's) 141 | 142 | The segment analysis for finding executable addresses seems to break on non-elf binaries often, such as PE files, kernel modules. 143 | 144 | Allow setting constraints on the generated chain e.g. bytes that are valid. 145 | 146 | ## Common gotchas 147 | Make sure to import angrop before calling proj.analyses.ROP() 148 | 149 | Make sure to call find_gadets() before trying to make chains 150 | -------------------------------------------------------------------------------- /angrop/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "9.2.12.dev0" 2 | 3 | from . import rop 4 | 5 | import sys 6 | if hasattr(sys, "set_int_max_str_digits"): sys.set_int_max_str_digits(0) 7 | 8 | -------------------------------------------------------------------------------- /angrop/arch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Architecture-dependent configurations 3 | """ 4 | 5 | class ROPArch: 6 | def __init__(self, project, kernel_mode=False): 7 | self.project = project 8 | self.kernel_mode = kernel_mode 9 | self.max_sym_mem_access = 4 10 | self.alignment = project.arch.instruction_alignment 11 | self.reg_set = self._get_reg_set() 12 | self.max_block_size = None 13 | self.fast_mode_max_block_size = None 14 | 15 | a = project.arch 16 | self.stack_pointer = a.register_names[a.sp_offset] 17 | self.base_pointer = a.register_names[a.bp_offset] 18 | self.syscall_insts = None 19 | self.ret_insts = None 20 | self.execve_num = None 21 | 22 | def _get_reg_set(self): 23 | """ 24 | get the set of names of general-purpose registers 25 | """ 26 | arch = self.project.arch 27 | _sp_reg = arch.register_names[arch.sp_offset] 28 | _ip_reg = arch.register_names[arch.ip_offset] 29 | 30 | # get list of general-purpose registers 31 | default_regs = arch.default_symbolic_registers 32 | # prune the register list of the instruction pointer and the stack pointer 33 | return {r for r in default_regs if r not in (_sp_reg, _ip_reg)} 34 | 35 | def block_make_sense(self, block): 36 | return True 37 | 38 | class X86(ROPArch): 39 | def __init__(self, project, kernel_mode=False): 40 | super().__init__(project, kernel_mode=kernel_mode) 41 | self.max_block_size = 20 42 | self.fast_mode_max_block_size = 12 43 | self.syscall_insts = {b"\xcd\x80"} # int 0x80 44 | self.ret_insts = {b"\xc2", b"\xc3", b"\xca", b"\xcb"} 45 | self.segment_regs = {"cs", "ds", "es", "fs", "gs", "ss"} 46 | self.execve_num = 0xb 47 | 48 | def _x86_block_make_sense(self, block): 49 | capstr = str(block.capstone).lower() 50 | # currently, angrop does not handle "repz ret" correctly, we filter it 51 | if any(x in capstr for x in ('cli', 'rex', 'repz ret')): 52 | return False 53 | if not self.kernel_mode: 54 | if "fs:" in capstr or "gs:" in capstr or "iret" in capstr: 55 | return False 56 | if block.size < 1 or block.bytes[0] == 0x4f: 57 | return False 58 | return True 59 | 60 | def block_make_sense(self, block): 61 | if not self._x86_block_make_sense(block): 62 | return False 63 | for x in block.capstone.insns: 64 | if x.mnemonic == 'syscall': 65 | return False 66 | return True 67 | 68 | class AMD64(X86): 69 | def __init__(self, project, kernel_mode=False): 70 | super().__init__(project, kernel_mode=kernel_mode) 71 | self.syscall_insts = {b"\x0f\x05"} # syscall 72 | self.segment_regs = {"cs_seg", "ds_seg", "es_seg", "fs_seg", "gs_seg", "ss_seg"} 73 | self.execve_num = 0x3b 74 | 75 | def block_make_sense(self, block): 76 | return self._x86_block_make_sense(block) 77 | 78 | arm_conditional_postfix = ['eq', 'ne', 'cs', 'hs', 'cc', 'lo', 'mi', 'pl', 79 | 'vs', 'vc', 'hi', 'ls', 'ge', 'lt', 'gt', 'le', 'al'] 80 | class ARM(ROPArch): 81 | 82 | def __init__(self, project, kernel_mode=False): 83 | super().__init__(project, kernel_mode=kernel_mode) 84 | self.is_thumb = False # by default, we don't use thumb mode 85 | self.alignment = self.project.arch.bytes 86 | self.max_block_size = self.alignment * 8 87 | self.fast_mode_max_block_size = self.alignment * 6 88 | self.execve_num = 0xb 89 | 90 | def set_thumb(self): 91 | self.is_thumb = True 92 | self.alignment = 2 93 | self.max_block_size = self.alignment * 8 94 | self.fast_mode_max_block_size = self.alignment * 6 95 | 96 | def set_arm(self): 97 | self.is_thumb = False 98 | self.alignment = self.project.arch.bytes 99 | self.max_block_size = self.alignment * 8 100 | self.fast_mode_max_block_size = self.alignment * 6 101 | 102 | def block_make_sense(self, block): 103 | # disable conditional jumps, for now 104 | # FIXME: we should handle conditional jumps, they are useful 105 | for insn in block.capstone.insns: 106 | if insn.insn.mnemonic[-2:] in arm_conditional_postfix: 107 | return False 108 | return True 109 | 110 | class AARCH64(ROPArch): 111 | def __init__(self, project, kernel_mode=False): 112 | super().__init__(project, kernel_mode=kernel_mode) 113 | self.ret_insts = {b'\xc0\x03_\xd6'} 114 | self.max_block_size = self.alignment * 10 115 | self.fast_mode_max_block_size = self.alignment * 6 116 | self.execve_num = 0xdd 117 | 118 | class MIPS(ROPArch): 119 | def __init__(self, project, kernel_mode=False): 120 | super().__init__(project, kernel_mode=kernel_mode) 121 | self.alignment = self.project.arch.bytes 122 | self.max_block_size = self.alignment * 8 123 | self.fast_mode_max_block_size = self.alignment * 6 124 | self.execve_num = 0xfab 125 | 126 | def get_arch(project, kernel_mode=False): 127 | name = project.arch.name 128 | mode = kernel_mode 129 | if name == 'X86': 130 | return X86(project, kernel_mode=mode) 131 | elif name == 'AMD64': 132 | return AMD64(project, kernel_mode=mode) 133 | elif name.startswith('ARM'): 134 | return ARM(project, kernel_mode=mode) 135 | elif name == 'AARCH64': 136 | return AARCH64(project, kernel_mode=mode) 137 | elif name.startswith('MIPS'): 138 | return MIPS(project, kernel_mode=mode) 139 | else: 140 | raise ValueError(f"Unknown arch: {name}") 141 | -------------------------------------------------------------------------------- /angrop/chain_builder/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .reg_setter import RegSetter 4 | from .reg_mover import RegMover 5 | from .mem_writer import MemWriter 6 | from .mem_changer import MemChanger 7 | from .func_caller import FuncCaller 8 | from .sys_caller import SysCaller 9 | from .pivot import Pivot 10 | from .shifter import Shifter 11 | from .. import rop_utils 12 | 13 | l = logging.getLogger("angrop.chain_builder") 14 | 15 | 16 | class ChainBuilder: 17 | """ 18 | This class provides functions to generate common ropchains based on existing gadgets. 19 | """ 20 | 21 | def __init__(self, project, rop_gadgets, pivot_gadgets, syscall_gadgets, arch, badbytes, roparg_filler): 22 | """ 23 | Initializes the chain builder. 24 | 25 | :param project: angr project 26 | :param gadgets: a list of RopGadget gadgets 27 | :param arch: a RopArch object 28 | :param badbytes: A list with badbytes, which we should avoid 29 | :param roparg_filler: An integer used when popping superfluous registers 30 | """ 31 | self.project = project 32 | self.arch = arch 33 | self.badbytes = badbytes 34 | self.roparg_filler = roparg_filler 35 | 36 | self.gadgets = rop_gadgets 37 | self.pivot_gadgets = pivot_gadgets 38 | self.syscall_gadgets = syscall_gadgets 39 | 40 | self._reg_setter = RegSetter(self) 41 | self._reg_mover = RegMover(self) 42 | self._mem_writer = MemWriter(self) 43 | self._mem_changer = MemChanger(self) 44 | self._func_caller = FuncCaller(self) 45 | self._pivot = Pivot(self) 46 | self._sys_caller = SysCaller(self) 47 | if not SysCaller.supported_os(self.project.loader.main_object.os): 48 | l.warning("%s is not a fully supported OS, SysCaller may not work on this OS", 49 | self.project.loader.main_object.os) 50 | self._shifter = Shifter(self) 51 | 52 | def set_regs(self, *args, **kwargs): 53 | """ 54 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 55 | :param registers: dict of registers to values 56 | :return: a chain which will set the registers to the requested values 57 | 58 | example: 59 | chain = rop.set_regs(rax=0x1234, rcx=0x41414141) 60 | """ 61 | return self._reg_setter.run(*args, **kwargs) 62 | 63 | def move_regs(self, **registers): 64 | """ 65 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 66 | :param registers: dict of registers, key is the destination register, value is the source register 67 | :return: a chain which will set the registers to the requested registers 68 | 69 | example: 70 | chain = rop.move_regs(rax='rcx', rcx='rbx') 71 | """ 72 | return self._reg_mover.run(**registers) 73 | 74 | def add_to_mem(self, addr, value, data_size=None): 75 | """ 76 | :param addr: the address to add to 77 | :param value: the value to add 78 | :param data_size: the size of the data for the add (defaults to project.arch.bits) 79 | :return: A chain which will do [addr] += value 80 | 81 | Example: 82 | chain = rop.add_to_mem(0x8048f124, 0x41414141) 83 | """ 84 | addr = rop_utils.cast_rop_value(addr, self.project) 85 | value = rop_utils.cast_rop_value(value, self.project) 86 | return self._mem_changer.add_to_mem(addr, value, data_size=data_size) 87 | 88 | def write_to_mem(self, addr, data, fill_byte=b"\xff"): 89 | """ 90 | :param addr: address to store the string 91 | :param data: string to store 92 | :param fill_byte: a byte to use to fill up the string if necessary 93 | :return: a rop chain 94 | """ 95 | addr = rop_utils.cast_rop_value(addr, self.project) 96 | return self._mem_writer.write_to_mem(addr, data, fill_byte=fill_byte) 97 | 98 | def pivot(self, thing): 99 | thing = rop_utils.cast_rop_value(thing, self.project) 100 | return self._pivot.pivot(thing) 101 | 102 | def func_call(self, address, args, **kwargs): 103 | """ 104 | :param address: address or name of function to call 105 | :param args: a list/tuple of arguments to the function 106 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 107 | :param needs_return: whether to continue the ROP after invoking the function 108 | :return: a RopChain which invokes the function with the arguments 109 | """ 110 | return self._func_caller.func_call(address, args, **kwargs) 111 | 112 | def do_syscall(self, syscall_num, args, needs_return=True, **kwargs): 113 | """ 114 | build a rop chain which performs the requested system call with the arguments set to 'registers' before 115 | the call is made 116 | :param syscall_num: the syscall number to execute 117 | :param args: the register values to have set at system call time 118 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 119 | :param needs_return: whether to continue the ROP after invoking the syscall 120 | :return: a RopChain which makes the system with the requested register contents 121 | """ 122 | if not self._sys_caller: 123 | l.exception("SysCaller does not support OS: %s", self.project.loader.main_object.os) 124 | return None 125 | return self._sys_caller.do_syscall(syscall_num, args, needs_return=needs_return, **kwargs) 126 | 127 | def execve(self, path=None, path_addr=None): 128 | """ 129 | build a rop chain that executes execve 130 | :param path: path of binary of execute, default to b"/bin/sh\x00" 131 | :param path_addr: where to store this path string 132 | """ 133 | if not self._sys_caller: 134 | l.exception("SysCaller does not support OS: %s", self.project.loader.main_object.os) 135 | return None 136 | return self._sys_caller.execve(path=path, path_addr=path_addr) 137 | 138 | def shift(self, length, preserve_regs=None, next_pc_idx=-1): 139 | """ 140 | build a rop chain to shift the stack to a specific value 141 | :param length: the length of sp you want to shift 142 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 143 | """ 144 | return self._shifter.shift(length, preserve_regs=preserve_regs, next_pc_idx=next_pc_idx) 145 | 146 | def retsled(self, size, preserve_regs=None): 147 | """ 148 | create a ret-sled ROP chain where if the control flow falls into any point of the chain, 149 | the control flow will be captured and maintained. 150 | for example, a series of ret gadgets in x86/x86_64 151 | :param size: the size of the retsled chain 152 | :param preserve_regs: set of registers to preserve, e.g. ('eax', 'ebx') 153 | """ 154 | return self._shifter.retsled(size, preserve_regs=preserve_regs) 155 | 156 | def set_badbytes(self, badbytes): 157 | self.badbytes = badbytes 158 | 159 | def set_roparg_filler(self, roparg_filler): 160 | self.roparg_filler = roparg_filler 161 | 162 | def bootstrap(self): 163 | self._reg_mover.bootstrap() 164 | self._reg_setter.bootstrap() 165 | self._mem_writer.bootstrap() 166 | self._mem_changer.bootstrap() 167 | self._func_caller.bootstrap() 168 | if self._sys_caller: 169 | self._sys_caller.bootstrap() 170 | self._pivot.bootstrap() 171 | self._shifter.bootstrap() 172 | 173 | self._reg_setter.optimize() 174 | 175 | # should also be able to do execve by providing writable memory 176 | # todo pass values to setregs as symbolic variables 177 | -------------------------------------------------------------------------------- /angrop/chain_builder/builder.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from abc import abstractmethod 3 | from functools import cmp_to_key 4 | 5 | import claripy 6 | 7 | from .. import rop_utils 8 | from ..errors import RopException 9 | from ..rop_gadget import RopGadget 10 | from ..rop_value import RopValue 11 | from ..rop_chain import RopChain 12 | from ..gadget_finder.gadget_analyzer import GadgetAnalyzer 13 | 14 | class Builder: 15 | """ 16 | a generic class to bootstrap more complicated chain building functionality 17 | """ 18 | def __init__(self, chain_builder): 19 | self.chain_builder = chain_builder 20 | self.project = chain_builder.project 21 | self.arch = chain_builder.arch 22 | self._gadget_analyzer = GadgetAnalyzer(self.project, 23 | True, 24 | kernel_mode=False, 25 | arch=self.arch) 26 | self._sim_state = rop_utils.make_symbolic_state( 27 | self.project, 28 | self.arch.reg_set, 29 | stack_gsize=80*3 30 | ) 31 | 32 | @property 33 | def badbytes(self): 34 | return self.chain_builder.badbytes 35 | 36 | @property 37 | def roparg_filler(self): 38 | return self.chain_builder.roparg_filler 39 | 40 | def make_sim_state(self, pc): 41 | """ 42 | make a symbolic state with all general purpose register + base pointer symbolized 43 | and emulate a `pop pc` situation 44 | """ 45 | arch_bytes = self.project.arch.bytes 46 | arch_endness = self.project.arch.memory_endness 47 | 48 | state = rop_utils.make_symbolic_state(self.project, self.arch.reg_set) 49 | rop_utils.make_reg_symbolic(state, self.arch.base_pointer) 50 | 51 | state.regs.ip = pc 52 | state.add_constraints(state.memory.load(state.regs.sp, arch_bytes, endness=arch_endness) == pc) 53 | state.regs.sp += arch_bytes 54 | state.solver._solver.timeout = 5000 55 | return state 56 | 57 | @staticmethod 58 | def _sort_chains(chains): 59 | def cmp_func(chain1, chain2): 60 | stack_change1 = sum(x.stack_change for x in chain1) 61 | stack_change2 = sum(x.stack_change for x in chain2) 62 | if stack_change1 > stack_change2: 63 | return 1 64 | elif stack_change1 < stack_change2: 65 | return -1 66 | 67 | num_mem_access1 = sum(x.num_sym_mem_access for x in chain1) 68 | num_mem_access2 = sum(x.num_sym_mem_access for x in chain2) 69 | if num_mem_access1 > num_mem_access2: 70 | return 1 71 | if num_mem_access1 < num_mem_access2: 72 | return -1 73 | return 0 74 | return sorted(chains, key=cmp_to_key(cmp_func)) 75 | 76 | def _word_contain_badbyte(self, ptr): 77 | """ 78 | check if a pointer contains any bad byte 79 | """ 80 | if isinstance(ptr, RopValue): 81 | if ptr.symbolic: 82 | return False 83 | else: 84 | ptr = ptr.concreted 85 | raw_bytes = struct.pack(self.project.arch.struct_fmt(), ptr) 86 | if any(x in raw_bytes for x in self.badbytes): 87 | return True 88 | return False 89 | 90 | def _get_ptr_to_writable(self, size): 91 | """ 92 | get a pointer to writable region that can fit `size` bytes 93 | it shouldn't contain bad byte 94 | """ 95 | # get all writable segments 96 | segs = [ s for s in self.project.loader.main_object.segments if s.is_writable ] 97 | # enumerate through all address to find a good address 98 | for seg in segs: 99 | for addr in range(seg.min_addr, seg.max_addr): 100 | if all(not self._word_contain_badbyte(x) for x in range(addr, addr+size, self.project.arch.bytes)): 101 | return addr 102 | return None 103 | 104 | def _get_ptr_to_null(self): 105 | # get all non-writable segments 106 | segs = [ s for s in self.project.loader.main_object.segments if not s.is_writable ] 107 | # enumerate through all address to find a good address 108 | for seg in segs: 109 | null = b'\x00'*self.project.arch.bytes 110 | for addr in self.project.loader.memory.find(null, search_min=seg.min_addr, search_max=seg.max_addr): 111 | if not self._word_contain_badbyte(addr): 112 | return addr 113 | return None 114 | 115 | @staticmethod 116 | def _ast_contains_stack_data(ast): 117 | vs = ast.variables 118 | return len(vs) == 1 and list(vs)[0].startswith('symbolic_stack_') 119 | 120 | def _build_ast_constraints(self, ast): 121 | var_map = {} 122 | 123 | # well, if this ast is just a symbolic value, just record itself 124 | if ast.op == 'BVS': 125 | name = ast.args[0] 126 | bits = ast.args[1] 127 | reg = name[5:].split('-')[0] 128 | old_var = ast 129 | new_var = claripy.BVS("sreg_" + reg + "-", bits) 130 | var_map[reg] = (old_var, new_var) 131 | 132 | # if this ast is a tree, record all the children_asts 133 | for x in ast.children_asts(): 134 | if x.op != 'BVS': 135 | continue 136 | name = x.args[0] 137 | bits = x.args[1] 138 | if not name.startswith("sreg_"): 139 | raise NotImplementedError(f"cannot rebuild ast: {ast}") 140 | reg = name[5:].split('-')[0] 141 | old_var = x 142 | if reg not in var_map: 143 | reg = name[5:].split('-')[0] 144 | new_var = claripy.BVS("sreg_" + reg + "-", bits) 145 | var_map[reg] = (old_var, new_var) 146 | 147 | consts = [] 148 | for old, new in var_map.values(): 149 | consts.append(old == new) 150 | rop_values = {x:RopValue(y[1], self.project) for x,y in var_map.items()} 151 | return rop_values, consts 152 | 153 | def _rebalance_ast(self, lhs, rhs): 154 | """ 155 | we know that lhs (stack content with modification) == rhs (user ropvalue) 156 | since user ropvalue may be symbolic, we need to present the stack content using the user ropvalue and store it 157 | on stack so that users can eval on their own ropvalue and get the correct solves 158 | TODO: currently, we only support add/sub 159 | """ 160 | assert self._ast_contains_stack_data(lhs) 161 | while lhs.depth != 1: 162 | match lhs.op: 163 | case "__add__" | "__sub__": 164 | arg0 = lhs.args[0] 165 | arg1 = lhs.args[1] 166 | flag = self._ast_contains_stack_data(arg0) 167 | op = lhs.op 168 | if flag: 169 | lhs = arg0 170 | other = arg1 171 | else: 172 | lhs = arg1 173 | other = arg0 174 | if op == "__add__": 175 | rhs -= other 176 | elif flag: 177 | rhs += other 178 | else: 179 | rhs = other - rhs 180 | case "Reverse": 181 | lhs = lhs.args[0] 182 | rhs = claripy.Reverse(rhs) 183 | case _: 184 | raise ValueError(f"{lhs.op} cannot be rebalanced at the moment. plz create an issue!") 185 | assert self._ast_contains_stack_data(lhs) 186 | return lhs, rhs 187 | 188 | @rop_utils.timeout(8) 189 | def _build_reg_setting_chain( 190 | self, gadgets, modifiable_memory_range, register_dict, stack_change 191 | ): 192 | """ 193 | This function figures out the actual values needed in the chain 194 | for a particular set of gadgets and register values 195 | This is done by stepping a symbolic state through each gadget 196 | then constraining the final registers to the values that were requested 197 | """ 198 | 199 | # emulate a 'pop pc' of the first gadget 200 | test_symbolic_state = rop_utils.make_symbolic_state( 201 | self.project, 202 | self.arch.reg_set, 203 | stack_gsize=stack_change // self.project.arch.bytes + 1, 204 | ) 205 | rop_utils.make_reg_symbolic(test_symbolic_state, self.arch.base_pointer) 206 | test_symbolic_state.ip = test_symbolic_state.stack_pop() 207 | test_symbolic_state.solver._solver.timeout = 5000 208 | 209 | # Maps each stack variable to the RopValue or RopGadget that should be placed there. 210 | stack_var_to_value = {} 211 | 212 | def map_stack_var(ast, value): 213 | if len(ast.variables) != 1: 214 | raise RopException("Target value not controlled by a single variable") 215 | var = next(iter(ast.variables)) 216 | if not var.startswith("symbolic_stack_"): 217 | raise RopException("Target value not controlled by the stack") 218 | stack_var_to_value[var] = value 219 | 220 | arch_bytes = self.project.arch.bytes 221 | 222 | state = test_symbolic_state.copy() 223 | 224 | # Step through each gadget and constrain the ip. 225 | for gadget in gadgets: 226 | map_stack_var(state.ip, gadget) 227 | state.solver.add(state.ip == gadget.addr) 228 | for addr in gadget.bbl_addrs[1:]: 229 | succ = state.step() 230 | succ_states = [ 231 | state 232 | for state in succ.successors 233 | if state.solver.is_true(state.ip == addr) 234 | ] 235 | if len(succ_states) != 1: 236 | raise RopException( 237 | "Zero or multiple states match address of next block" 238 | ) 239 | state = succ_states[0] 240 | succ = state.step() 241 | if succ.flat_successors or len(succ.unconstrained_successors) != 1: 242 | raise RopException( 243 | "Executing gadget doesn't result in a single unconstrained state" 244 | ) 245 | state = succ.unconstrained_successors[0] 246 | 247 | if len(state.solver.eval_upto(state.ip, 2)) < 2: 248 | raise RopException("The final pc is not unconstrained!") 249 | 250 | # Record the variable that controls the final ip. 251 | next_pc_val = rop_utils.cast_rop_value( 252 | test_symbolic_state.solver.BVS("next_pc", self.project.arch.bits), 253 | self.project, 254 | ) 255 | map_stack_var(state.ip, next_pc_val) 256 | 257 | # Constrain final register values. 258 | for reg, val in register_dict.items(): 259 | var = state.registers.load(reg) 260 | if val.is_register: 261 | if var.op != "BVS" or not next(iter(var.variables)).startswith( 262 | f"sreg_{val.reg_name}-" 263 | ): 264 | raise RopException("Register wasn't moved correctly") 265 | elif not var.symbolic and not val.symbolic: 266 | if var.concrete_value != val.concreted: 267 | raise RopException("Register set to incorrect value") 268 | else: 269 | state.solver.add(var == val.data) 270 | lhs, rhs = self._rebalance_ast(var, val.data) 271 | if self.project.arch.memory_endness == 'Iend_LE': 272 | rhs = claripy.Reverse(rhs) 273 | ropvalue = val.copy() 274 | if val.rebase: 275 | ropvalue._value = rhs - ropvalue._code_base 276 | else: 277 | ropvalue._value = rhs 278 | map_stack_var(lhs, ropvalue) 279 | 280 | # Constrain memory access addresses. 281 | for action in state.history.actions: 282 | if action.type == action.MEM and action.addr.symbolic: 283 | if modifiable_memory_range is None: 284 | raise RopException( 285 | "Symbolic memory address without modifiable memory range" 286 | ) 287 | state.solver.add(action.addr.ast >= modifiable_memory_range[0]) 288 | state.solver.add(action.addr.ast < modifiable_memory_range[1]) 289 | 290 | # now import the constraints from the state that has reached the end of the ropchain 291 | test_symbolic_state.solver.add(*state.solver.constraints) 292 | 293 | bytes_per_pop = arch_bytes 294 | 295 | # constrain the "filler" values 296 | if self.roparg_filler is not None: 297 | for offset in range(0, stack_change, bytes_per_pop): 298 | sym_word = test_symbolic_state.stack_read(offset, bytes_per_pop) 299 | # check if we can constrain val to be the roparg_filler 300 | if test_symbolic_state.solver.satisfiable([sym_word == self.roparg_filler]): 301 | # constrain the val to be the roparg_filler 302 | test_symbolic_state.add_constraints(sym_word == self.roparg_filler) 303 | 304 | # create the ropchain 305 | chain = RopChain(self.project, 306 | self, 307 | state=test_symbolic_state.copy(), 308 | badbytes=self.badbytes) 309 | 310 | # iterate through the stack values that need to be in the chain 311 | for offset in range(-bytes_per_pop, stack_change, bytes_per_pop): 312 | sym_word = test_symbolic_state.stack_read(offset, bytes_per_pop) 313 | assert len(sym_word.variables) == 1 314 | sym_var = next(iter(sym_word.variables)) 315 | if sym_var in stack_var_to_value: 316 | val = stack_var_to_value[sym_var] 317 | if isinstance(val, RopGadget): 318 | # this is special, we know this won't be "next_pc", so don't try 319 | # to take "next_pc"'s position 320 | value = RopValue(val.addr, self.project) 321 | value.rebase_analysis(chain=chain) 322 | chain.add_value(value) 323 | else: 324 | chain.add_value(val) 325 | else: 326 | chain.add_value(sym_word) 327 | 328 | chain.set_gadgets(gadgets) 329 | 330 | return chain 331 | 332 | def _get_fill_val(self): 333 | if self.roparg_filler is not None: 334 | return self.roparg_filler 335 | else: 336 | return claripy.BVS("filler", self.project.arch.bits) 337 | 338 | @abstractmethod 339 | def _same_effect(self, g1, g2): 340 | raise NotImplementedError("_same_effect is not implemented!") 341 | 342 | @abstractmethod 343 | def _better_than(self, g1, g2): 344 | raise NotImplementedError("_better_than is not implemented!") 345 | 346 | def same_effect(self, g1, g2): 347 | return self._same_effect(g1, g2) 348 | 349 | def better_than(self, g1, g2): 350 | if not self.same_effect(g1, g2): 351 | return False 352 | return self._better_than(g1, g2) 353 | 354 | def __filter_gadgets(self, gadgets): 355 | """ 356 | remove any gadgets that are strictly worse than others 357 | FIXME: make all gadget filtering logic like what we do in reg_setter, which is correct and way more faster 358 | """ 359 | gadgets = set(gadgets) 360 | bests = set() 361 | while gadgets: 362 | g1 = gadgets.pop() 363 | # check if nothing is better than g1 364 | for g2 in bests|gadgets: 365 | if self._better_than(g2, g1): #pylint: disable=arguments-out-of-order 366 | break 367 | else: 368 | bests.add(g1) 369 | return bests 370 | 371 | def _filter_gadgets(self, gadgets): 372 | bests = set() 373 | gadgets = set(gadgets) 374 | while gadgets: 375 | g0 = gadgets.pop() 376 | equal_class = {g for g in gadgets if self._same_effect(g0, g)} 377 | equal_class.add(g0) 378 | bests = bests.union(self.__filter_gadgets(equal_class)) 379 | 380 | gadgets -= equal_class 381 | return bests 382 | 383 | @abstractmethod 384 | def bootstrap(self): 385 | """ 386 | update the builder based on current gadgets to bootstrap a functional builder 387 | """ 388 | raise NotImplementedError("each Builder class should have an `update` method!") 389 | 390 | @abstractmethod 391 | def optimize(self): 392 | """ 393 | improve the capability of this builder using other builders 394 | """ 395 | cls_name = self.__class__.__name__ 396 | raise NotImplementedError(f"`advanced_update` is not implemented for {cls_name}!") 397 | -------------------------------------------------------------------------------- /angrop/chain_builder/func_caller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import angr 4 | import claripy 5 | from angr.calling_conventions import SimRegArg, SimStackArg 6 | 7 | from .builder import Builder 8 | from .. import rop_utils 9 | from ..errors import RopException 10 | from ..rop_gadget import FunctionGadget 11 | 12 | l = logging.getLogger(__name__) 13 | 14 | class FuncCaller(Builder): 15 | """ 16 | handle function calls by automatically detecting the target binary's 17 | calling convention 18 | thanks to @tomgond for a great portion of this class 19 | """ 20 | def __init__(self, chain_builder): 21 | super().__init__(chain_builder) 22 | # invoke a function but cannot maintain the control flow afterwards (pop rdi; jmp rax) 23 | self._func_jmp_gadgets = None 24 | # invoke a function and still maintain the control flow afterwards (call rax; ret) 25 | # TODO: currently not supported 26 | self._func_call_gadgets = None 27 | # record the calling convention 28 | self._cc = angr.default_cc( 29 | self.project.arch.name, 30 | platform=self.project.simos.name if self.project.simos is not None else None, 31 | )(self.project.arch) 32 | 33 | def bootstrap(self): 34 | cc = self._cc 35 | self._func_jmp_gadgets = set() 36 | for g in self.chain_builder.gadgets: 37 | if g.self_contained: 38 | continue 39 | if g.popped_regs.intersection(cc.ARG_REGS): 40 | self._func_jmp_gadgets.add(g) 41 | continue 42 | for move in g.reg_moves: 43 | if move.to_reg in cc.ARG_REGS: 44 | self._func_jmp_gadgets.add(g) 45 | break 46 | 47 | def _is_valid_pointer(self, addr): 48 | """ 49 | Validate if an address is a legitimate pointer in the binary 50 | Checks: 51 | 1. Address is within memory ranges 52 | 2. Address points to readable memory 53 | 3. Address is aligned 54 | """ 55 | arch_bytes = self.project.arch.bytes 56 | 57 | # Check basic alignment 58 | if addr % arch_bytes != 0: 59 | return False 60 | 61 | # Check against memory ranges 62 | if (addr < self.project.loader.min_addr or 63 | addr >= self.project.loader.max_addr): 64 | return False 65 | 66 | # Check readable writable sections 67 | for section in self.project.loader.main_object.sections: 68 | if (section.is_readable and 69 | section.min_addr <= addr < section.max_addr): 70 | return True 71 | 72 | return False 73 | 74 | def _find_function_pointer_in_got_plt(self, func_addr): 75 | """ 76 | Search if a func addr is in plt. If it's in plt, find func name and 77 | translate it to GOT so that we can directly call/jmp to the location pointed there. 78 | """ 79 | # Search GOT and PLT across all loaded objects 80 | func_name = None 81 | for sym in self.project.loader.main_object.symbols: 82 | if sym.rebased_addr == func_addr: 83 | func_name = sym.name 84 | 85 | for sym, val in self.project.loader.main_object.plt.items(): 86 | if val == func_addr: 87 | func_name = sym 88 | # addr is found in plt. we look for this symbol in got 89 | if func_name: 90 | func_got = self.project.loader.main_object.imports.get(func_name) 91 | if func_got: 92 | return func_got.rebased_addr 93 | else: 94 | # this is from plt but not in got somehow 95 | return None 96 | # not in plt. We can search in other ways 97 | else: 98 | return None 99 | 100 | def _find_function_pointer(self, func_addr): 101 | """Find pointer to function, allowing for potential memory locations""" 102 | # Existing GOT/PLT search logic first 103 | got_ptr = self._find_function_pointer_in_got_plt(func_addr) 104 | if got_ptr is not None: 105 | return got_ptr 106 | 107 | # Broader search strategy 108 | for obj in self.project.loader.all_objects: 109 | for section in obj.sections: 110 | if not section.is_readable: 111 | continue 112 | 113 | # Scan section for potential pointers 114 | for offset in range(0, section.max_addr - section.min_addr, self.project.arch.bytes): 115 | potential_ptr = section.min_addr + offset 116 | try: 117 | ptr_value = self.project.loader.memory.unpack_word(potential_ptr) 118 | if (ptr_value == func_addr and 119 | self._is_valid_pointer(potential_ptr)): 120 | return potential_ptr 121 | except Exception: # pylint: disable=broad-exception-caught 122 | continue 123 | 124 | raise RopException("Could not find mem pointing to func in binary memory") 125 | 126 | def _func_call(self, func_gadget, cc, args, extra_regs=None, preserve_regs=None, 127 | needs_return=True, jmp_mem_target=None, **kwargs): 128 | """ 129 | func_gadget: the address of the function to invoke 130 | cc: calling convention 131 | args: the arguments to the function 132 | extra_regs: what extra registers to set besides the function arguments, useful for invoking system calls 133 | preserve_res: what registers preserve 134 | needs_return: whether we need to cleanup stack after the function invocation, 135 | setting this to False will result in a shorter chain 136 | """ 137 | assert type(args) in [list, tuple], "function arguments must be a list or tuple!" 138 | if kwargs: 139 | l.warning("passing deprecated arguments %s to angrop.chain_builder.FuncCaller", kwargs) 140 | 141 | preserve_regs = set(preserve_regs) if preserve_regs else set() 142 | arch_bytes = self.project.arch.bytes 143 | 144 | # distinguish register and stack arguments 145 | register_arguments = args 146 | stack_arguments = [] 147 | if len(args) > len(cc.ARG_REGS): 148 | register_arguments = args[:len(cc.ARG_REGS)] 149 | stack_arguments = args[len(cc.ARG_REGS):] 150 | 151 | # set register arguments 152 | if needs_return and isinstance(cc.RETURN_ADDR, SimRegArg) and cc.RETURN_ADDR.reg_name != 'ip_at_syscall': 153 | reg_name = cc.RETURN_ADDR.reg_name 154 | preserve_regs.add(reg_name) 155 | registers = {} if extra_regs is None else extra_regs 156 | for arg, reg in zip(register_arguments, cc.ARG_REGS): 157 | registers[reg] = arg 158 | for reg in preserve_regs: 159 | registers.pop(reg, None) 160 | 161 | # if this is a simple function call, just set the registers and invoke it 162 | if not jmp_mem_target: 163 | chain = self.chain_builder.set_regs(**registers, preserve_regs=preserve_regs) 164 | else: 165 | # this is a jmp_mem function call, we need to constrain the jmp_mem target 166 | rop_values, constraints = self._build_ast_constraints(func_gadget.pc_target) 167 | registers.update(rop_values) 168 | chain = self.chain_builder.set_regs(**registers, preserve_regs=preserve_regs) 169 | state = chain._blank_state 170 | state.solver.add(claripy.And(*constraints)) 171 | state.solver.add(jmp_mem_target == func_gadget.pc_target) 172 | 173 | # invoke the function 174 | chain.add_gadget(func_gadget) 175 | for delta in range(func_gadget.stack_change//arch_bytes): 176 | if func_gadget.pc_offset is None or delta != func_gadget.pc_offset: 177 | chain.add_value(self._get_fill_val()) 178 | else: 179 | chain.add_value(claripy.BVS("next_pc", self.project.arch.bits)) 180 | 181 | # we are done here if we don't need to return 182 | if not needs_return: 183 | return chain 184 | 185 | # now we need to cleanly finish the calling convention 186 | # 1. handle stack arguments 187 | # 2. handle function return address to maintain the control flow 188 | if stack_arguments: 189 | shift_bytes = (len(stack_arguments)+1)*arch_bytes 190 | # TODO: currently, we only shift stack only for the minimal 191 | # but if this shift fails, we should try larger shifts 192 | cleaner = self.chain_builder.shift(shift_bytes, next_pc_idx=-1, preserve_regs=preserve_regs) 193 | chain.add_gadget(cleaner._gadgets[0]) 194 | for arg in stack_arguments: 195 | chain.add_value(arg) 196 | next_pc = claripy.BVS("next_pc", self.project.arch.bits) 197 | chain.add_value(next_pc) 198 | 199 | # handle return address 200 | if not isinstance(cc.RETURN_ADDR, (SimStackArg, SimRegArg)): 201 | raise RopException(f"What is the calling convention {cc} I'm dealing with?") 202 | if isinstance(cc.RETURN_ADDR, SimRegArg) and cc.RETURN_ADDR.reg_name != 'ip_at_syscall': 203 | # now we know this function will return to a specific register 204 | # so we need to set the return address before invoking the function 205 | reg_name = cc.RETURN_ADDR.reg_name 206 | shifter = self.chain_builder._shifter.shift(self.project.arch.bytes) 207 | next_ip = rop_utils.cast_rop_value(shifter._gadgets[0].addr, self.project) 208 | pre_chain = self.chain_builder.set_regs(**{reg_name: next_ip}) 209 | chain = pre_chain + chain 210 | return chain 211 | 212 | def func_call(self, address, args, **kwargs): 213 | """ 214 | :param address: address or name of function to call 215 | :param args: a list/tuple of arguments to the function 216 | :param preserve_regs: list of registers which shouldn't be set 217 | :param needs_return: whether to continue the ROP after invoking the function 218 | :return: a RopChain which invokes the function with the arguments 219 | """ 220 | symbol = None 221 | # is it a symbol? 222 | if isinstance(address, str): 223 | symbol = address 224 | symobj = self.project.loader.main_object.get_symbol(symbol) 225 | if hasattr(self.project.loader.main_object, 'plt') and address in self.project.loader.main_object.plt: 226 | address = self.project.loader.main_object.plt[symbol] 227 | elif symobj is not None: 228 | address = symobj.rebased_addr 229 | else: 230 | raise RopException("Symbol passed to func_call does not exist in the binary") 231 | 232 | # try to invoke the function using all self-contained gadgets 233 | func_gadget = FunctionGadget(address, symbol) 234 | func_gadget.stack_change = self.project.arch.bytes 235 | func_gadget.pc_offset = 0 236 | try: 237 | return self._func_call(func_gadget, self._cc, args, **kwargs) 238 | except RopException: 239 | pass 240 | 241 | # well, let's try non-self-contained gadgets, but this time, we don't guarantee returns 242 | needs_return = kwargs.get("needs_return", None) 243 | if needs_return: 244 | raise RopException("fail to invoke function and return using all self-contained gadgets") 245 | if needs_return is None: 246 | s = symbol if symbol else hex(address) 247 | l.warning("function %s won't return!", s) 248 | kwargs['needs_return'] = False 249 | 250 | # try func_jmp_gadgets 251 | register_args = args[:len(self._cc.ARG_REGS)] 252 | registers = {self._cc.ARG_REGS[i]:register_args[i] for i in range(len(register_args))} 253 | reg_names = set(registers.keys()) 254 | ptr_to_func = self._find_function_pointer(address) 255 | for g in self._func_jmp_gadgets: 256 | if g.popped_regs.intersection(reg_names): 257 | raise NotImplementedError("do not support func_jmp_gadgets that have pops") 258 | 259 | # build the new target registers 260 | registers = registers.copy() 261 | for move in g.reg_moves: 262 | if move.to_reg in registers.keys(): 263 | val = registers[move.to_reg] 264 | assert move.from_reg not in registers, "oops, overlapped moves not handled atm" 265 | del registers[move.to_reg] 266 | registers[move.from_reg] = val 267 | 268 | if g.transit_type != 'jmp_mem': 269 | raise NotImplementedError("currently only support jmp_mem type func_jmp_gadgets!") 270 | #func_gadget.stack_change = self.project.arch.bytes 271 | #func_gadget.pc_offset = 0 272 | # try to invoke the function using the new target registers 273 | try: 274 | return self._func_call(g, self._cc, [], extra_regs=registers, 275 | jmp_mem_target=ptr_to_func, **kwargs) 276 | except RopException: 277 | pass 278 | 279 | s = symbol if symbol else hex(address) 280 | raise RopException(f"fail to invoke function: {s}") 281 | -------------------------------------------------------------------------------- /angrop/chain_builder/mem_changer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import cmp_to_key 3 | 4 | import claripy 5 | import angr 6 | 7 | from .builder import Builder 8 | from .. import rop_utils 9 | from ..errors import RopException 10 | 11 | l = logging.getLogger(__name__) 12 | 13 | class MemChanger(Builder): 14 | """ 15 | part of angrop's chainbuilder engine, responsible for adding values to a memory location 16 | """ 17 | def __init__(self, chain_builder): 18 | super().__init__(chain_builder) 19 | self._mem_change_gadgets = None 20 | self._mem_add_gadgets = None 21 | 22 | def bootstrap(self): 23 | self._mem_change_gadgets = self._get_all_mem_change_gadgets(self.chain_builder.gadgets) 24 | self._mem_add_gadgets = self._get_all_mem_add_gadgets() 25 | 26 | def verify(self, chain, addr, value, _): 27 | arch_bytes = self.project.arch.bytes 28 | endness = self.project.arch.memory_endness 29 | 30 | # verify the chain actually works 31 | chain2 = chain.copy() 32 | chain2._blank_state.memory.store(addr.data, 0x41424344, arch_bytes, endness=endness) 33 | state = chain2.exec() 34 | sim_data = state.memory.load(addr.data, arch_bytes, endness=endness) 35 | if not state.solver.eval(sim_data == 0x41424344 + value.data): 36 | raise RopException("memory add fails - 1") 37 | # the next pc must come from the stack 38 | if len(state.regs.pc.variables) != 1: 39 | raise RopException("memory add fails - 2") 40 | if not set(state.regs.pc.variables).pop().startswith("next_pc_"): 41 | raise RopException("memory add fails - 3") 42 | 43 | def _set_regs(self, *args, **kwargs): 44 | return self.chain_builder._reg_setter.run(*args, **kwargs) 45 | 46 | def _same_effect(self, g1, g2): 47 | change1 = g1.mem_changes[0] 48 | change2 = g2.mem_changes[0] 49 | 50 | if change1.op != change2.op: 51 | return False 52 | if change1.data_size != change2.data_size: 53 | return False 54 | if change1.data_constant != change2.data_constant: 55 | return False 56 | if change1.addr_dependencies != change2.addr_dependencies: 57 | return False 58 | if change1.data_dependencies != change2.data_dependencies: 59 | return False 60 | return True 61 | 62 | def _better_than(self, g1, g2): 63 | if g1.isn_count <= g2.isn_count and \ 64 | g1.stack_change <= g2.stack_change and \ 65 | len(g1.changed_regs) <= len(g2.changed_regs) and \ 66 | g1.num_sym_mem_access <= g2.num_sym_mem_access: 67 | return True 68 | return False 69 | 70 | def _get_all_mem_change_gadgets(self, gadgets): 71 | possible_gadgets = set() 72 | for g in gadgets: 73 | if not g.self_contained: 74 | continue 75 | sym_rw = set(m for m in g.mem_reads + g.mem_writes if m.is_symbolic_access()) 76 | if len(sym_rw) > 0 or len(g.mem_changes) != 1: 77 | continue 78 | for m_access in g.mem_changes: 79 | # assume we need intersection of addr_dependencies and data_dependencies to be 0 80 | if m_access.addr_controllable() and m_access.data_controllable() and m_access.addr_data_independent(): 81 | possible_gadgets.add(g) 82 | gadgets = self._filter_gadgets(possible_gadgets) 83 | return sorted(gadgets, key=lambda x: x.stack_change) 84 | 85 | def _get_all_mem_add_gadgets(self): 86 | return [x for x in self._mem_change_gadgets if x.mem_changes[0].op in ('__add__', '__sub__')] 87 | 88 | @staticmethod 89 | def _sort_gadgets(gadgets): 90 | def cmp_func(g1, g2): 91 | # prefer gadget with fewer memory accesses 92 | if g1.num_sym_mem_access > g2.num_sym_mem_access: 93 | return 1 94 | if g1.num_sym_mem_access < g2.num_sym_mem_access: 95 | return -1 96 | # prefer gadget taking less space 97 | if g1.stack_change > g2.stack_change: 98 | return 1 99 | elif g1.stack_change < g2.stack_change: 100 | return -1 101 | # prefer shorter gadget 102 | if g1.isn_count > g2.isn_count: 103 | return 1 104 | elif g1.isn_count < g2.isn_count: 105 | return -1 106 | return 0 107 | return sorted(gadgets, key=cmp_to_key(cmp_func)) 108 | 109 | def add_to_mem(self, addr, value, data_size=None): 110 | # TODO could allow mem_reads as long as we control the address? 111 | 112 | if data_size is None: 113 | data_size = self.project.arch.bits 114 | 115 | possible_gadgets = [x for x in self._mem_add_gadgets if x.mem_changes[0].data_size == data_size] 116 | if not possible_gadgets: 117 | raise RopException("Fail to find any gadget that can perform memory adding...") 118 | 119 | # get the data from trying to set all the registers 120 | registers = dict((reg, 0x41) for reg in self.chain_builder.arch.reg_set) 121 | l.debug("getting reg data for mem adds") 122 | _, _, reg_data = self.chain_builder._reg_setter.find_candidate_chains_graph_search(max_stack_change=0x50, 123 | **registers) 124 | l.debug("trying mem_add gadgets") 125 | 126 | # filter out gadgets that certainly cannot be used for add_mem 127 | # e.g. we can't set needed registers 128 | gadgets = set() 129 | for t, _ in reg_data.items(): 130 | for g in possible_gadgets: 131 | mem_change = g.mem_changes[0] 132 | if (set(mem_change.addr_dependencies) | set(mem_change.data_dependencies)).issubset(set(t)): 133 | gadgets.add(g) 134 | 135 | # sort the gadgets with number of memory accesses and stack_change 136 | gadgets = self._sort_gadgets(gadgets) 137 | 138 | if not gadgets: 139 | raise RopException("Couldnt set registers for any memory add gadget") 140 | 141 | l.debug("Now building the mem add chain") 142 | 143 | # try to build the chain 144 | for g in gadgets: 145 | try: 146 | chain = self._add_mem_with_gadget(g, addr, data_size, difference=value) 147 | self.verify(chain, addr, value, data_size) 148 | return chain 149 | except RopException: 150 | pass 151 | 152 | raise RopException("Fail to perform add_to_mem!") 153 | 154 | def _add_mem_with_gadget(self, gadget, addr, data_size, final_val=None, difference=None): 155 | # sanity check for simple gadget 156 | if len(gadget.mem_writes) + len(gadget.mem_changes) != 1 or len(gadget.mem_reads) != 0: 157 | raise RopException("too many memory accesses for my lazy implementation") 158 | 159 | if (final_val is not None and difference is not None) or (final_val is None and difference is None): 160 | raise RopException("must specify difference or final value and not both") 161 | 162 | arch_endness = self.project.arch.memory_endness 163 | 164 | # constrain the successor to be at the gadget 165 | # emulate 'pop pc' 166 | test_state = self.make_sim_state(gadget.addr) 167 | 168 | if difference is not None: 169 | test_state.memory.store(addr.concreted, claripy.BVV(~(difference.concreted), data_size)) # pylint:disable=invalid-unary-operand-type 170 | if final_val is not None: 171 | test_state.memory.store(addr.concreted, claripy.BVV(~final_val, data_size)) # pylint:disable=invalid-unary-operand-type 172 | 173 | # step the gadget 174 | pre_gadget_state = test_state 175 | state = rop_utils.step_to_unconstrained_successor(self.project, pre_gadget_state) 176 | 177 | # constrain the change 178 | mem_change = gadget.mem_changes[0] 179 | the_action = None 180 | for a in state.history.actions.hardcopy: 181 | if a.type != "mem" or a.action != "write": 182 | continue 183 | if set(rop_utils.get_ast_dependency(a.addr.ast)) == set(mem_change.addr_dependencies): 184 | the_action = a 185 | break 186 | 187 | if the_action is None: 188 | raise RopException("Couldn't find the matching action") 189 | 190 | # constrain the addr 191 | test_state.add_constraints(the_action.addr.ast == addr.concreted) 192 | pre_gadget_state.add_constraints(the_action.addr.ast == addr.concreted) 193 | pre_gadget_state.options.discard(angr.options.AVOID_MULTIVALUED_WRITES) 194 | pre_gadget_state.options.discard(angr.options.AVOID_MULTIVALUED_READS) 195 | state = rop_utils.step_to_unconstrained_successor(self.project, pre_gadget_state) 196 | 197 | # constrain the data 198 | if final_val is not None: 199 | test_state.add_constraints(state.memory.load(addr.concreted, data_size//8, endness=arch_endness) == 200 | claripy.BVV(final_val, data_size)) 201 | if difference is not None: 202 | test_state.add_constraints(state.memory.load(addr.concreted, data_size//8, endness=arch_endness) - 203 | test_state.memory.load(addr.concreted, data_size//8, endness=arch_endness) == 204 | claripy.BVV(difference.concreted, data_size)) 205 | 206 | # get the actual register values 207 | all_deps = list(mem_change.addr_dependencies) + list(mem_change.data_dependencies) 208 | reg_vals = {} 209 | for reg in set(all_deps): 210 | reg_vals[reg] = test_state.solver.eval(test_state.registers.load(reg)) 211 | 212 | chain = self._set_regs(**reg_vals) 213 | chain.add_gadget(gadget) 214 | 215 | bytes_per_pop = self.project.arch.bytes 216 | for offset in range(0, gadget.stack_change, bytes_per_pop): 217 | if offset == gadget.pc_offset: 218 | chain.add_value(claripy.BVS("next_pc", self.project.arch.bits)) 219 | else: 220 | chain.add_value(self._get_fill_val()) 221 | return chain 222 | -------------------------------------------------------------------------------- /angrop/chain_builder/mem_writer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import angr 4 | import claripy 5 | 6 | from .builder import Builder 7 | from .. import rop_utils 8 | from ..errors import RopException 9 | from ..rop_chain import RopChain 10 | from ..rop_value import RopValue 11 | 12 | l = logging.getLogger("angrop.chain_builder.mem_writer") 13 | 14 | class MemWriter(Builder): 15 | """ 16 | part of angrop's chainbuilder engine, responsible for writing data into memory 17 | using various techniques 18 | """ 19 | def __init__(self, chain_builder): 20 | super().__init__(chain_builder) 21 | self._mem_write_gadgets: set = None # type: ignore 22 | self._good_mem_write_gadgets: set = None # type: ignore 23 | 24 | def bootstrap(self): 25 | self._mem_write_gadgets = self._get_all_mem_write_gadgets(self.chain_builder.gadgets) 26 | self._good_mem_write_gadgets = set() 27 | 28 | def _set_regs(self, *args, **kwargs): 29 | return self.chain_builder._reg_setter.run(*args, **kwargs) 30 | 31 | @staticmethod 32 | def _get_all_mem_write_gadgets(gadgets): 33 | possible_gadgets = set() 34 | for g in gadgets: 35 | if not g.self_contained: 36 | continue 37 | sym_rw = set(m for m in g.mem_reads + g.mem_changes if m.is_symbolic_access()) 38 | if len(sym_rw) > 0 or len(g.mem_writes) != 1: 39 | continue 40 | if g.stack_change <= 0: 41 | continue 42 | for m_access in g.mem_writes: 43 | if m_access.addr_controllable() and m_access.data_controllable() and m_access.addr_data_independent(): 44 | possible_gadgets.add(g) 45 | return possible_gadgets 46 | 47 | def _better_than(self, g1, g2): 48 | if g1.stack_change > g2.stack_change: 49 | return False 50 | if g1.num_sym_mem_access > g2.num_sym_mem_access: 51 | return False 52 | if g1.isn_count > g2.isn_count: 53 | return False 54 | if not g1.changed_regs.issubset(g2.changed_regs): 55 | return False 56 | return True 57 | 58 | def _gen_mem_write_gadgets(self, string_data): 59 | # create a dict of bytes per write to gadgets 60 | # assume we need intersection of addr_dependencies and data_dependencies to be 0 61 | # TODO could allow mem_reads as long as we control the address? 62 | 63 | # generate from the cache first 64 | if self._good_mem_write_gadgets: 65 | yield from self._good_mem_write_gadgets 66 | 67 | possible_gadgets = {g for g in self._mem_write_gadgets.copy() if g.transit_type != 'jmp_reg'} 68 | possible_gadgets -= self._good_mem_write_gadgets # already yield these 69 | 70 | # use the graph-search to gain a rough idea about (stack_change, register setting) 71 | registers = dict((reg, 0x41) for reg in self.arch.reg_set) 72 | l.debug("getting reg data for mem writes") 73 | reg_setter = self.chain_builder._reg_setter 74 | _, _, reg_data = reg_setter.find_candidate_chains_graph_search(max_stack_change=0x50, **registers) 75 | l.debug("trying mem_write gadgets") 76 | 77 | # find a write gadget that induces the smallest stack_change 78 | while possible_gadgets: 79 | # limit the maximum size of the chain 80 | best_stack_change = 0x400 81 | best_gadget = None 82 | # regs: according to the graph search, what registers can be controlled 83 | # vals[1]: stack_change to set those registers 84 | for regs, vals in reg_data.items(): 85 | reg_set_stack_change = vals[1] 86 | if reg_set_stack_change > best_stack_change: 87 | continue 88 | for g in possible_gadgets: 89 | mem_write = g.mem_writes[0] 90 | if not (mem_write.addr_dependencies | mem_write.data_dependencies).issubset(regs): 91 | continue 92 | stack_change = g.stack_change + reg_set_stack_change 93 | bytes_per_write = mem_write.data_size // 8 94 | num_writes = (len(string_data) + bytes_per_write - 1)//bytes_per_write 95 | stack_change *= num_writes 96 | if stack_change < best_stack_change: 97 | best_gadget = g 98 | best_stack_change = stack_change 99 | if stack_change == best_stack_change and self._better_than(g, best_gadget): 100 | best_gadget = g 101 | 102 | if best_gadget: 103 | possible_gadgets.remove(best_gadget) 104 | yield best_gadget 105 | else: 106 | break 107 | 108 | @rop_utils.timeout(5) 109 | def _try_write_to_mem(self, gadget, use_partial_controllers, addr, string_data, fill_byte): 110 | gadget_code = str(self.project.factory.block(gadget.addr).capstone) 111 | l.debug("building mem_write chain with gadget:\n%s", gadget_code) 112 | mem_write = gadget.mem_writes[0] 113 | 114 | # build the chain 115 | # there should be only two cases. Either it is a string, or it is a single badbyte 116 | chain = RopChain(self.project, self, badbytes=self.badbytes) 117 | if len(string_data) == 1 and ord(string_data) in self.badbytes: 118 | chain += self._write_to_mem_with_gadget(gadget, addr, string_data, use_partial_controllers) 119 | else: 120 | bytes_per_write = mem_write.data_size//8 if not use_partial_controllers else 1 121 | for i in range(0, len(string_data), bytes_per_write): 122 | to_write = string_data[i: i+bytes_per_write] 123 | # pad if needed 124 | if len(to_write) < bytes_per_write and fill_byte: 125 | to_write += fill_byte * (bytes_per_write-len(to_write)) 126 | chain += self._write_to_mem_with_gadget(gadget, addr + i, to_write, use_partial_controllers) 127 | 128 | return chain 129 | 130 | def _write_to_mem(self, addr, string_data, fill_byte=b"\xff"):# pylint:disable=inconsistent-return-statements 131 | """ 132 | :param addr: address to store the string 133 | :param string_data: string to store 134 | :param fill_byte: a byte to use to fill up the string if necessary 135 | :return: a rop chain 136 | """ 137 | for gadget in self._gen_mem_write_gadgets(string_data): 138 | try: 139 | chain = self._try_write_to_mem(gadget, False, addr, string_data, fill_byte) 140 | self._good_mem_write_gadgets.add(gadget) 141 | return chain 142 | except (RopException, angr.errors.SimEngineError, angr.errors.SimUnsatError): 143 | pass 144 | 145 | raise RopException("Fail to write data to memory :(") 146 | 147 | def write_to_mem(self, addr, data, fill_byte=b"\xff"): 148 | 149 | # sanity check 150 | if not (isinstance(fill_byte, bytes) and len(fill_byte) == 1): 151 | raise RopException("fill_byte is not a one byte string, aborting") 152 | if not isinstance(data, bytes): 153 | raise RopException("data is not a byte string, aborting") 154 | if ord(fill_byte) in self.badbytes: 155 | raise RopException("fill_byte is a bad byte!") 156 | 157 | # split the string into smaller elements so that we can 158 | # handle bad bytes 159 | if all(x not in self.badbytes for x in data): 160 | elems = [data] 161 | else: 162 | elems = [] 163 | e = b'' 164 | for x in data: 165 | if x not in self.badbytes: 166 | e += bytes([x]) 167 | else: 168 | if e: 169 | elems.append(e) 170 | elems.append(bytes([x])) 171 | e = b'' 172 | if e: 173 | elems.append(e) 174 | 175 | # do the write 176 | offset = 0 177 | chain = RopChain(self.project, self, badbytes=self.badbytes) 178 | for elem in elems: 179 | ptr = addr + offset 180 | if self._word_contain_badbyte(ptr): 181 | raise RopException(f"{ptr} contains bad byte!") 182 | if len(elem) != 1 or ord(elem) not in self.badbytes: 183 | chain += self._write_to_mem(ptr, elem, fill_byte=fill_byte) 184 | offset += len(elem) 185 | else: 186 | chain += self._write_to_mem(ptr, elem, fill_byte=fill_byte) 187 | offset += 1 188 | return chain 189 | 190 | def _write_to_mem_with_gadget(self, gadget, addr_val, data, use_partial_controllers=False): 191 | """ 192 | addr_val is a RopValue 193 | """ 194 | addr_bvs = claripy.BVS("addr", self.project.arch.bits) 195 | 196 | # sanity check for simple gadget 197 | if len(gadget.mem_writes) != 1 or len(gadget.mem_reads) + len(gadget.mem_changes) > 0: 198 | raise RopException("too many memory accesses for my lazy implementation") 199 | 200 | if use_partial_controllers and len(data) < self.project.arch.bytes: 201 | data = data.ljust(self.project.arch.bytes, b"\x00") 202 | 203 | # constrain the successor to be at the gadget 204 | # emulate 'pop pc' 205 | test_state = self.make_sim_state(gadget.addr) 206 | 207 | # step the gadget 208 | pre_gadget_state = test_state 209 | state = rop_utils.step_to_unconstrained_successor(self.project, pre_gadget_state) 210 | 211 | # constrain the write 212 | mem_write = gadget.mem_writes[0] 213 | the_action = None 214 | for a in state.history.actions.hardcopy: 215 | if a.type != "mem" or a.action != "write": 216 | continue 217 | if set(rop_utils.get_ast_dependency(a.addr.ast)) == set(mem_write.addr_dependencies) or \ 218 | set(rop_utils.get_ast_dependency(a.data.ast)) == set(mem_write.data_dependencies): 219 | the_action = a 220 | break 221 | 222 | if the_action is None: 223 | raise RopException("Couldn't find the matching action") 224 | 225 | # constrain the addr 226 | test_state.add_constraints(the_action.addr.ast == addr_bvs, addr_bvs == addr_val.data) 227 | pre_gadget_state.add_constraints(the_action.addr.ast == addr_bvs, addr_bvs == addr_val.data) 228 | pre_gadget_state.options.discard(angr.options.AVOID_MULTIVALUED_WRITES) 229 | state = rop_utils.step_to_unconstrained_successor(self.project, pre_gadget_state) 230 | 231 | # constrain the data 232 | test_state.add_constraints(state.memory.load(addr_val.data, len(data)) == claripy.BVV(data)) 233 | 234 | # get the actual register values 235 | all_deps = list(mem_write.addr_dependencies) + list(mem_write.data_dependencies) 236 | reg_vals = {} 237 | name = addr_bvs._encoded_name.decode() 238 | for reg in set(all_deps): 239 | var = test_state.solver.eval(test_state.registers.load(reg)) 240 | # check whether this reg will propagate to addr 241 | # if yes, propagate its rebase value 242 | for c in test_state.solver.constraints: 243 | if len(c.variables) != 2: # xx == yy 244 | continue 245 | if name not in c.variables: 246 | continue 247 | var_names = set(c.variables) 248 | var_names.remove(name) 249 | if reg in var_names.pop(): 250 | var = RopValue(var, self.project) 251 | var._rebase = False 252 | if addr_val._rebase: 253 | var.rebase_ptr() 254 | var._rebase = True 255 | break 256 | reg_vals[reg] = var 257 | 258 | 259 | chain = self._set_regs(**reg_vals) 260 | chain.add_gadget(gadget) 261 | 262 | bytes_per_pop = self.project.arch.bytes 263 | pc_offset = None 264 | if gadget.transit_type == 'pop_pc': 265 | pc_offset = gadget.pc_offset 266 | else: 267 | raise ValueError(f"Unknown gadget transit_type: {gadget.transit_type}") 268 | 269 | for idx in range(gadget.stack_change // bytes_per_pop): 270 | if idx == pc_offset//bytes_per_pop: 271 | next_pc_val = rop_utils.cast_rop_value( 272 | chain._blank_state.solver.BVS("next_pc", self.project.arch.bits), 273 | self.project, 274 | ) 275 | chain.add_value(next_pc_val) 276 | continue 277 | chain.add_value(self._get_fill_val()) 278 | 279 | # verify the write actually works 280 | state = chain.exec() 281 | sim_data = state.memory.load(addr_val.data, len(data)) 282 | if not state.solver.eval(sim_data == data): 283 | raise RopException("memory write fails") 284 | 285 | # the next pc must be in our control 286 | if len(state.regs.pc.variables) != 1: 287 | raise RopException("must have only one pc variable") 288 | if not set(state.regs.pc.variables).pop().startswith("next_pc_"): 289 | raise RopException("the next pc is not in our control!") 290 | return chain 291 | -------------------------------------------------------------------------------- /angrop/chain_builder/pivot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import functools 3 | 4 | from .builder import Builder 5 | from .. import rop_utils 6 | from ..errors import RopException 7 | 8 | l = logging.getLogger(__name__) 9 | 10 | def cmp(g1, g2): 11 | if len(g1.sp_reg_controllers) < len(g2.sp_reg_controllers): 12 | return -1 13 | if len(g1.sp_reg_controllers) > len(g2.sp_reg_controllers): 14 | return 1 15 | 16 | if g1.stack_change + g1.stack_change_after_pivot < g2.stack_change + g2.stack_change_after_pivot: 17 | return -1 18 | if g1.stack_change + g1.stack_change_after_pivot > g2.stack_change + g2.stack_change_after_pivot: 19 | return 1 20 | 21 | if g1.isn_count < g2.isn_count: 22 | return -1 23 | if g1.isn_count > g2.isn_count: 24 | return 1 25 | return 0 26 | 27 | class Pivot(Builder): 28 | """ 29 | a chain_builder that builds stack pivoting rop chains 30 | """ 31 | def __init__(self, chain_builder): 32 | super().__init__(chain_builder) 33 | self._pivot_gadgets: list = None # type: ignore 34 | 35 | def bootstrap(self): 36 | self._pivot_gadgets = self.filter_gadgets(self.chain_builder.pivot_gadgets) 37 | 38 | def pivot(self, thing): 39 | if thing.is_register: 40 | return self.pivot_reg(thing) 41 | return self.pivot_addr(thing) 42 | 43 | def pivot_addr(self, addr): 44 | for gadget in self._pivot_gadgets: 45 | # constrain the successor to be at the gadget 46 | # emulate 'pop pc' 47 | init_state = self.make_sim_state(gadget.addr) 48 | 49 | # step the gadget 50 | final_state = rop_utils.step_to_unconstrained_successor(self.project, init_state) 51 | 52 | # constrain the final sp 53 | final_state.solver.add(final_state.regs.sp == addr.data) 54 | registers = {} 55 | for x in gadget.sp_reg_controllers: 56 | registers[x] = final_state.solver.eval(init_state.registers.load(x)) 57 | chain = self.chain_builder.set_regs(**registers) 58 | 59 | try: 60 | chain.add_gadget(gadget) 61 | # iterate through the stack values that need to be in the chain 62 | sp = init_state.regs.sp 63 | arch_bytes = self.project.arch.bytes 64 | for i in range(gadget.stack_change // arch_bytes): 65 | sym_word = init_state.memory.load(sp + arch_bytes*i, arch_bytes, 66 | endness=self.project.arch.memory_endness) 67 | 68 | val = final_state.solver.eval(sym_word) 69 | chain.add_value(val) 70 | state = chain.exec() 71 | if state.solver.eval(state.regs.sp == addr.data): 72 | return chain 73 | except Exception: # pylint: disable=broad-exception-caught 74 | continue 75 | 76 | raise RopException(f"Fail to pivot the stack to {addr.data}!") 77 | 78 | def pivot_reg(self, reg_val): 79 | reg = reg_val.reg_name 80 | for gadget in self._pivot_gadgets: 81 | if reg not in gadget.sp_reg_controllers: 82 | continue 83 | 84 | init_state = self.make_sim_state(gadget.addr) 85 | final_state = rop_utils.step_to_unconstrained_successor(self.project, init_state) 86 | 87 | chain = self.chain_builder.set_regs() 88 | 89 | try: 90 | chain.add_gadget(gadget) 91 | # iterate through the stack values that need to be in the chain 92 | sp = init_state.regs.sp 93 | arch_bytes = self.project.arch.bytes 94 | for i in range(gadget.stack_change // arch_bytes): 95 | sym_word = init_state.memory.load(sp + arch_bytes*i, arch_bytes, 96 | endness=self.project.arch.memory_endness) 97 | 98 | val = final_state.solver.eval(sym_word) 99 | chain.add_value(val) 100 | state = chain.exec() 101 | variables = set(state.regs.sp.variables) 102 | if len(variables) == 1 and variables.pop().startswith(f'reg_{reg}'): 103 | return chain 104 | else: 105 | chain_str = chain.dstr() 106 | l.exception("Somehow angrop thinks\n%s\ncan be use for stack pivoting", chain_str) 107 | except Exception: # pylint: disable=broad-exception-caught 108 | continue 109 | 110 | raise RopException(f"Fail to pivot the stack to {reg}!") 111 | 112 | def _same_effect(self, g1, g2): 113 | if g1.sp_controllers != g2.sp_controllers: 114 | return False 115 | if g1.stack_change != g2.stack_change: 116 | return False 117 | if g1.stack_change_after_pivot != g2.stack_change_after_pivot: 118 | return False 119 | return True 120 | 121 | def _better_than(self, g1, g2): 122 | if g1.num_sym_mem_access > g2.num_sym_mem_access: 123 | return False 124 | if not g1.changed_regs.issubset(g2.changed_regs): 125 | return False 126 | if g1.isn_count > g2.isn_count: 127 | return False 128 | return True 129 | 130 | def filter_gadgets(self, gadgets): 131 | gadgets = [x for x in gadgets if not x.has_conditional_branch] 132 | gadgets = self._filter_gadgets(gadgets) 133 | return sorted(gadgets, key=functools.cmp_to_key(cmp)) 134 | -------------------------------------------------------------------------------- /angrop/chain_builder/reg_mover.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import itertools 3 | from collections import defaultdict 4 | 5 | import networkx as nx 6 | from angr.errors import SimUnsatError 7 | 8 | from .builder import Builder 9 | from .. import rop_utils 10 | from ..rop_chain import RopChain 11 | from ..rop_block import RopBlock 12 | from ..errors import RopException 13 | from ..rop_gadget import RopRegMove 14 | 15 | l = logging.getLogger(__name__) 16 | 17 | class RegMover(Builder): 18 | """ 19 | handle register moves such as `mov rax, rcx` 20 | """ 21 | def __init__(self, chain_builder): 22 | super().__init__(chain_builder) 23 | self._reg_moving_blocks: set[RopBlock] = None # type: ignore 24 | self._graph: nx.Graph = None # type: ignore 25 | 26 | def bootstrap(self): 27 | reg_moving_gadgets = self.filter_gadgets(self.chain_builder.gadgets) 28 | self._reg_moving_blocks = {g for g in reg_moving_gadgets if g.self_contained} 29 | self._build_move_graph() 30 | 31 | def _build_move_graph(self): 32 | self._graph = nx.DiGraph() 33 | graph = self._graph 34 | # each node is a register 35 | graph.add_nodes_from(self.arch.reg_set) 36 | # an edge means there is a move from the src register to the dst register 37 | objects = defaultdict(set) 38 | for block in self._reg_moving_blocks: 39 | for move in block.reg_moves: 40 | objects[(move.from_reg, move.to_reg)].add(block) 41 | for key, val in objects.items(): 42 | graph.add_edge(key[0], key[1], block=val) 43 | 44 | def verify(self, chain, preserve_regs, registers): 45 | """ 46 | given a potential chain, verify whether the chain can move the registers correctly by symbolically 47 | execute the chain 48 | """ 49 | chain_str = chain.dstr() 50 | state = chain.exec() 51 | for reg, val in registers.items(): 52 | bv = getattr(state.regs, reg) 53 | if bv.depth != 1 or type(bv.args[0]) != str or val.reg_name not in bv._encoded_name.decode(): 54 | l.exception("Somehow angrop thinks \n%s\n can be used for the chain generation.", chain_str) 55 | return False 56 | for act in state.history.actions.hardcopy: 57 | if act.type not in ("mem", "reg"): 58 | continue 59 | if act.type == 'mem': 60 | if act.addr.ast.variables: 61 | l.exception("memory access outside stackframe\n%s\n", chain_str) 62 | return False 63 | if act.type == 'reg' and act.action == 'write': 64 | # get the full name of the register 65 | offset = act.offset 66 | offset -= act.offset % self.project.arch.bytes 67 | reg_name = self.project.arch.translate_register_name(offset) 68 | if reg_name in preserve_regs: 69 | l.exception("Somehow angrop thinks \n%s\n can be used for the chain generation.", chain_str) 70 | return False 71 | # the next pc must be "next_pc" 72 | if len(state.regs.pc.variables) != 1: 73 | return False 74 | if not set(state.regs.pc.variables).pop().startswith("next_pc_"): 75 | return False 76 | return True 77 | 78 | def _recursively_find_chains(self, gadgets, chain, source_regs, hard_preserve_regs, todo_moves): 79 | """ 80 | source_regs: registers that contain the original values of the source registers 81 | """ 82 | # FIXME: what if the first gadget moves the second move.from_reg to another reg? 83 | if not todo_moves: 84 | return [chain] 85 | 86 | todo_list = [] 87 | for g in gadgets: 88 | new_moves = set(g.reg_moves).intersection(todo_moves) 89 | if not new_moves: 90 | continue 91 | if g.changed_regs.intersection(hard_preserve_regs): 92 | continue 93 | new_source_regs = set() 94 | for move in new_moves: 95 | if move.from_reg in source_regs: 96 | new_source_regs.add(move.to_reg) 97 | g_source_regs = source_regs.copy() 98 | g_source_regs -= g.changed_regs 99 | g_source_regs.update(new_source_regs) 100 | new_todo_moves = todo_moves - new_moves 101 | if any(m.from_reg not in g_source_regs for m in new_todo_moves): 102 | continue 103 | new_preserve = hard_preserve_regs.copy() 104 | new_preserve.update({x.to_reg for x in new_moves}) 105 | new_chain = chain.copy() 106 | new_chain.append(g) 107 | todo_list.append((new_chain, g_source_regs, new_preserve, new_todo_moves)) 108 | 109 | res = [] 110 | for todo in todo_list: 111 | res += self._recursively_find_chains(gadgets, *todo) 112 | return res 113 | 114 | def run(self, preserve_regs=None, **registers): 115 | if len(registers) == 0: 116 | return RopChain(self.project, self, badbytes=self.badbytes) 117 | 118 | # sanity check 119 | preserve_regs = set(preserve_regs) if preserve_regs else set() 120 | unknown_regs = set(registers.keys()).union(preserve_regs) - self.arch.reg_set 121 | if unknown_regs: 122 | raise RopException("unknown registers: %s" % unknown_regs) 123 | 124 | # cast values to RopValue 125 | for x in registers: 126 | registers[x] = rop_utils.cast_rop_value(registers[x], self.project) 127 | 128 | # find all blocks that are relevant to our moves 129 | assert all(val.is_register for _, val in registers.items()) 130 | moves = {RopRegMove(val.reg_name, reg, self.project.arch.bits) for reg, val in registers.items()} 131 | rop_blocks = self._find_relevant_blocks(moves) 132 | 133 | # use greedy algorithm to find a chain that can do all the moves 134 | source_regs = {x.from_reg for x in moves} 135 | chains = self._recursively_find_chains(rop_blocks, [], source_regs, preserve_regs.copy(), moves) 136 | chains = self._sort_chains(chains) 137 | 138 | # now see whether any of the chain candidates can work 139 | for rop_blocks in chains: 140 | chain_str = "\n".join(g.dstr() for g in rop_blocks) 141 | l.debug("building reg_setting chain with chain:\n%s", chain_str) 142 | try: 143 | rb = rop_blocks[0] 144 | for x in rop_blocks[1:]: 145 | rb += x 146 | if self.verify(rb, preserve_regs, registers): 147 | return rb 148 | except (RopException, SimUnsatError): 149 | pass 150 | 151 | raise RopException("Couldn't move registers :(") 152 | 153 | def _find_relevant_blocks(self, target_moves): 154 | """ 155 | find rop_blocks that may perform any of the requested moves 156 | """ 157 | rop_blocks = set() 158 | 159 | # handle moves using graph search, this allows gadget chaining 160 | # to perform hard moves that requires multiple gadgets 161 | graph = self._graph 162 | for move in target_moves: 163 | # only consider the shortest path 164 | # TODO: we should use longer paths if the shortest one does work 165 | paths = nx.all_shortest_paths(graph, source=move.from_reg, target=move.to_reg) 166 | block_gadgets = [] 167 | for path in paths: 168 | edges = zip(path, path[1:]) 169 | edge_block_list = [] 170 | for edge in edges: 171 | edge_blocks = graph.get_edge_data(edge[0], edge[1])['block'] 172 | edge_block_list.append(edge_blocks) 173 | block_gadgets += list(itertools.product(*edge_block_list)) 174 | 175 | # now turn them into blocks 176 | for gs in block_gadgets: 177 | assert gs 178 | rb = RopBlock.from_gadget_list(gs, self) 179 | rop_blocks.add(rb) 180 | return rop_blocks 181 | 182 | def filter_gadgets(self, gadgets): 183 | """ 184 | filter gadgets having the same effect 185 | """ 186 | # first: filter out gadgets that don't do register move 187 | gadgets = {g for g in gadgets if g.reg_moves and not g.has_conditional_branch} 188 | gadgets = self._filter_gadgets(gadgets) 189 | new_gadgets = set(x for x in gadgets if any(y.from_reg != y.to_reg for y in x.reg_moves)) 190 | return new_gadgets 191 | 192 | def _same_effect(self, g1, g2): 193 | """ 194 | having the same register moving effect compared to the other gadget 195 | """ 196 | if set(g1.reg_moves) != set(g2.reg_moves): 197 | return False 198 | if g1.reg_dependencies != g2.reg_dependencies: 199 | return False 200 | return True 201 | 202 | def _better_than(self, g1, g2): 203 | if g1.stack_change <= g2.stack_change and \ 204 | g1.num_sym_mem_access <= g2.num_sym_mem_access and \ 205 | g1.isn_count <= g2.isn_count: 206 | return True 207 | return False 208 | -------------------------------------------------------------------------------- /angrop/chain_builder/shifter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import defaultdict 3 | 4 | from .. import rop_utils 5 | from .builder import Builder 6 | from ..rop_chain import RopChain 7 | from ..errors import RopException 8 | 9 | l = logging.getLogger(__name__) 10 | 11 | class Shifter(Builder): 12 | """ 13 | A class to find stack shifting gadgets, like add rsp; ret or pop chains 14 | """ 15 | def __init__(self, chain_builder): 16 | super().__init__(chain_builder) 17 | 18 | self.shift_gadgets: dict = None # type: ignore 19 | 20 | def bootstrap(self): 21 | self.shift_gadgets = self.filter_gadgets(self.chain_builder.gadgets) 22 | 23 | def verify_shift(self, chain, length, preserve_regs): 24 | arch_bytes = self.project.arch.bytes 25 | init_sp = chain._blank_state.regs.sp.concrete_value 26 | state = chain.exec() 27 | if state.regs.sp.concrete_value != init_sp + length + arch_bytes: 28 | return False 29 | for act in state.history.actions: 30 | if act.type != 'reg' or act.action != 'write': 31 | continue 32 | offset = act.offset 33 | offset -= act.offset % self.project.arch.bytes 34 | reg_name = self.project.arch.translate_register_name(offset) 35 | if reg_name in preserve_regs: 36 | chain_str = chain.dstr() 37 | l.exception("Somehow angrop thinks \n%s\n can be used for the chain generation.", chain_str) 38 | return False 39 | return True 40 | 41 | def verify_retsled(self, chain, size, preserve_regs): 42 | if len(chain.payload_str()) != size: 43 | return False 44 | state = chain.exec() 45 | for act in state.history.actions: 46 | if act.type != 'reg' or act.action != 'write': 47 | continue 48 | offset = act.offset 49 | offset -= act.offset % self.project.arch.bytes 50 | reg_name = self.project.arch.translate_register_name(offset) 51 | if reg_name == self.arch.stack_pointer: 52 | continue 53 | if reg_name in preserve_regs: 54 | chain_str = chain.dstr() 55 | l.exception("Somehow angrop thinks \n%s\n can be used for the chain generation.", chain_str) 56 | return False 57 | return True 58 | 59 | def shift(self, length, preserve_regs=None, next_pc_idx=-1): 60 | """ 61 | length: how many bytes to shift 62 | preserve_regs: what registers not to clobber 63 | next_pc_idx: where is the next pc, e.g for ret, it is -1 64 | """ 65 | preserve_regs = set(preserve_regs) if preserve_regs else set() 66 | arch_bytes = self.project.arch.bytes 67 | 68 | if length % arch_bytes != 0: 69 | raise RopException("Currently, we do not support shifting misaligned sp change") 70 | if length not in self.shift_gadgets or \ 71 | all(preserve_regs.intersection(x.changed_regs) for x in self.shift_gadgets[length]): 72 | raise RopException("Encounter a shifting request that requires chaining multiple shifting gadgets " + 73 | "together which is not support atm. Plz create an issue on GitHub " + 74 | "so we can add the support!") 75 | g_cnt = length // arch_bytes 76 | next_pc_idx = (next_pc_idx % g_cnt + g_cnt) % g_cnt # support negative indexing 77 | for g in self.shift_gadgets[length]: 78 | if preserve_regs.intersection(g.changed_regs): 79 | continue 80 | if g.transit_type != 'pop_pc': 81 | continue 82 | if g.pc_offset != next_pc_idx*arch_bytes: 83 | continue 84 | try: 85 | chain = RopChain(self.project, self.chain_builder) 86 | chain.add_gadget(g) 87 | for idx in range(g_cnt): 88 | if idx != next_pc_idx: 89 | chain.add_value(self._get_fill_val()) 90 | else: 91 | next_pc_val = rop_utils.cast_rop_value( 92 | chain._blank_state.solver.BVS("next_pc", self.project.arch.bits), 93 | self.project, 94 | ) 95 | chain.add_value(next_pc_val) 96 | if self.verify_shift(chain, length, preserve_regs): 97 | return chain 98 | except RopException: 99 | continue 100 | 101 | raise RopException(f"Failed to shift sp for {length:#x} bytes while preserving {preserve_regs}") 102 | 103 | def retsled(self, size, preserve_regs=None): 104 | preserve_regs = set(preserve_regs) if preserve_regs else set() 105 | arch_bytes = self.project.arch.bytes 106 | 107 | if size % arch_bytes != 0: 108 | raise RopException("the size of a retsled must be word aligned") 109 | if not self.shift_gadgets[arch_bytes]: 110 | raise RopException("fail to find a ret-equivalent gadget in this binary!") 111 | for g in self.shift_gadgets[arch_bytes]: 112 | try: 113 | chain = RopChain(self.project, self.chain_builder) 114 | for _ in range(size//arch_bytes): 115 | chain.add_gadget(g) 116 | if self.verify_retsled(chain, size, preserve_regs): 117 | return chain 118 | except RopException: 119 | continue 120 | 121 | raise RopException(f"Failed to create a ret-sled sp for {size:#x} bytes while preserving {preserve_regs}") 122 | 123 | def _same_effect(self, g1, g2): 124 | if g1.stack_change != g2.stack_change: 125 | return False 126 | if g1.transit_type != g2.transit_type: 127 | return False 128 | if g1.pc_offset != g2.pc_offset: 129 | return False 130 | return True 131 | 132 | def _better_than(self, g1, g2): 133 | if g1.num_sym_mem_access > g2.num_sym_mem_access: 134 | return False 135 | if not g1.changed_regs.issubset(g2.changed_regs): 136 | return False 137 | if g1.isn_count > g2.isn_count: 138 | return False 139 | return True 140 | 141 | def filter_gadgets(self, gadgets): 142 | """ 143 | filter gadgets having the same effect 144 | """ 145 | # we don't like gadgets with any memory accesses 146 | gadgets = [ 147 | x 148 | for x in gadgets 149 | if x.num_sym_mem_access == 0 150 | and x.self_contained 151 | ] 152 | 153 | gadgets = self._filter_gadgets(gadgets) 154 | 155 | d = defaultdict(list) 156 | for g in gadgets: 157 | d[g.stack_change].append(g) 158 | for x in d: 159 | d[x] = sorted(d[x], key=lambda g: len(g.changed_regs)) 160 | return d 161 | -------------------------------------------------------------------------------- /angrop/chain_builder/sys_caller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import functools 3 | 4 | import angr 5 | 6 | from .func_caller import FuncCaller 7 | from ..errors import RopException 8 | from ..import rop_utils 9 | 10 | l = logging.getLogger(__name__) 11 | 12 | def cmp(g1, g2): 13 | if g1.can_return and not g2.can_return: 14 | return -1 15 | if not g1.can_return and g2.can_return: 16 | return 1 17 | 18 | if g1.num_sym_mem_access < g2.num_sym_mem_access: 19 | return -1 20 | if g1.num_sym_mem_access > g2.num_sym_mem_access: 21 | return 1 22 | 23 | if g1.stack_change < g2.stack_change: 24 | return -1 25 | if g1.stack_change > g2.stack_change: 26 | return 1 27 | 28 | if g1.isn_count < g2.isn_count: 29 | return -1 30 | if g1.isn_count > g2.isn_count: 31 | return 1 32 | return 0 33 | 34 | class SysCaller(FuncCaller): 35 | """ 36 | handle linux system calls invocations 37 | """ 38 | def __init__(self, chain_builder): 39 | super().__init__(chain_builder) 40 | 41 | self.syscall_gadgets: list = None # type: ignore 42 | self.sysnum_reg = self.project.arch.register_names[self.project.arch.syscall_num_offset] 43 | 44 | @staticmethod 45 | def supported_os(os): 46 | return "unix" in os.lower() 47 | 48 | def bootstrap(self): 49 | self.syscall_gadgets = self.filter_gadgets(self.chain_builder.syscall_gadgets) 50 | 51 | @staticmethod 52 | def verify(chain, registers, preserve_regs): 53 | # these registers are marked as preserved, so they are set by the user 54 | # don't verify them here 55 | registers = dict(registers) 56 | for reg in preserve_regs: 57 | if reg in registers: 58 | del registers[reg] 59 | state = chain.sim_exec_til_syscall() 60 | if state is None: 61 | return False 62 | 63 | for reg, val in registers.items(): 64 | bv = getattr(state.regs, reg) 65 | if (val.symbolic != bv.symbolic) or state.solver.eval(bv != val.data): 66 | chain_str = chain.dstr() 67 | l.exception("Somehow angrop thinks\n%s\ncan be used for the chain generation-2.\nregisters: %s", 68 | chain_str, registers) 69 | return False 70 | 71 | return True 72 | 73 | def filter_gadgets(self, gadgets) -> list: # pylint: disable=no-self-use 74 | # currently, we don't support negative stack_change 75 | # syscall gadgets 76 | gadgets = list({g for g in gadgets if g.stack_change >= 0}) 77 | return sorted(gadgets, key=functools.cmp_to_key(cmp)) 78 | 79 | def _try_invoke_execve(self, path_addr): 80 | execve_syscall = self.chain_builder.arch.execve_num 81 | # next, try to invoke execve(path, ptr, ptr), where ptr points is either NULL or nullptr 82 | if 0 not in self.badbytes: 83 | ptr = 0 84 | else: 85 | nullptr = self._get_ptr_to_null() 86 | ptr = nullptr 87 | 88 | try: 89 | return self.do_syscall(execve_syscall, [path_addr, ptr, ptr], 90 | use_partial_controllers=False, needs_return=False) 91 | except RopException: 92 | pass 93 | 94 | # Try to use partial controllers 95 | l.warning("Trying to use partial controllers for syscall") 96 | try: 97 | return self.do_syscall(execve_syscall, [path_addr, 0, 0], 98 | use_partial_controllers=True, needs_return=False) 99 | except RopException: 100 | pass 101 | 102 | raise RopException("Fail to invoke execve!") 103 | 104 | def execve(self, path=None, path_addr=None): 105 | if "unix" not in self.project.loader.main_object.os.lower(): 106 | raise RopException("unknown unix platform") 107 | if not self.syscall_gadgets: 108 | raise RopException("target does not contain syscall gadget!") 109 | 110 | # determine the execution path 111 | if path is None: 112 | path = b"/bin/sh\x00" 113 | if path[-1] != 0: 114 | path += b"\x00" 115 | 116 | # look for a good buffer to store the payload 117 | if path_addr: 118 | if self._word_contain_badbyte(path_addr): 119 | raise RopException(f"{path_addr:#x} contains bad byte!") 120 | else: 121 | # reserve a little bit more bytes to fit pointers 122 | path_addr = self._get_ptr_to_writable(len(path)+self.project.arch.bytes) 123 | if path_addr is None: 124 | raise RopException("Fail to automatically find a good pointer to a writable region") 125 | l.warning("writing to %#x", path_addr) 126 | 127 | # now, write the path to memory 128 | chain = self.chain_builder.write_to_mem(path_addr, path) 129 | 130 | # finally, let's invoke execve! 131 | chain2 = self._try_invoke_execve(path_addr) 132 | 133 | return chain + chain2 134 | 135 | def _can_set_sysnum_reg(self, syscall_num): 136 | try: 137 | self.chain_builder.set_regs(**{self.sysnum_reg: syscall_num}) 138 | except RopException: 139 | return False 140 | return True 141 | 142 | def _per_request_filtering(self, syscall_num, registers, preserve_regs, needs_return): 143 | """ 144 | filter out gadgets that cannot be used at all for the chain 145 | """ 146 | 147 | gadgets = self.syscall_gadgets 148 | if needs_return: 149 | gadgets = [x for x in gadgets if x.can_return] 150 | def concrete_val_ok(g): 151 | for key, val in g.prologue.concrete_regs.items(): 152 | if key in registers and type(registers[key]) == int and registers[key] != val: 153 | return False 154 | if key == self.sysnum_reg and val != syscall_num: 155 | return False 156 | return True 157 | gadgets = [x for x in gadgets if concrete_val_ok(x)] 158 | target_regs = dict(registers) 159 | target_regs[self.sysnum_reg] = syscall_num 160 | 161 | # now try to set sysnum_reg, if we can't do it, that means we have to rely on concrete values 162 | def set_sysnum(g): 163 | if self.sysnum_reg not in g.prologue.concrete_regs: 164 | return False 165 | return g.prologue.concrete_regs[self.sysnum_reg] == syscall_num 166 | if self.sysnum_reg not in preserve_regs and not self._can_set_sysnum_reg(syscall_num): 167 | gadgets = [g for g in gadgets if set_sysnum(g)] 168 | 169 | # prioritize gadgets that can set more arguments 170 | def key_func(g): 171 | good_sets = set() 172 | for reg, val in g.prologue.concrete_regs.items(): 173 | if target_regs[reg] == val: 174 | good_sets.add(reg) 175 | return len(good_sets) 176 | gadgets = sorted(gadgets, reverse=True, key=key_func) 177 | return gadgets 178 | 179 | def do_syscall(self, syscall_num, args, needs_return=True, **kwargs): 180 | """ 181 | build a rop chain which performs the requested system call with the arguments set to 'registers' before 182 | the call is made 183 | :param syscall_num: the syscall number to execute 184 | :param args: the register values to have set at system call time 185 | :param preserve_regs: list of registers which shouldn't be set 186 | :param needs_return: whether to continue the ROP after invoking the syscall 187 | :return: a RopChain which makes the system with the requested register contents 188 | """ 189 | if not self.syscall_gadgets: 190 | raise RopException("target does not contain syscall gadget!") 191 | 192 | # set the system call number 193 | cc = angr.SYSCALL_CC[self.project.arch.name]["default"](self.project.arch) 194 | 195 | # find small stack change syscall gadget that also fits the stack arguments we want 196 | # FIXME: does any arch/OS take syscall arguments on stack? (windows? sysenter?) 197 | if len(args) > len(cc.ARG_REGS): 198 | raise NotImplementedError("Currently, we can't handle on stack system call arguments!") 199 | registers = {} 200 | for arg, reg in zip(args, cc.ARG_REGS): 201 | registers[reg] = rop_utils.cast_rop_value(arg, self.project) 202 | 203 | more = kwargs.pop('preserve_regs', set()) 204 | 205 | # do per-request gadget filtering 206 | gadgets = self._per_request_filtering(syscall_num, registers, more, needs_return) 207 | orig_registers = registers 208 | for gadget in gadgets: 209 | registers = dict(orig_registers) # create a copy of it 210 | preserve_regs = set(more) 211 | extra_regs = {self.sysnum_reg: syscall_num} 212 | 213 | # at this point, we know all the concrete_regs are good, just remove the requirementes 214 | for reg in gadget.prologue.concrete_regs: 215 | if reg in extra_regs: 216 | del extra_regs[reg] 217 | if reg in registers: 218 | preserve_regs.add(reg) 219 | 220 | # now, check whether there are clobbered registers 221 | p = gadget.prologue 222 | clobbered_regs = p.changed_regs - p.popped_regs - set(p.concrete_regs.keys()) 223 | tmp = set(preserve_regs) 224 | tmp = tmp.union(registers.keys()) 225 | tmp = tmp.union(extra_regs.keys()) 226 | if clobbered_regs.intersection(tmp): 227 | continue 228 | 229 | try: 230 | chain = self._func_call(gadget, cc, args, extra_regs=extra_regs, 231 | needs_return=needs_return, preserve_regs=preserve_regs, **kwargs) 232 | if self.verify(chain, registers, more): 233 | return chain 234 | except RopException: 235 | continue 236 | 237 | raise RopException(f"Fail to invoke syscall {syscall_num} with arguments: {args}!") 238 | -------------------------------------------------------------------------------- /angrop/common.py: -------------------------------------------------------------------------------- 1 | def str_find_all(a_str, sub): 2 | start = 0 3 | while True: 4 | start = a_str.find(sub, start) 5 | if start == -1: 6 | return 7 | yield start 8 | start += 1 9 | -------------------------------------------------------------------------------- /angrop/errors.py: -------------------------------------------------------------------------------- 1 | class RegNotFoundException(Exception): 2 | pass 3 | 4 | 5 | class RopException(Exception): 6 | pass 7 | 8 | 9 | class RopTimeoutException(RopException): 10 | pass 11 | -------------------------------------------------------------------------------- /angrop/rop.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import inspect 3 | import logging 4 | from typing import cast 5 | 6 | from angr import Analysis, register_analysis 7 | 8 | from . import chain_builder 9 | from .gadget_finder import GadgetFinder 10 | from .rop_gadget import RopGadget, PivotGadget, SyscallGadget 11 | 12 | l = logging.getLogger('angrop.rop') 13 | 14 | class ROP(Analysis): 15 | """ 16 | This class is a semantic aware rop gadget finder 17 | It is a work in progress, so don't be surprised if something doesn't quite work 18 | 19 | After calling find_gadgets(), find_gadgets_single_threaded() or load_gadgets(), 20 | self.rop_gadgets, self.pivot_gadgets, self.syscall_gadgets are populated. 21 | Additionally, all public methods from ChainBuilder are copied into ROP. 22 | """ 23 | 24 | def __init__(self, only_check_near_rets=True, max_block_size=None, max_sym_mem_access=None, 25 | fast_mode=None, rebase=None, is_thumb=False, kernel_mode=False, stack_gsize=80): 26 | """ 27 | Initializes the rop gadget finder 28 | :param only_check_near_rets: If true we skip blocks that are not near rets 29 | :param max_block_size: limits the size of blocks considered, longer blocks are less likely to be good rop 30 | gadgets so we limit the size we consider 31 | :param fast_mode: True/False, if set to None makes a decision based on the size of the binary 32 | if True, skip gadgets with conditonal_branches, floating point operations, jumps 33 | allow smaller gadget size 34 | :param is_thumb: execute ROP chain in thumb mode. Only makes difference on ARM architecture. 35 | angrop does not switch mode within a rop chain 36 | :param kernel_mode: find kernel mode gadgets 37 | :param stack_gsize: change the maximum allowable stack change for gadgets 38 | :return: 39 | """ 40 | 41 | # private list of RopGadget's 42 | self._all_gadgets: list[RopGadget] = [] # all types of gadgets 43 | # all equivalent gadgets (with the same instructions) 44 | self._duplicates: dict = None # type: ignore 45 | 46 | # public list of RopGadget's 47 | self.rop_gadgets = [] # gadgets used for ROP, like pop rax; ret 48 | self.pivot_gadgets = [] # gadgets used for stack pivoting, like mov rsp, rbp; ret 49 | self.syscall_gadgets = [] # gadgets used for invoking system calls, such as syscall; ret or int 0x80; ret 50 | 51 | # RopChain settings 52 | self.badbytes = [] 53 | self.roparg_filler = None 54 | 55 | # gadget finder configurations 56 | self.gadget_finder = GadgetFinder(self.project, fast_mode=fast_mode, only_check_near_rets=only_check_near_rets, 57 | max_block_size=max_block_size, max_sym_mem_access=max_sym_mem_access, 58 | is_thumb=is_thumb, kernel_mode=kernel_mode, stack_gsize=stack_gsize) 59 | self.arch = self.gadget_finder.arch 60 | 61 | # chain builder 62 | self._chain_builder = None 63 | 64 | if rebase is not None: 65 | l.warning("rebase is deprecated in angrop!") 66 | 67 | def _screen_gadgets(self): 68 | # screen gadgets based on badbytes and gadget types 69 | self.rop_gadgets = [] 70 | self.pivot_gadgets = [] 71 | self.syscall_gadgets = [] 72 | for g in self._all_gadgets: 73 | if self._contain_badbytes(g.addr): 74 | # in case the gadget contains bad byte, try to take an equivalent one from 75 | # the duplicates (other gadgets with the same instructions) 76 | block = self.project.factory.block(g.addr) 77 | h = self.gadget_finder.block_hash(block) 78 | addr = None 79 | if h not in self._duplicates: 80 | continue 81 | for addr in self._duplicates[h]: 82 | if not self._contain_badbytes(addr): 83 | break 84 | if not addr: 85 | continue 86 | g = self.gadget_finder.analyze_gadget(addr) 87 | if type(g) is RopGadget: 88 | self.rop_gadgets.append(g) 89 | if type(g) is PivotGadget: 90 | self.pivot_gadgets.append(g) 91 | if type(g) is SyscallGadget: 92 | self.syscall_gadgets.append(g) 93 | 94 | self.chain_builder.gadgets = self.rop_gadgets 95 | self.chain_builder.pivot_gadgets = self.pivot_gadgets 96 | self.chain_builder.syscall_gadgets = self.syscall_gadgets 97 | self.chain_builder.bootstrap() 98 | 99 | def analyze_addr(self, addr): 100 | """ 101 | return a list of gadgets that starts from addr 102 | this is possible because of conditional branches 103 | """ 104 | res = self.gadget_finder.analyze_gadget(addr, allow_conditional_branches=True) 105 | gs:list[RopGadget]|None = cast(list[RopGadget]|None, res) 106 | if not gs: 107 | return gs 108 | self._all_gadgets += gs 109 | self._screen_gadgets() 110 | return gs 111 | 112 | def analyze_gadget(self, addr): 113 | """ 114 | return a gadget or None, it filters out gadgets containing conditional_branches 115 | if you'd like those, use analyze_addr 116 | """ 117 | res = self.gadget_finder.analyze_gadget(addr, allow_conditional_branches=False) 118 | g = cast(RopGadget|None, res) 119 | if g is None: 120 | return g 121 | self._all_gadgets.append(g) 122 | self._screen_gadgets() 123 | return g 124 | 125 | def analyze_gadget_list(self, addr_list, processes=4, show_progress=True): 126 | """ 127 | Analyzes a list of addresses to identify ROP gadgets. 128 | Saves rop gadgets in self.rop_gadgets 129 | Saves syscall gadgets in self.syscall_gadgets 130 | Saves stack pivots in self.stack_pivots 131 | :param processes: number of processes to use 132 | :param show_progress: whether or not to show progress bar 133 | """ 134 | 135 | self._all_gadgets = self.gadget_finder.analyze_gadget_list( 136 | addr_list, processes=processes, show_progress=show_progress) 137 | self._screen_gadgets() 138 | return self.rop_gadgets 139 | 140 | def find_gadgets(self, processes=4, show_progress=True): 141 | """ 142 | Finds all the gadgets in the binary by calling analyze_gadget on every address near a ret. 143 | Saves rop gadgets in self.rop_gadgets 144 | Saves syscall gadgets in self.syscall_gadgets 145 | Saves stack pivots in self.stack_pivots 146 | :param processes: number of processes to use 147 | """ 148 | self._all_gadgets, self._duplicates = self.gadget_finder.find_gadgets(processes=processes, 149 | show_progress=show_progress) 150 | self._screen_gadgets() 151 | return self.rop_gadgets 152 | 153 | def find_gadgets_single_threaded(self, show_progress=True): 154 | """ 155 | Finds all the gadgets in the binary by calling analyze_gadget on every address near a ret 156 | Saves rop gadgets in self.rop_gadgets 157 | Saves syscall gadgets in self.syscall_gadgets 158 | Saves stack pivots in self.stack_pivots 159 | """ 160 | self._all_gadgets, self._duplicates = self.gadget_finder.find_gadgets_single_threaded( 161 | show_progress=show_progress) 162 | self._screen_gadgets() 163 | return self.rop_gadgets 164 | 165 | def _get_cache_tuple(self): 166 | all_gadgets = self._all_gadgets 167 | for g in all_gadgets: 168 | g.project = None 169 | return (all_gadgets, self._duplicates) 170 | 171 | def _load_cache_tuple(self, tup): 172 | self._all_gadgets = tup[0] 173 | self._duplicates = tup[1] 174 | for g in self._all_gadgets: 175 | g.project = self.project 176 | self._screen_gadgets() 177 | 178 | def save_gadgets(self, path): 179 | """ 180 | Saves gadgets in a file. 181 | :param path: A path for a file where the gadgets are stored 182 | """ 183 | with open(path, "wb") as f: 184 | pickle.dump(self._get_cache_tuple(), f) 185 | for g in self._all_gadgets: 186 | g.project = self.project 187 | 188 | def load_gadgets(self, path): 189 | """ 190 | Loads gadgets from a file. 191 | :param path: A path for a file where the gadgets are loaded 192 | """ 193 | with open(path, "rb") as f: 194 | cache_tuple = pickle.load(f) 195 | self._load_cache_tuple(cache_tuple) 196 | 197 | def set_badbytes(self, badbytes): 198 | """ 199 | Define badbytes which should not appear in the generated ropchain. 200 | :param badbytes: a list of 8 bit integers 201 | """ 202 | if not isinstance(badbytes, list): 203 | l.error("Require a list, e.g: [0x00, 0x09]") 204 | return 205 | badbytes = [x if type(x) == int else ord(x) for x in badbytes] 206 | self.badbytes = badbytes 207 | if self._chain_builder: 208 | self._chain_builder.set_badbytes(self.badbytes) 209 | self._screen_gadgets() 210 | 211 | def set_roparg_filler(self, roparg_filler): 212 | """ 213 | Define rop gadget filler argument. These will be used if the rop chain needs to pop 214 | useless registers. 215 | If roparg_filler is None, symbolic values will be used and the concrete values will 216 | be whatever the constraint solver chooses (usually 0). 217 | :param roparg_filler: A integer which is used when popping useless register or None. 218 | """ 219 | if not isinstance(roparg_filler, (int, type(None))): 220 | l.error("Require an integer, e.g: 0x41414141 or None") 221 | return 222 | 223 | self.roparg_filler = roparg_filler 224 | self.chain_builder.set_roparg_filler(self.roparg_filler) 225 | 226 | def get_badbytes(self): 227 | """ 228 | Returns list of badbytes. 229 | :returns the list of badbytes 230 | """ 231 | return self.badbytes 232 | 233 | @property 234 | def chain_builder(self): 235 | if self._chain_builder is not None: 236 | return self._chain_builder 237 | 238 | if len(self._all_gadgets) == 0: 239 | l.warning("Could not find gadgets for %s", self.project) 240 | l.warning("check your badbytes and make sure find_gadgets() or load_gadgets() was called.") 241 | self._chain_builder = chain_builder.ChainBuilder(self.project, self.rop_gadgets, self.pivot_gadgets, 242 | self.syscall_gadgets, self.arch, self.badbytes, 243 | self.roparg_filler) 244 | for f_name, f in inspect.getmembers(self._chain_builder, predicate=inspect.ismethod): 245 | if f_name.startswith("_"): 246 | continue 247 | setattr(self, f_name, f) 248 | return self._chain_builder 249 | 250 | # inspired by ropper 251 | def _contain_badbytes(self, addr): 252 | n_bytes = self.project.arch.bytes 253 | 254 | for b in self.badbytes: 255 | tmp_addr = addr 256 | for _ in range(n_bytes): 257 | if (tmp_addr & 0xff) == b: 258 | return True 259 | tmp_addr >>= 8 260 | return False 261 | 262 | register_analysis(ROP, 'ROP') 263 | -------------------------------------------------------------------------------- /angrop/rop_block.py: -------------------------------------------------------------------------------- 1 | from .rop_chain import RopChain 2 | from .rop_value import RopValue 3 | from .rop_gadget import RopGadget 4 | from . import rop_utils 5 | 6 | class RopBlock(RopChain): 7 | """ 8 | A mini-chain that satisfies the following conditions: 9 | 1. positive stack_change 10 | 2. no accesses outside the stack_change 11 | 3. no conditional branches: the flag should be set so the execution flow is determined 12 | 4. self-contained, in the sense that it does not require extra gadgets to maintain 13 | the contain-flow 14 | """ 15 | 16 | def __init__(self, project, builder, state=None, badbytes=None): 17 | super().__init__(project, builder, state=state, badbytes=badbytes) 18 | 19 | self.stack_change = None 20 | 21 | # register effect information 22 | self.changed_regs = set() 23 | self.popped_regs = set() 24 | # Stores the stack variables that each register depends on. 25 | # Used to check for cases where two registers are popped from the same location. 26 | self.popped_reg_vars = {} 27 | self.concrete_regs = {} 28 | self.reg_dependencies = {} # like rax might depend on rbx, rcx 29 | self.reg_controllers = {} # like rax might be able to be controlled by rbx (for any value of rcx) 30 | self.reg_moves = [] 31 | 32 | # memory effect information 33 | self.mem_reads = [] 34 | self.mem_writes = [] 35 | self.mem_changes = [] 36 | 37 | self.bbl_addrs = [] 38 | self.isn_count: int = None # type: ignore 39 | 40 | @staticmethod 41 | def new_sim_state(builder): 42 | state = builder._sim_state 43 | return state.copy() 44 | 45 | @property 46 | def num_sym_mem_access(self): 47 | accesses = set(self.mem_reads + self.mem_writes + self.mem_changes) 48 | return len([x for x in accesses if x.is_symbolic_access()]) 49 | 50 | def _chain_block(self, other): 51 | assert type(other) is RopBlock 52 | res = super().__add__(other) 53 | return res 54 | 55 | def __add__(self, other): 56 | res = self._chain_block(other) 57 | res._analyze_effect() 58 | return res 59 | 60 | def _analyze_effect(self): 61 | rb = self 62 | init_state, final_state = rb.sim_exec() 63 | 64 | ga = self._builder._gadget_analyzer 65 | 66 | # stack change 67 | ga._compute_sp_change(init_state, final_state, rb) 68 | 69 | # clear the effects 70 | rb.changed_regs = set() 71 | rb.popped_regs = set() 72 | rb.popped_reg_vars = {} 73 | rb.concrete_regs = {} 74 | rb.reg_dependencies = {} 75 | rb.reg_controllers = {} 76 | rb.reg_moves = [] 77 | rb.mem_reads = [] 78 | rb.mem_writes = [] 79 | rb.mem_changes = [] 80 | 81 | # reg effect 82 | ga._check_reg_changes(final_state, init_state, rb) 83 | ga._check_reg_change_dependencies(init_state, final_state, rb) 84 | ga._check_reg_movers(init_state, final_state, rb) 85 | 86 | # mem effect 87 | ga._analyze_concrete_regs(init_state, final_state, rb) 88 | ga._analyze_mem_access(final_state, init_state, rb) 89 | 90 | rb.bbl_addrs = list(final_state.history.bbl_addrs) 91 | project = init_state.project 92 | rb.isn_count = sum(project.factory.block(addr).instructions for addr in rb.bbl_addrs) 93 | 94 | def sim_exec(self): 95 | project = self._p 96 | # this is different RopChain.exec because the execution needs to be symbolic 97 | state = self._blank_state.copy() 98 | for idx, val in enumerate(self._values): 99 | offset = idx*project.arch.bytes 100 | state.memory.store(state.regs.sp+offset, val.data, project.arch.bytes, endness=project.arch.memory_endness) 101 | 102 | state.ip = state.stack_pop() 103 | 104 | simgr = self._p.factory.simgr(state, save_unconstrained=True) 105 | while simgr.active: 106 | simgr.step() 107 | assert len(simgr.active + simgr.unconstrained) == 1 108 | final_state = simgr.unconstrained[0] 109 | return state, final_state 110 | 111 | def import_gadget_effect(self, gadget): 112 | self.stack_change = gadget.stack_change 113 | self.changed_regs = gadget.changed_regs 114 | self.popped_regs = gadget.popped_regs 115 | self.popped_reg_vars = gadget.popped_reg_vars 116 | self.concrete_regs = gadget.concrete_regs 117 | self.reg_dependencies = gadget.reg_dependencies 118 | self.reg_controllers = gadget.reg_controllers 119 | self.reg_moves = gadget.reg_moves 120 | self.mem_reads = gadget.mem_reads 121 | self.mem_writes = gadget.mem_writes 122 | self.mem_changes = gadget.mem_changes 123 | self.isn_count = gadget.isn_count 124 | 125 | @staticmethod 126 | def from_gadget(gadget, builder): 127 | assert isinstance(gadget, RopGadget) 128 | assert gadget.stack_change > 0 129 | assert not gadget.has_conditional_branch 130 | assert gadget.transit_type == 'pop_pc' 131 | 132 | # build the block(chain) state first 133 | project = builder.project 134 | bytes_per_pop = project.arch.bytes 135 | state = RopBlock.new_sim_state(builder) 136 | next_pc_val = rop_utils.cast_rop_value( 137 | state.solver.BVS("next_pc", project.arch.bits), 138 | project, 139 | ) 140 | state.memory.store(state.regs.sp + gadget.pc_offset + bytes_per_pop, next_pc_val.ast, 141 | endness=project.arch.memory_endness) 142 | state.stack_pop() 143 | state.ip = gadget.addr 144 | 145 | # now build the block(chain) 146 | rb = RopBlock(project, builder, state=state, badbytes=builder.badbytes) 147 | rb.import_gadget_effect(gadget) 148 | 149 | # fill in values and gadgets 150 | value = RopValue(gadget.addr, project) 151 | value.rebase_analysis(chain=rb) 152 | rb.add_value(value) 153 | for offset in range(0, gadget.stack_change, bytes_per_pop): 154 | sym_word = state.stack_read(offset, bytes_per_pop) 155 | sym_val = rop_utils.cast_rop_value(sym_word, project) 156 | if offset != gadget.pc_offset: 157 | rb.add_value(sym_val) 158 | else: 159 | rb.add_value(next_pc_val) 160 | 161 | rb.set_gadgets([gadget]) 162 | return rb 163 | 164 | @staticmethod 165 | def from_gadget_list(gs, builder): 166 | assert gs 167 | rb = RopBlock.from_gadget(gs[0], builder) 168 | for g in gs[1:]: 169 | rb = rb._chain_block(RopBlock.from_gadget(g, builder)) 170 | rb._analyze_effect() 171 | return rb 172 | 173 | @staticmethod 174 | def from_chain(chain): 175 | state = chain._blank_state.copy() 176 | badbytes = chain._builder.badbytes 177 | rb = RopBlock(chain._p, chain._builder, state=state, badbytes=badbytes) 178 | rb._gadgets = chain._gadgets.copy() 179 | rb._values = chain._values.copy() 180 | rb.payload_len = chain.payload_len 181 | rb._analyze_effect() 182 | return rb 183 | 184 | def has_symbolic_access(self): 185 | accesses = set(self.mem_reads + self.mem_writes + self.mem_changes) 186 | return any(x.is_symbolic_access() for x in accesses) 187 | 188 | def copy(self): 189 | cp = super().copy() 190 | cp.changed_regs = set(self.changed_regs) 191 | cp.popped_regs = set(self.popped_regs) 192 | cp.popped_reg_vars = dict(self.popped_reg_vars) 193 | cp.concrete_regs = dict(self.concrete_regs) 194 | cp.reg_dependencies = dict(self.reg_dependencies) 195 | cp.reg_controllers = dict(self.reg_controllers) 196 | cp.stack_change = self.stack_change 197 | cp.reg_moves = list(self.reg_moves) 198 | cp.mem_reads = list(self.mem_reads) 199 | cp.mem_writes = list(self.mem_writes) 200 | cp.mem_changes = list(self.mem_changes) 201 | cp.isn_count = self.isn_count 202 | return cp 203 | -------------------------------------------------------------------------------- /angrop/rop_chain.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from . import rop_utils 4 | from .errors import RopException 5 | from .rop_gadget import RopGadget 6 | from .rop_value import RopValue 7 | 8 | CHAIN_TIMEOUT_DEFAULT = 3 9 | 10 | l = logging.getLogger("angrop.chain_builder.reg_setter") 11 | 12 | class RopChain: 13 | """ 14 | This class holds rop chains returned by the rop chain building methods such as rop.set_regs() 15 | """ 16 | cls_timeout = CHAIN_TIMEOUT_DEFAULT 17 | 18 | def __init__(self, project, builder, state=None, badbytes=None): 19 | """ 20 | """ 21 | self._p = project 22 | self._pie = self._p.loader.main_object.pic 23 | self._builder = builder 24 | 25 | self._gadgets = [] 26 | self._values = [] 27 | # use self.payload_len in presentation layer, use self._payload in internal stuff 28 | # because next_pc is an internal mechanism, we don't expose it to users 29 | self.payload_len = 0 30 | 31 | # blank state used for solving 32 | self._blank_state = self._p.factory.blank_state() if state is None else state 33 | self.badbytes = badbytes if badbytes else [] 34 | 35 | self._timeout = self.cls_timeout 36 | 37 | def __add__(self, other): 38 | # need to add the values from the other's stack and the constraints to the result state 39 | result = self.copy() 40 | o_state = other._blank_state 41 | o_stack = o_state.memory.load(o_state.regs.sp, other.payload_len) 42 | result._blank_state.memory.store(result._blank_state.regs.sp + self.payload_len, o_stack) 43 | result._blank_state.add_constraints(*o_state.solver.constraints) 44 | if not other._values: 45 | return result 46 | # add the other values and gadgets 47 | result._gadgets.extend(other._gadgets) 48 | idx = self.next_pc_idx() 49 | assert idx is not None or not self._values, "can't add to a chain that does not return!" 50 | result.payload_len = self.payload_len + other.payload_len 51 | if idx is not None: 52 | result._values[idx] = other._values[0] 53 | result._values.extend(other._values[1:]) 54 | result.payload_len -= self._p.arch.bytes 55 | else: 56 | result._values.extend(other._values) 57 | return result 58 | 59 | def set_timeout(self, timeout): 60 | self._timeout = timeout 61 | 62 | @classmethod 63 | def set_cls_timeout(cls, timeout): 64 | cls.cls_timeout = timeout 65 | 66 | def add_value(self, value): 67 | if type(value) is not RopValue: 68 | value = RopValue(value, self._p) 69 | value.rebase_analysis(chain=self) 70 | self._values.append(value) 71 | self.payload_len += self._p.arch.bytes 72 | 73 | def add_gadget(self, gadget): 74 | value = gadget.addr 75 | if self._pie: 76 | value -= self._p.loader.main_object.mapped_base 77 | value = RopValue(value, self._p) 78 | value._rebase = self._pie is True 79 | 80 | if (idx := self.next_pc_idx()) is None: 81 | self.add_value(value) 82 | else: 83 | self._values[idx] = value 84 | 85 | self._gadgets.append(gadget) 86 | 87 | def set_gadgets(self, gadgets: list[RopGadget]): 88 | self._gadgets = gadgets 89 | 90 | def add_constraint(self, cons): 91 | """ 92 | helpful if the chain contains variables 93 | """ 94 | self._blank_state.add_constraints(cons) 95 | 96 | def next_pc_idx(self): 97 | """ 98 | in some gadgets, we have this situation: 99 | pop pc,r1, which means pc is not the last popped value like ret (retn is another example) 100 | in these case, the value will be presented as symbolic "next_pc" in _values. 101 | it will be concretized when adding new gadgets or doing chain concatenation 102 | """ 103 | for idx, x in enumerate(self._values): 104 | if x.symbolic and any(y.startswith("next_pc_") for y in x.ast.variables): 105 | return idx 106 | # chains that don't return don't have next_pc value 107 | return None 108 | 109 | def find_symbol(self, addr): 110 | plt = self._p.loader.find_plt_stub_name(addr) 111 | if plt: 112 | return plt + '@plt' 113 | symbol = self._p.loader.find_symbol(addr) 114 | if symbol: 115 | return symbol.name 116 | return None 117 | 118 | def exec(self, timeout=None): 119 | """ 120 | symbolically execute the ROP chain and return the final state 121 | """ 122 | project = self._p 123 | state = self._blank_state.copy() 124 | state.solver.reload_solver([]) # remove constraints 125 | concrete_vals = self._concretize_chain_values(timeout=timeout, preserve_next_pc=True, append_shift=False) 126 | 127 | # when the chain data includes symbolic values, we need to replace the concrete values 128 | # with the user's symbolic data 129 | values = concrete_vals 130 | for idx, val in enumerate(self._values): 131 | if not val.symbolic: 132 | continue 133 | if all(var.startswith("symbolic_stack") for var in val.ast.variables): 134 | continue 135 | values[idx] = (val.data, val.rebase) 136 | 137 | # now store all those values onto the stack 138 | for idx, val in enumerate(values): 139 | offset = idx*project.arch.bytes 140 | state.memory.store(state.regs.sp+offset, val[0], project.arch.bytes, endness=project.arch.memory_endness) 141 | state.regs.pc = state.stack_pop() 142 | 143 | # execute the chain using simgr 144 | simgr = project.factory.simgr(state, save_unconstrained=True) 145 | while simgr.active: 146 | simgr.step() 147 | if len(simgr.active + simgr.unconstrained) != 1: 148 | code = self.payload_code(print_instructions=True) 149 | l.error("The following chain fails to execute!") 150 | l.error(code) 151 | raise RopException("fail to execute") 152 | return simgr.unconstrained[0] 153 | 154 | def concrete_exec_til_addr(self, target_addr): 155 | project = self._p 156 | s = project.factory.blank_state() 157 | s.memory.store(s.regs.sp, self.payload_str()) 158 | s.ip = s.stack_pop() 159 | simgr = project.factory.simgr(s) 160 | while simgr.one_active.addr != target_addr: 161 | simgr.step() 162 | assert len(simgr.active) == 1 163 | return simgr.one_active 164 | 165 | def sim_exec_til_syscall(self): 166 | project = self._p 167 | state = project.factory.blank_state() 168 | for idx, val in enumerate(self._values): 169 | offset = idx*project.arch.bytes 170 | state.memory.store(state.regs.sp+offset, val.data, project.arch.bytes, endness=project.arch.memory_endness) 171 | state.ip = state.stack_pop() 172 | return rop_utils.step_to_syscall(state) 173 | 174 | def copy(self): 175 | cp = self.__class__(self._p, self._builder) 176 | cp._gadgets = list(self._gadgets) 177 | cp._values = list(self._values) 178 | cp.payload_len = self.payload_len 179 | cp._blank_state = self._blank_state.copy() 180 | cp.badbytes = self.badbytes.copy() 181 | 182 | return cp 183 | 184 | #### Solver Layer #### 185 | def __concretize_chain_values(self, constraints=None): 186 | """ 187 | with the flexibilty of chains to have symbolic values, this helper function 188 | makes the chain into a list of concrete ints before printing 189 | :param constraints: constraints to use when concretizing values 190 | :return: a list of tuples of type (int, needs_rebase) 191 | """ 192 | solver_state = self._blank_state.copy() 193 | if constraints is not None: 194 | if isinstance(constraints, (list, tuple)): 195 | for c in constraints: 196 | solver_state.add_constraints(c) 197 | else: 198 | solver_state.add_constraints(constraints) 199 | 200 | concrete_vals = [] 201 | for value in self._values: 202 | # make sure it does not have badbytes in it 203 | ast = value.data 204 | constraints = [] 205 | # for each byte, it should not be equal to any bad bytes 206 | # TODO: we should do the badbyte verification when adding values 207 | # not when concretizing them 208 | for idx in range(ast.length//8): 209 | b = ast.get_byte(idx) 210 | constraints += [ b != c for c in self.badbytes] 211 | # apply the constraints 212 | for expr in constraints: 213 | solver_state.solver.add(expr) 214 | if not solver_state.solver.satisfiable(): 215 | raise RopException("bad chain!") 216 | concrete_vals.append((solver_state.solver.eval(ast), value.rebase)) 217 | 218 | return concrete_vals 219 | 220 | def _concretize_chain_values(self, constraints=None, timeout=None, preserve_next_pc=False, append_shift=False): 221 | """ 222 | concretize chain values with a timeout 223 | """ 224 | if self.next_pc_idx() is not None and append_shift: 225 | try: 226 | # the following line is the final touch for chains ending with retn-style 227 | # gadget to make sure that the next_pc is at the end of the chain 228 | chain = self + self._builder.chain_builder.shift(self._p.arch.bytes) 229 | values = chain._concretize_chain_values( 230 | constraints=constraints, 231 | timeout=timeout, 232 | preserve_next_pc=preserve_next_pc, 233 | append_shift=False, 234 | ) 235 | return values 236 | except RopException: 237 | pass 238 | if timeout is None: 239 | timeout = self._timeout 240 | values = rop_utils.timeout(timeout)(self.__concretize_chain_values)(constraints=constraints) 241 | if not preserve_next_pc: 242 | return values 243 | idx = self.next_pc_idx() 244 | if idx is None: 245 | return values 246 | values[idx] = (self._values[idx].ast, None) 247 | 248 | return values 249 | 250 | #### Presentation Layer #### 251 | def addr_to_asmstring(self, addr): 252 | for g in self._gadgets: 253 | if g.addr == addr: 254 | return g.dstr() 255 | return "" 256 | 257 | def _is_code_ptr(self, ptr): 258 | """ 259 | try both sections and segments, some code is just mapped into 260 | executable segments not sections 261 | """ 262 | sec = self._p.loader.find_section_containing(ptr) 263 | if sec and sec.is_executable: 264 | return True 265 | seg = self._p.loader.find_segment_containing(ptr) 266 | if seg and seg.is_executable: 267 | return True 268 | return False 269 | 270 | def payload_bv(self): 271 | test_state = self._blank_state.copy() 272 | 273 | for value in reversed(self._values): 274 | test_state.stack_push(value.data) 275 | 276 | sp = test_state.regs.sp 277 | return test_state.memory.load(sp, self.payload_len) 278 | 279 | def payload_str(self, constraints=None, base_addr=None, timeout=None): 280 | """ 281 | :param base_addr: the base address of the binary 282 | :return: a string that does the rop payload 283 | """ 284 | if base_addr is None: 285 | base_addr = self._p.loader.main_object.mapped_base 286 | test_state = self._blank_state.copy() 287 | concrete_vals = self._concretize_chain_values(constraints, timeout=timeout, append_shift=True) 288 | if self.next_pc_idx() == len(self._values) - 1: 289 | concrete_vals = concrete_vals[:-1] 290 | for value, rebased in reversed(concrete_vals): 291 | if rebased: 292 | test_state.stack_push(value - self._p.loader.main_object.mapped_base + base_addr) 293 | else: 294 | test_state.stack_push(value) 295 | sp = test_state.regs.sp 296 | rop_str = test_state.solver.eval(test_state.memory.load(sp, self.payload_len), cast_to=bytes) 297 | if any(bytes([c]) in rop_str for c in self.badbytes): 298 | raise RopException() 299 | return rop_str 300 | 301 | def payload_code(self, constraints=None, print_instructions=True, timeout=None): 302 | """ 303 | :param print_instructions: prints the instructions that the rop gadgets use 304 | :return: prints the code for the rop payload 305 | """ 306 | if self._p.arch.bits == 32: 307 | pack = "p32(%#x)" 308 | pack_rebase = "p32(code_base + %#x)" 309 | else: 310 | pack = "p64(%#x)" 311 | pack_rebase = "p64(code_base + %#x)" 312 | 313 | if self._pie: 314 | payload = "code_base = 0x0\n" 315 | else: 316 | payload = "" 317 | payload += 'chain = b""\n' 318 | 319 | concrete_vals = self._concretize_chain_values(constraints, timeout=timeout, append_shift=True) 320 | if self.next_pc_idx() == len(self._values) - 1: 321 | concrete_vals = concrete_vals[:-1] 322 | for value, rebased in concrete_vals: 323 | 324 | instruction_code = "" 325 | if print_instructions : 326 | if self._is_code_ptr(value): 327 | symbol = self.find_symbol(value) 328 | if symbol: 329 | instruction_code = f"\t# {symbol}" 330 | else: 331 | asmstring = self.addr_to_asmstring(value) 332 | if asmstring != "": 333 | instruction_code = "\t# " + asmstring 334 | 335 | if rebased: 336 | value -= self._p.loader.main_object.mapped_base 337 | payload += "chain += " + pack_rebase % value + instruction_code 338 | else: 339 | payload += "chain += " + pack % value + instruction_code 340 | payload += "\n" 341 | return payload 342 | 343 | def print_payload_code(self, constraints=None, print_instructions=True): 344 | print(self.payload_code(constraints=constraints, print_instructions=print_instructions)) 345 | 346 | def __str__(self): 347 | return self.payload_code() 348 | 349 | def dstr(self): 350 | res = '' 351 | bs = self._p.arch.bytes 352 | prefix_len = bs*2+2 353 | prefix = " "*prefix_len 354 | for v in self._values: 355 | if v.symbolic: 356 | res += prefix + f" {v.ast}\n" 357 | continue 358 | for g in self._gadgets: 359 | if g.addr == v.concreted: 360 | fmt = f"%#0{prefix_len}x" 361 | res += fmt % g.addr + f": {g.dstr()}\n" 362 | break 363 | else: 364 | res += prefix + f" {v.concreted:#x}\n" 365 | return res 366 | 367 | def pp(self): 368 | print(self.dstr()) 369 | -------------------------------------------------------------------------------- /angrop/rop_gadget.py: -------------------------------------------------------------------------------- 1 | from angr import Project 2 | from .rop_utils import addr_to_asmstring 3 | 4 | class RopMemAccess: 5 | """Holds information about memory accesses 6 | Attributes: 7 | addr_dependencies (set): All the registers that affect the memory address. 8 | addr_controller (set): All the registers that can determine the symbolic memory access address by itself 9 | addr_offset (int): Constant offset in the memory address relative to register(s) 10 | addr_stack_controller (set): all the controlled gadgets on the stack that can determine the address by itself 11 | data_dependencies (set): All the registers that affect the data written. 12 | data_controller (set): All the registers that can determine the symbolic data by itself 13 | addr_constant (int): If the address is a constant it is stored here. 14 | data_constant (int): If the data is constant it is stored here. 15 | addr_size (int): Number of bits used for the address. 16 | data_size (int): Number of bits used for data 17 | """ 18 | def __init__(self): 19 | self.addr_dependencies = set() 20 | self.addr_controllers = set() 21 | self.addr_offset: int | None = None 22 | self.addr_stack_controllers = set() 23 | self.data_dependencies = set() 24 | self.data_controllers = set() 25 | self.data_stack_controllers = set() 26 | self.addr_constant = None 27 | self.data_constant = None 28 | self.addr_size = None 29 | self.data_size = None 30 | self.op = None 31 | 32 | def is_valid(self): 33 | """ 34 | the memory access address must be one of 35 | 1. constant 36 | 2. controlled by registers 37 | 3. controlled by controlled stack 38 | """ 39 | return self.addr_constant or self.addr_controllers or self.addr_stack_controllers 40 | 41 | def is_symbolic_access(self): 42 | return self.addr_controllable() or bool(self.addr_dependencies) 43 | 44 | def addr_controllable(self): 45 | return bool(self.addr_controllers or self.addr_stack_controllers) 46 | 47 | def data_controllable(self): 48 | return bool(self.data_controllers or self.data_stack_controllers) 49 | 50 | def addr_data_independent(self): 51 | return len(set(self.addr_controllers) & set(self.data_controllers)) == 0 and \ 52 | len(set(self.addr_stack_controllers) & set(self.data_stack_controllers)) == 0 53 | 54 | def __hash__(self): 55 | to_hash = sorted(self.addr_dependencies) + sorted(self.data_dependencies) + [self.addr_constant] + \ 56 | [self.data_constant] + [self.addr_size] + [self.data_size] 57 | return hash(tuple(to_hash)) 58 | 59 | def __eq__(self, other): 60 | if type(other) != RopMemAccess: 61 | return False 62 | if self.addr_dependencies != other.addr_dependencies or self.data_dependencies != other.data_dependencies: 63 | return False 64 | if self.addr_controllers != other.addr_controllers or self.data_controllers != other.data_controllers: 65 | return False 66 | if self.addr_constant != other.addr_constant or self.data_constant != other.data_constant: 67 | return False 68 | if self.addr_size != other.addr_size or self.data_size != other.data_size: 69 | return False 70 | return True 71 | 72 | class RopRegMove: 73 | """ 74 | Holds information about Register moves 75 | Attributes: 76 | from_reg (string): register that started with the data 77 | to_reg (string): register that the data was moved to 78 | bits (int): number of bits that were moved 79 | """ 80 | def __init__(self, from_reg, to_reg, bits): 81 | self.from_reg = from_reg 82 | self.to_reg = to_reg 83 | self.bits = bits 84 | 85 | def __hash__(self): 86 | return hash((self.from_reg, self.to_reg, self.bits)) 87 | 88 | def __eq__(self, other): 89 | if type(other) != RopRegMove: 90 | return False 91 | return self.from_reg == other.from_reg and self.to_reg == other.to_reg and self.bits == other.bits 92 | 93 | def __repr__(self): 94 | return f"RegMove: {self.to_reg} <= {self.from_reg} ({self.bits} bits)" 95 | 96 | class RopGadget: 97 | """ 98 | Gadget objects 99 | """ 100 | def __init__(self, addr): 101 | self.project: Project = None # type: ignore 102 | self.addr = addr 103 | self.stack_change: int = None # type: ignore 104 | 105 | # register effect information 106 | self.changed_regs = set() 107 | self.popped_regs = set() 108 | # Stores the stack variables that each register depends on. 109 | # Used to check for cases where two registers are popped from the same location. 110 | self.popped_reg_vars = {} 111 | self.concrete_regs = {} 112 | self.reg_dependencies = {} # like rax might depend on rbx, rcx 113 | self.reg_controllers = {} # like rax might be able to be controlled by rbx (for any value of rcx) 114 | self.reg_moves = [] 115 | 116 | # memory effect information 117 | self.mem_reads = [] 118 | self.mem_writes = [] 119 | self.mem_changes = [] 120 | 121 | # gadget transition 122 | # we now support the following gadget transitions 123 | # 1. pop_pc: ret, jmp [sp+X], pop pc,X,Y, retn), this type of gadgets are "self-contained" 124 | # 2. jmp_reg: jmp reg <- requires reg setting before using it (call falls here as well) 125 | # 3. jmp_mem: jmp [reg+X] <- requires mem setting before using it (call falls here as well) 126 | self.transit_type: str = None # type: ignore 127 | 128 | self.pc_offset = None # for pop_pc, ret is basically pc_offset == stack_change - arch.bytes 129 | self.pc_reg = None # for jmp_reg, which register it jumps to 130 | self.pc_target = None # for jmp_mem, where it jumps to 131 | 132 | # List of basic block addresses for gadgets with conditional branches 133 | self.bbl_addrs = [] 134 | # Registers that affect path constraints 135 | self.constraint_regs = set() 136 | # Instruction count to estimate complexity 137 | self.isn_count: int = None # type: ignore 138 | self.has_conditional_branch: bool = None # type: ignore 139 | 140 | @property 141 | def self_contained(self): 142 | """ 143 | the gadget is useable by itself, doesn't rely on the existence of other gadgets 144 | e.g. 'jmp_reg' gadgets requires another one setting the registers 145 | (a gadget like mov rax, [rsp]; add rsp, 8; jmp rax will be considered pop_pc) 146 | """ 147 | return (not self.has_conditional_branch) and self.transit_type == 'pop_pc' 148 | 149 | @property 150 | def num_sym_mem_access(self): 151 | accesses = set(self.mem_reads + self.mem_writes + self.mem_changes) 152 | return len([x for x in accesses if x.is_symbolic_access()]) 153 | 154 | def has_symbolic_access(self): 155 | accesses = set(self.mem_reads + self.mem_writes + self.mem_changes) 156 | return any(x.is_symbolic_access() for x in accesses) 157 | 158 | def dstr(self): 159 | return "; ".join(addr_to_asmstring(self.project, addr) for addr in self.bbl_addrs) 160 | 161 | def pp(self): 162 | print(self.dstr()) 163 | 164 | def __str__(self): 165 | s = "Gadget %#x\n" % self.addr 166 | s += "Stack change: %#x\n" % self.stack_change 167 | s += "Changed registers: " + str(self.changed_regs) + "\n" 168 | s += "Popped registers: " + str(self.popped_regs) + "\n" 169 | for move in self.reg_moves: 170 | s += "Register move: [%s to %s, %d bits]\n" % (move.from_reg, move.to_reg, move.bits) 171 | s += "Register dependencies:\n" 172 | for reg, deps in self.reg_dependencies.items(): 173 | controllers = self.reg_controllers.get(reg, []) 174 | dependencies = [x for x in deps if x not in controllers] 175 | s += " " + reg + ": [" + " ".join(controllers) + " (" + " ".join(dependencies) + ")]" + "\n" 176 | for mem_access in self.mem_changes: 177 | if mem_access.op == "__add__": 178 | s += "Memory add:\n" 179 | elif mem_access.op == "__sub__": 180 | s += "Memory subtract:\n" 181 | elif mem_access.op == "__or__": 182 | s += "Memory or:\n" 183 | elif mem_access.op == "__and__": 184 | s += "Memory and:\n" 185 | else: 186 | s += "Memory change:\n" 187 | if mem_access.addr_constant is None: 188 | s += " " + "address (%d bits) depends on: " % mem_access.addr_size 189 | s += str(list(mem_access.addr_dependencies)) + "\n" 190 | else: 191 | s += " " + "address (%d bits): %#x\n" % (mem_access.addr_size, mem_access.addr_constant) 192 | s += " " + "data (%d bits) depends on: " % mem_access.data_size 193 | s += str(list(mem_access.data_dependencies)) + "\n" 194 | for mem_access in self.mem_writes: 195 | s += "Memory write:\n" 196 | if mem_access.addr_constant is None: 197 | s += " " + "address (%d bits) depends on: " % mem_access.addr_size 198 | s += str(list(mem_access.addr_dependencies)) + "\n" 199 | else: 200 | s += " " + "address (%d bits): %#x\n" % (mem_access.addr_size, mem_access.addr_constant) 201 | if mem_access.data_constant is None: 202 | s += " " + "data (%d bits) depends on: " % mem_access.data_size 203 | s += str(list(mem_access.data_dependencies)) + "\n" 204 | else: 205 | s += " " + "data (%d bits): %#x\n" % (mem_access.data_size, mem_access.data_constant) 206 | for mem_access in self.mem_reads: 207 | s += "Memory read:\n" 208 | if mem_access.addr_constant is None: 209 | s += " " + "address (%d bits) depends on: " % mem_access.addr_size 210 | s += str(list(mem_access.addr_dependencies)) + "\n" 211 | else: 212 | s += " " + "address (%d bits): %#x" % (mem_access.addr_size, mem_access.addr_constant) 213 | s += " " + "data (%d bits) stored in regs:" % mem_access.data_size 214 | s += str(list(mem_access.data_dependencies)) + "\n" 215 | return s 216 | 217 | def __repr__(self): 218 | return "" % self.addr 219 | 220 | def copy(self): 221 | out = self.__class__(self.addr) 222 | out.project = self.project 223 | out.addr = self.addr 224 | out.changed_regs = set(self.changed_regs) 225 | out.popped_regs = set(self.popped_regs) 226 | out.popped_reg_vars = dict(self.popped_reg_vars) 227 | out.concrete_regs = dict(self.concrete_regs) 228 | out.reg_dependencies = dict(self.reg_dependencies) 229 | out.reg_controllers = dict(self.reg_controllers) 230 | out.stack_change = self.stack_change 231 | out.mem_reads = list(self.mem_reads) 232 | out.mem_changes = list(self.mem_changes) 233 | out.mem_writes = list(self.mem_writes) 234 | out.reg_moves = list(self.reg_moves) 235 | out.transit_type = self.transit_type 236 | out.pc_reg = self.pc_reg 237 | return out 238 | 239 | 240 | class PivotGadget(RopGadget): 241 | """ 242 | stack pivot gadget, the definition of a PivotGadget is that 243 | it can arbitrarily control the stack pointer register, and do the pivot exactly once 244 | TODO: so currently, it cannot directly construct a `pop rbp; leave ret;` 245 | chain to pivot stack 246 | """ 247 | def __init__(self, addr): 248 | super().__init__(addr) 249 | self.stack_change_after_pivot = None 250 | # TODO: sp_controllers can be registers, payload on stack, and symbolic read data 251 | # but we do not handle symbolic read data, yet 252 | self.sp_reg_controllers = set() 253 | self.sp_stack_controllers = set() 254 | 255 | def __str__(self): 256 | s = f"PivotGadget {self.addr:#x}\n" 257 | s += f" sp_controllers: {self.sp_controllers}\n" 258 | s += f" stack change: {self.stack_change:#x}\n" 259 | s += f" stack change after pivot: {self.stack_change_after_pivot:#x}\n" 260 | return s 261 | 262 | @property 263 | def sp_controllers(self): 264 | s = self.sp_reg_controllers.copy() 265 | return s.union(self.sp_stack_controllers) 266 | 267 | def __repr__(self): 268 | return f"" 269 | 270 | def copy(self): 271 | 272 | new = super().copy() 273 | new.stack_change_after_pivot = self.stack_change_after_pivot 274 | new.sp_reg_controllers = set(self.sp_reg_controllers) 275 | new.sp_stack_controllers = set(self.sp_stack_controllers) 276 | return new 277 | 278 | class SyscallGadget(RopGadget): 279 | """ 280 | we collect two types of syscall gadgets: 281 | 1. with return: syscall; ret 282 | 2. without return: syscall; xxxx 283 | """ 284 | def __init__(self, addr): 285 | super().__init__(addr) 286 | self.prologue: RopGadget = None # type: ignore 287 | 288 | def __str__(self): 289 | s = f"SyscallGadget {self.addr:#x}\n" 290 | s += f" stack change: {self.stack_change:#x}\n" 291 | s += f" can return: {self.can_return}\n" 292 | return s 293 | 294 | def __repr__(self): 295 | return f"" 296 | 297 | @property 298 | def can_return(self): 299 | return self.transit_type is not None 300 | 301 | def copy(self): 302 | new = super().copy() 303 | new.prologue = self.prologue 304 | return new 305 | 306 | class FunctionGadget(RopGadget): 307 | """ 308 | a function call 309 | """ 310 | def __init__(self, addr, symbol): 311 | super().__init__(addr) 312 | self.symbol = symbol 313 | 314 | def dstr(self): 315 | if self.symbol: 316 | return f"<{self.symbol}>" 317 | return f"" 318 | -------------------------------------------------------------------------------- /angrop/rop_utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import signal 3 | 4 | import angr 5 | import claripy 6 | from angr.engines.successors import SimSuccessors 7 | 8 | from .errors import RegNotFoundException, RopException, RopTimeoutException 9 | from .rop_value import RopValue 10 | 11 | def addr_to_asmstring(project, addr): 12 | block = project.factory.block(addr) 13 | return "; ".join(["%s %s" %(i.mnemonic, i.op_str) for i in block.capstone.insns]) 14 | 15 | 16 | def get_ast_dependency(ast) -> set: 17 | """ 18 | ast must be created from a symbolic state where registers values are named "sreg_REG-" 19 | looks for registers that if we make the register symbolic then the ast becomes symbolic 20 | :param ast: the ast of which we are trying to analyze dependencies 21 | :return: A set of register names which affect the ast 22 | """ 23 | dependencies = set() 24 | 25 | for var in ast.variables: 26 | if var.startswith("sreg_"): 27 | dependencies.add(var[5:].split("-")[0]) 28 | else: 29 | return set() 30 | return dependencies 31 | 32 | 33 | def get_ast_controllers(state, ast, reg_deps) -> set: 34 | """ 35 | looks for registers that we can make symbolic then the ast can be "anything" 36 | :param state: the input state 37 | :param ast: the ast of which we are trying to analyze controllers 38 | :param reg_deps: All registers which it depends on 39 | :return: A set of register names which can control the ast 40 | """ 41 | 42 | test_val = 0x4141414141414141 % (2 << state.arch.bits) 43 | 44 | controllers = set() 45 | if not ast.symbolic: 46 | return controllers 47 | 48 | # make sure it can't be symbolic if all the registers are constrained 49 | constraints = [] 50 | for reg in reg_deps: 51 | if not state.registers.load(reg).symbolic: 52 | continue 53 | constraints.append(state.registers.load(reg) == test_val) 54 | if len(state.solver.eval_upto(ast, 2, extra_constraints=constraints)) > 1: 55 | return controllers 56 | 57 | for reg in reg_deps: 58 | extra_constraints = [] 59 | for r in [a for a in reg_deps if a != reg]: 60 | # for bp and registers that might be set 61 | if not state.registers.load(r).symbolic: 62 | continue 63 | extra_constraints.append(state.registers.load(r) == test_val) 64 | 65 | if unconstrained_check(state, ast, extra_constraints=extra_constraints): 66 | controllers.add(reg) 67 | 68 | return controllers 69 | 70 | 71 | def get_ast_const_offset(state, ast, reg_deps) -> int: 72 | """ 73 | Gets the constant offset for a memory access 74 | :param state: the input state 75 | :param ast: the ast of which we are trying to analyze controllers 76 | :param reg_deps: All registers which it depends on 77 | :return: Constant value 78 | """ 79 | size = ast.size() 80 | zero_val = claripy.BVV(0, size) 81 | 82 | # Replace symbolic values with zero to get the constant value 83 | # This is faster than eval with extra contraints 84 | for reg in reg_deps: 85 | reg_val = state.registers.load(reg) 86 | ast = claripy.algorithm.replace( 87 | expr=ast, old=reg_val, new=zero_val) 88 | 89 | assert not ast.symbolic 90 | return state.solver.eval(ast) 91 | 92 | 93 | def unconstrained_check(state, ast, extra_constraints=None): 94 | """ 95 | Attempts to check if an ast is completely unconstrained 96 | :param state: the state to use 97 | :param ast: the ast to check 98 | :return: True if the ast is probably completely unconstrained 99 | """ 100 | size = ast.size() 101 | test_val_0 = 0x0 102 | test_val_1 = (1 << size) - 1 103 | test_val_2 = int("1010"*16, 2) % (1 << size) 104 | test_val_3 = int("0101"*16, 2) % (1 << size) 105 | # chars need to be able to be different 106 | test_val_4 = int(("1001"*2 + "1010"*2 + "1011"*2 + "1100"*2 + "1101"*2 + "1110"*2 + "1110"*2 + "0001"*2), 2) \ 107 | % (1 << size) 108 | extra = extra_constraints if extra_constraints is not None else [] 109 | 110 | if not state.solver.satisfiable(extra_constraints= extra + [ast == test_val_0]): 111 | return False 112 | if not state.solver.satisfiable(extra_constraints= extra + [ast == test_val_1]): 113 | return False 114 | if not state.solver.satisfiable(extra_constraints= extra + [ast == test_val_2]): 115 | return False 116 | if not state.solver.satisfiable(extra_constraints= extra + [ast == test_val_3]): 117 | return False 118 | if not state.solver.satisfiable(extra_constraints= extra + [ast == test_val_4]): 119 | return False 120 | return True 121 | 122 | 123 | def fast_unconstrained_check(state, ast): 124 | """ 125 | Attempts to check if an ast has any common unreversable operations mul, div 126 | :param state: the state to use 127 | :param ast: the ast to check 128 | :return: True if the ast is probably unconstrained 129 | """ 130 | good_ops = {"Extract", "BVS", "__add__", "__sub__", "Reverse"} 131 | if len(ast.variables) != 1: 132 | return unconstrained_check(state, ast) 133 | 134 | passes_prefilter = True 135 | for a in ast.children_asts(): 136 | if a.op not in good_ops: 137 | passes_prefilter = False 138 | if ast.op not in good_ops: 139 | passes_prefilter = False 140 | 141 | if passes_prefilter: 142 | return True 143 | 144 | return unconstrained_check(state, ast) 145 | 146 | 147 | def get_reg_name(arch, reg_offset): 148 | """ 149 | :param reg_offset: Tries to find the name of a register given the offset in the registers. 150 | :return: The register name 151 | """ 152 | # todo does this make sense 153 | if reg_offset is None: 154 | raise RegNotFoundException("register offset is None") 155 | 156 | original_offset = reg_offset 157 | while reg_offset >= 0 and reg_offset >= original_offset - arch.bytes: 158 | if reg_offset in arch.register_names: 159 | return arch.register_names[reg_offset] 160 | else: 161 | reg_offset -= 1 162 | raise RegNotFoundException("register %s not found" % str(original_offset)) 163 | 164 | 165 | # todo this doesn't work if there is a timeout 166 | def _asts_must_be_equal(state, ast1, ast2): 167 | """ 168 | :param state: the state to use for solving 169 | :param ast1: first ast 170 | :param ast2: second ast 171 | :return: True if the ast's must be equal 172 | """ 173 | if state.solver.satisfiable(extra_constraints=(ast1 != ast2,)): 174 | return False 175 | return True 176 | 177 | 178 | def fast_uninitialized_filler(_, addr, size, state): 179 | return state.solver.BVS("uninitialized" + hex(addr), size, explicit_name=True) 180 | 181 | 182 | def make_initial_state(project, stack_gsize, fast_mode=False): 183 | """ 184 | :return: an initial state with a symbolic stack and good options for rop 185 | """ 186 | # create a new plugin for memory 187 | # the purpose of this plugin is to optimize away some slowness with the default uninitialized memory 188 | class SpecialMem(angr.storage.memory_mixins.SpecialFillerMixin, angr.storage.DefaultMemory): 189 | """ 190 | class to use angr's SpecialFillerMixin to replace uninitialized memory 191 | """ 192 | def __init__(self, **kwargs): 193 | super().__init__(**kwargs) 194 | 195 | angr.SimState.register_default("sym_memory", SpecialMem) 196 | 197 | remove_set = angr.options.resilience | angr.options.simplification 198 | if fast_mode: 199 | remove_set.add(angr.options.SUPPORT_FLOATING_POINT) 200 | add_set = {angr.options.AVOID_MULTIVALUED_READS, angr.options.AVOID_MULTIVALUED_WRITES, 201 | angr.options.NO_SYMBOLIC_JUMP_RESOLUTION, angr.options.CGC_NO_SYMBOLIC_RECEIVE_LENGTH, 202 | angr.options.NO_SYMBOLIC_SYSCALL_RESOLUTION, angr.options.TRACK_ACTION_HISTORY, 203 | angr.options.ADD_AUTO_REFS, angr.options.SPECIAL_MEMORY_FILL} 204 | 205 | initial_state = project.factory.blank_state( 206 | special_memory_filler=fast_uninitialized_filler, 207 | add_options=add_set, remove_options=remove_set) 208 | 209 | initial_state.options.discard(angr.options.CGC_ZERO_FILL_UNCONSTRAINED_MEMORY) 210 | initial_state.options.update({angr.options.TRACK_REGISTER_ACTIONS, angr.options.TRACK_MEMORY_ACTIONS, 211 | angr.options.TRACK_JMP_ACTIONS, angr.options.TRACK_CONSTRAINT_ACTIONS}) 212 | symbolic_stack = claripy.Concat(*[ 213 | initial_state.solver.BVS(f"symbolic_stack_{i}", project.arch.bits) for i in range(stack_gsize) 214 | ]) 215 | initial_state.memory.store(initial_state.regs.sp, symbolic_stack) 216 | if initial_state.arch.bp_offset != initial_state.arch.sp_offset: 217 | initial_state.regs.bp = initial_state.regs.sp + 20*initial_state.arch.bytes 218 | initial_state.solver._solver.timeout = 500 # only solve for half a second at most 219 | 220 | angr.SimState.register_default("sym_memory", angr.storage.DefaultMemory) 221 | 222 | return initial_state 223 | 224 | 225 | def make_symbolic_state(project, reg_set, extra_reg_set=None, stack_gsize=80, fast_mode=False): 226 | """ 227 | converts an input state into a state with symbolic registers 228 | :return: the symbolic state 229 | """ 230 | if extra_reg_set is None: 231 | extra_reg_set = set() 232 | input_state = make_initial_state(project, stack_gsize, fast_mode) 233 | symbolic_state = input_state.copy() 234 | # overwrite all registers 235 | for reg in reg_set: 236 | symbolic_state.registers.store(reg, symbolic_state.solver.BVS("sreg_" + reg + "-", project.arch.bits)) 237 | # extra regs have a different name so they aren't processed 238 | for reg in extra_reg_set: 239 | symbolic_state.registers.store(reg, symbolic_state.solver.BVS("esreg_" + reg + "-", project.arch.bits)) 240 | 241 | # vex registers should be symbolic set once 242 | for reg in ("cc_ndep", "cc_dep1", "cc_dep2"): 243 | if reg in symbolic_state.arch.registers: 244 | symbolic_state.registers.store(reg, symbolic_state.solver.BVS("badreg_" + reg + "-", project.arch.bits)) 245 | 246 | # restore sp 247 | symbolic_state.regs.sp = input_state.regs.sp 248 | return symbolic_state 249 | 250 | def make_reg_symbolic(state, reg): 251 | state.registers.store(reg, 252 | state.solver.BVS("sreg_" + reg + "-", state.arch.bits)) 253 | 254 | def cast_rop_value(val, project): 255 | if not isinstance(val, RopValue): 256 | val = RopValue(val, project) 257 | val.rebase_analysis() 258 | return val 259 | 260 | def is_in_kernel(project, state): 261 | ip = state.ip 262 | if not ip.symbolic: 263 | return is_kernel_addr(project, ip.concrete_value) 264 | return False 265 | 266 | def is_kernel_addr(project, addr): 267 | obj = project.loader.find_object_containing(addr) 268 | if obj is None: 269 | return False 270 | if obj.binary == 'cle##kernel': 271 | return True 272 | return False 273 | 274 | def step_one_block(project, state, stop_at_syscall=False): 275 | block = state.block() 276 | num_insts = len(block.capstone.insns) 277 | 278 | if not num_insts: 279 | raise RopException("No instructions!") 280 | 281 | if project.is_hooked(state.addr): 282 | succ = project.factory.successors(state) 283 | return succ, None 284 | 285 | if is_in_kernel(project, state): 286 | succ = project.factory.successors(state) 287 | if stop_at_syscall: 288 | return None, succ.flat_successors[0] 289 | return succ, None 290 | 291 | if project.arch.linux_name.startswith("mips"): 292 | last_inst_addr = block.capstone.insns[-2].address 293 | else: 294 | last_inst_addr = block.capstone.insns[-1].address 295 | for _ in range(num_insts): # considering that it may get into kernel mode 296 | if state.addr != last_inst_addr: 297 | state = step_one_inst(project, state, stop_at_syscall=stop_at_syscall) 298 | if stop_at_syscall and is_in_kernel(project, state): 299 | return None, state 300 | else: 301 | succ = project.factory.successors(state, num_inst=1) 302 | if not succ.flat_successors: 303 | return succ, None 304 | if stop_at_syscall and is_in_kernel(project, succ.flat_successors[0]): 305 | return None, succ.flat_successors[0] 306 | return succ, None 307 | raise RopException("Fail to reach the last instruction!") 308 | 309 | def step_one_inst(project, state, stop_at_syscall=False): 310 | if is_in_kernel(project, state): 311 | if stop_at_syscall: 312 | return state 313 | succ = project.factory.successors(state) 314 | return step_one_inst(project, succ.flat_successors[0]) 315 | 316 | if project.is_hooked(state.addr): 317 | succ = project.factory.successors(state) 318 | return step_one_inst(project, succ.flat_successors[0]) 319 | 320 | succ = project.factory.successors(state, num_inst=1) 321 | if not succ.flat_successors: 322 | raise RopException(f"fail to step state: {state}") 323 | return succ.flat_successors[0] 324 | 325 | def step_to_unconstrained_successor(project, state, max_steps=2, allow_simprocedures=False, 326 | stop_at_syscall=False, precise_action=False): 327 | """ 328 | steps up to two times to try to find an unconstrained successor 329 | :param state: the input state 330 | :param max_steps: maximum number of additional steps to try to get to an unconstrained state 331 | :return: a path at the unconstrained successor 332 | """ 333 | try: 334 | # might only want to enable this option for arches / oses which don't care about bad syscall 335 | # nums 336 | state.options.add(angr.options.BYPASS_UNSUPPORTED_SYSCALL) 337 | 338 | succ: SimSuccessors = None # type: ignore 339 | if not precise_action: 340 | succ = project.factory.successors(state) 341 | if stop_at_syscall and succ.flat_successors: 342 | next_state = succ.flat_successors[0] 343 | if is_in_kernel(project, next_state): 344 | return next_state 345 | else: 346 | # FIXME: we step instruction by instruction because of an angr bug: xxxx 347 | # the bug makes angr may merge sim_actions from two instructions into one 348 | # making analysis based on sim_actions inaccurate 349 | succ, state = step_one_block(project, state, stop_at_syscall=stop_at_syscall) 350 | if state: 351 | return state 352 | 353 | if len(succ.flat_successors) + len(succ.unconstrained_successors) != 1: 354 | raise RopException("Does not get to a single successor") 355 | if len(succ.flat_successors) == 1 and max_steps > 0: 356 | if not allow_simprocedures and project.is_hooked(succ.flat_successors[0].addr): 357 | # it cannot be a syscall as now syscalls are not explicitly hooked 358 | raise RopException("Skipping simprocedure") 359 | return step_to_unconstrained_successor(project, succ.flat_successors[0], max_steps=max_steps-1, 360 | allow_simprocedures=allow_simprocedures, 361 | stop_at_syscall=stop_at_syscall, 362 | precise_action=precise_action) 363 | if len(succ.flat_successors) == 1 and max_steps == 0: 364 | raise RopException("Does not get to an unconstrained successor") 365 | return succ.unconstrained_successors[0] 366 | 367 | except (angr.errors.AngrError, angr.errors.SimError) as e: 368 | raise RopException("Does not get to a single unconstrained successor") from e 369 | 370 | def at_syscall(state): 371 | return state.project.factory.block(state.addr, num_inst=1).vex.jumpkind.startswith("Ijk_Sys") 372 | 373 | def step_to_syscall(state): 374 | """ 375 | windup state to a state just about to make a syscall 376 | """ 377 | if at_syscall(state): 378 | return state 379 | 380 | simgr = state.project.factory.simgr(state) 381 | while True: 382 | simgr.step(num_inst=1) 383 | if not simgr.active: 384 | raise RuntimeError("unable to reach syscall instruction") 385 | state = simgr.active[0] 386 | if at_syscall(state): 387 | return state 388 | return None 389 | 390 | def timeout(seconds_before_timeout): 391 | def decorate(f): 392 | def handler(signum, frame):# pylint:disable=unused-argument 393 | raise RopTimeoutException("[angrop] Timeout!") 394 | def new_f(*args, **kwargs): 395 | old = signal.signal(signal.SIGALRM, handler) 396 | old_time_left = signal.alarm(seconds_before_timeout) 397 | if 0 < old_time_left < seconds_before_timeout: # never lengthen existing timer 398 | signal.alarm(old_time_left) 399 | start_time = time.time() 400 | try: 401 | result = f(*args, **kwargs) 402 | finally: 403 | if old_time_left > 0: # deduct f's run time from the saved timer 404 | old_time_left -= int(time.time() - start_time) 405 | signal.signal(signal.SIGALRM, old) 406 | signal.alarm(old_time_left) 407 | return result 408 | return new_f 409 | return decorate 410 | -------------------------------------------------------------------------------- /angrop/rop_value.py: -------------------------------------------------------------------------------- 1 | import claripy 2 | 3 | class RopValue: 4 | """ 5 | This class represents a value that needs to be concretized in a ROP chain 6 | Automatically handles rebase 7 | """ 8 | def __init__(self, value, project): 9 | if not isinstance(value, (int, str, claripy.ast.bv.BV)): 10 | raise ValueError("bad value type!") 11 | 12 | self.reg_name = None 13 | if type(value) is str: 14 | if value not in project.arch.default_symbolic_registers: 15 | raise ValueError(f"{value} is not a general purpose register!") 16 | self.reg_name = value 17 | value = claripy.BVS(value, project.arch.bits) 18 | 19 | self._value = value # when rebase is needed, value here holds the offset 20 | self._project = project 21 | self._rebase = None # rebase needs to be either specified or inferred 22 | self._code_base = None 23 | 24 | self._project_update() 25 | 26 | def _project_update(self): 27 | if type(self._value) is int: 28 | self._value = claripy.BVV(self._value, self._project.arch.bits) 29 | pie = self._project.loader.main_object.pic 30 | self._code_base = self._project.loader.main_object.mapped_base if pie else 0 31 | if not pie: 32 | self._rebase = False 33 | 34 | def __add__(self, other): 35 | cp = self.copy() 36 | if type(other) is int: 37 | cp._value += other 38 | elif isinstance(other, RopValue): 39 | cp._value += other._value 40 | cp._rebase |= other._rebase 41 | else: 42 | raise ValueError(f"Can't add {other} to RopValue!") 43 | return cp 44 | 45 | def determined(self, chain): 46 | res = chain._blank_state.solver.eval_upto(self._value, 2) 47 | return len(res) <= 1 48 | 49 | def rebase_ptr(self): 50 | pie = self._project.loader.main_object.pic 51 | if pie: 52 | self._value -= self._code_base 53 | self._rebase = True 54 | 55 | def rebase_analysis(self, chain=None): 56 | """ 57 | use our best effort to infer whether we should rebase this RopValue or not 58 | """ 59 | # if not pie, great, we are done 60 | pie = self._project.loader.main_object.pic 61 | if not pie: 62 | self._rebase = False 63 | return 64 | # if fully symbolic, we don't know whether it should be rebased or not 65 | if self.symbolic: 66 | if chain is None or not self.determined(chain): 67 | self._rebase = None 68 | return 69 | concreted = chain._blank_state.solver.eval(self._value) 70 | else: 71 | concreted = self.concreted 72 | 73 | # if concrete, check whether it is a pointer that needs rebase: 74 | # it is an address within a PIC object 75 | if concreted < self._project.loader.min_addr or concreted >= self._project.loader.max_addr: 76 | self._rebase = False 77 | return 78 | for obj in self._project.loader.all_elf_objects: 79 | if obj.pic and obj.min_addr <= concreted < obj.max_addr: 80 | self._value -= obj.min_addr 81 | self._rebase = True 82 | if obj != self._project.loader.main_object: 83 | raise NotImplementedError("Currently, angrop does not support rebase library address!") 84 | return 85 | self._rebase = False 86 | return 87 | 88 | @property 89 | def symbolic(self): 90 | return self._value.symbolic 91 | 92 | @property 93 | def ast(self): 94 | assert self._value.symbolic 95 | return self.data 96 | 97 | @property 98 | def is_register(self): 99 | return self.reg_name is not None 100 | 101 | @property 102 | def concreted(self): 103 | assert not self._value.symbolic 104 | if self.rebase: 105 | return (self._code_base + self._value).concrete_value 106 | return self._value.concrete_value 107 | 108 | @property 109 | def data(self): 110 | if self.rebase: 111 | return self._code_base + self._value 112 | return self._value 113 | 114 | @property 115 | def rebase(self): 116 | #if self._rebase is None: 117 | # raise RuntimeError("Somehow rebase is not specified in this RopValue") 118 | return self._rebase 119 | 120 | def __repr__(self): 121 | return f"RopValue({self.data}, {self._rebase})" 122 | 123 | def copy(self): 124 | cp = RopValue(self._value, self._project) 125 | cp._value = self._value 126 | cp._project = self._project 127 | cp._rebase = self._rebase 128 | cp._code_base = self._code_base 129 | return cp 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = angrop 3 | version = attr: angrop.__version__ 4 | url = https://github.com/angr/angrop 5 | classifiers = 6 | License :: OSI Approved :: BSD License 7 | Programming Language :: Python :: 3 8 | Programming Language :: Python :: 3.6 9 | license = BSD 2 Clause 10 | license_files = LICENSE 11 | description = The rop chain builder based off of angr 12 | long_description = file: README.md 13 | long_description_content_type = text/markdown 14 | 15 | [options] 16 | install_requires = 17 | angr 18 | tqdm 19 | python_requires = >= 3.6 20 | packages = find: 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup() 3 | -------------------------------------------------------------------------------- /tests/test_backend.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import angr 4 | import angrop # pylint: disable=unused-import 5 | 6 | from angrop.rop_gadget import SyscallGadget 7 | 8 | #BIN_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "binaries") 9 | #CACHE_DIR = os.path.join(BIN_DIR, 'tests_data', 'angrop_gadgets_cache') 10 | 11 | def test_blob(): 12 | """ 13 | make sure angrop works well with the blob backend 14 | """ 15 | bio = io.BytesIO(b"\x58\xC3\x0F\x05") # pop rax; ret; syscall 16 | proj = angr.Project(bio, main_opts={'backend': 'blob', 'arch': 'amd64'}, simos='linux') 17 | rop = proj.analyses.ROP(only_check_near_rets=False) 18 | 19 | gadget = rop.analyze_gadget(2) 20 | assert gadget 21 | assert isinstance(gadget, SyscallGadget) 22 | 23 | def run_all(): 24 | functions = globals() 25 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 26 | for f in sorted(all_functions.keys()): 27 | if hasattr(all_functions[f], '__call__'): 28 | all_functions[f]() 29 | 30 | if __name__ == "__main__": 31 | import sys 32 | import logging 33 | 34 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 35 | 36 | if len(sys.argv) > 1: 37 | globals()['test_' + sys.argv[1]]() 38 | else: 39 | run_all() 40 | -------------------------------------------------------------------------------- /tests/test_badbytes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | import angr 5 | import angrop # pylint: disable=unused-import 6 | 7 | l = logging.getLogger(__name__) 8 | 9 | bin_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'binaries')) 10 | tests_dir = os.path.join(bin_path, 'tests') 11 | data_dir = os.path.join(bin_path, 'tests_data', 'angrop_gadgets_cache') 12 | 13 | 14 | """ 15 | Suggestions on how to debug angr changes that break angrop. 16 | 17 | If the gadget is completely missing after your changes. Pick the address that didn't work and run the following. 18 | The logging should say why the gadget was discarded. 19 | 20 | rop = p.analyses.ROP() 21 | angrop.gadget_analyzer.l.setLevel("DEBUG") 22 | rop._gadget_analyzer.analyze_gadget(addr) 23 | 24 | If a gadget is missing memory reads / memory writes / memory changes, the actions are probably missing. 25 | Memory changes require a read action followed by a write action to the same address. 26 | """ 27 | 28 | def test_badbyte(): 29 | cache_path = os.path.join(data_dir, "bronze_ropchain") 30 | proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False) 31 | rop = proj.analyses.ROP() 32 | rop.set_badbytes([0x0, 0x0a]) 33 | 34 | if os.path.exists(cache_path): 35 | rop.load_gadgets(cache_path) 36 | else: 37 | rop.find_gadgets() 38 | rop.save_gadgets(cache_path) 39 | 40 | # make sure it can set 0 first 41 | chain = rop.set_regs(eax=0) 42 | state = chain.exec() 43 | assert not state.regs.eax.symbolic 44 | assert state.solver.eval(state.regs.eax == 0) 45 | assert all(x not in chain.payload_str() for x in [0, 0xa]) 46 | 47 | # make sure it can set 0x16, which requires gadgets like `mov eax, 0x16` 48 | chain = rop.set_regs(eax=0x16) 49 | state = chain.exec() 50 | assert not state.regs.eax.symbolic 51 | assert state.solver.eval(state.regs.eax == 0x16) 52 | assert all(x not in chain.payload_str() for x in [0, 0xa]) 53 | 54 | # make sure it can set 0xb 55 | # this binary does not have gadget that sets 0xb directly, it needs to do calculation 56 | # something like `mov eax, 8` and then `add eax, 3` 57 | chain = rop.set_regs(eax=0xb) 58 | state = chain.exec() 59 | assert not state.regs.eax.symbolic 60 | assert state.solver.eval(state.regs.eax == 0xb) 61 | assert all(x not in chain.payload_str() for x in [0, 0xa]) 62 | 63 | # make sure it can write '/bin/sh\x00\n' into memory, notice that '\x00' and '\n' are bad bytes 64 | ptr = rop.chain_builder._mem_writer._get_ptr_to_writable(9+4) 65 | chain = rop.write_to_mem(ptr, b'/bin/sh\x00\n') 66 | state = chain.exec() 67 | assert state.solver.eval(state.memory.load(ptr, 9), cast_to=bytes) 68 | assert all(x not in chain.payload_str() for x in [0, 0xa]) 69 | 70 | # finally, make sure setting multiple registers can work 71 | nullptr = rop.chain_builder._mem_writer._get_ptr_to_null() 72 | chain = rop.set_regs(eax=0xb, ebx=ptr, ecx=nullptr, edx=nullptr) 73 | state = chain.exec() 74 | assert not state.regs.eax.symbolic 75 | assert state.solver.eval(state.regs.eax == 0xb) 76 | assert not state.regs.ebx.symbolic 77 | assert state.solver.eval(state.regs.ebx == ptr) 78 | assert not state.regs.ecx.symbolic 79 | assert state.solver.eval(state.regs.ecx == nullptr) 80 | assert not state.regs.edx.symbolic 81 | assert state.solver.eval(state.regs.edx == nullptr) 82 | assert all(x not in chain.payload_str() for x in [0, 0xa]) 83 | 84 | 85 | def run_all(): 86 | functions = globals() 87 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 88 | for f in sorted(all_functions.keys()): 89 | if hasattr(all_functions[f], '__call__'): 90 | all_functions[f]() 91 | 92 | 93 | if __name__ == "__main__": 94 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 95 | 96 | import sys 97 | if len(sys.argv) > 1: 98 | globals()['test_' + sys.argv[1]]() 99 | else: 100 | run_all() 101 | -------------------------------------------------------------------------------- /tests/test_find_gadgets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | import angr 5 | import angrop # pylint: disable=unused-import 6 | 7 | l = logging.getLogger(__name__) 8 | 9 | bin_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'binaries')) 10 | tests_dir = os.path.join(bin_path, 'tests') 11 | data_dir = os.path.join(bin_path, 'tests_data', 'angrop_gadgets_cache') 12 | 13 | 14 | """ 15 | Suggestions on how to debug angr changes that break angrop. 16 | 17 | If the gadget is completely missing after your changes. Pick the address that didn't work and run the following. 18 | The logging should say why the gadget was discarded. 19 | 20 | rop = p.analyses.ROP() 21 | rop.analyze_gadget(addr) 22 | 23 | If a gadget is missing memory reads / memory writes / memory changes, the actions are probably missing. 24 | Memory changes require a read action followed by a write action to the same address. 25 | """ 26 | 27 | def gadget_exists(rop, addr): 28 | return rop.analyze_gadget(addr) is not None 29 | 30 | def test_badbyte(): 31 | proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False) 32 | rop = proj.analyses.ROP() 33 | 34 | assert all(gadget_exists(rop, x) for x in [0x080a9773, 0x08091cf5, 0x08092d80, 0x080920d3]) 35 | 36 | def local_multiprocess_find_gadgets(): 37 | proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False) 38 | rop = proj.analyses.ROP() 39 | 40 | rop.find_gadgets(show_progress=False) 41 | 42 | assert all(gadget_exists(rop, x) for x in [0x080a9773, 0x08091cf5, 0x08092d80, 0x080920d3]) 43 | 44 | def test_symbolic_memory_access_from_stack(): 45 | proj = angr.Project(os.path.join(tests_dir, "armel", "test_angrop_arm_gadget"), auto_load_libs=False) 46 | rop = proj.analyses.ROP() 47 | 48 | assert all(gadget_exists(rop, x) for x in [0x000103f4]) 49 | 50 | def test_arm_thumb_mode(): 51 | proj = angr.Project(os.path.join(bin_path, "tests", "armel", "libc-2.31.so"), auto_load_libs=False) 52 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False, is_thumb=True) 53 | 54 | gadget = rop.analyze_gadget(0x4bf858+1) 55 | 56 | assert gadget 57 | assert gadget.isn_count == 2 58 | 59 | def test_pivot_gadget(): 60 | # pylint: disable=pointless-string-statement 61 | proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False) 62 | rop = proj.analyses.ROP() 63 | 64 | assert all(gadget_exists(rop, x) for x in [0x80488e8, 0x8048998, 0x8048fd6, 0x8052cac, 0x805658c, ]) 65 | 66 | gadget = rop.analyze_gadget(0x8048592) 67 | assert not gadget 68 | 69 | proj = angr.Project(os.path.join(bin_path, "tests", "armel", "libc-2.31.so"), auto_load_libs=False) 70 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False, is_thumb=True) 71 | 72 | """ 73 | 4c7b5a mov sp, r7 74 | 4c7b5c pop.w {r4, r5, r6, r7, r8, sb, sl, fp, pc 75 | """ 76 | 77 | gadget = rop.analyze_gadget(0x4c7b5a+1) 78 | assert gadget is not None 79 | 80 | proj = angr.Project(os.path.join(tests_dir, "i386", "i386_glibc_2.35"), auto_load_libs=False) 81 | rop = proj.analyses.ROP() 82 | """ 83 | 439ad3 pop esp 84 | 439ad4 lea esp, [ebp-0xc] 85 | 439ad7 pop ebx 86 | 439ad8 pop esi 87 | 439ad9 pop edi 88 | 439ada pop ebp 89 | 439adb ret 90 | """ 91 | gadget = rop.analyze_gadget(0x439ad3) 92 | assert gadget is None 93 | 94 | proj = angr.Project(os.path.join(tests_dir, "x86_64", "libc.so.6"), auto_load_libs=False) 95 | rop = proj.analyses.ROP() 96 | 97 | """ 98 | 402bc8 leave 99 | 402bc9 clc 100 | 402bca repz ret 101 | """ 102 | gadget = rop.analyze_gadget(0x402bc8) 103 | assert gadget is None 104 | 105 | # this is not a valid gadget because sal shifts the memory 106 | # and we don't fully control the shifted memory 107 | """ 108 | 50843e sal byte ptr [rbp-0x11], cl 109 | 508441 leave 110 | 508442 ret 111 | """ 112 | gadget = rop.analyze_gadget(0x50843e) 113 | assert gadget is None 114 | 115 | def test_syscall_gadget(): 116 | proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False) 117 | rop = proj.analyses.ROP() 118 | assert all(gadget_exists(rop, x) for x in [0x0806f860, 0x0806f85e, 0x080939e3, 0x0806f2f1]) 119 | 120 | def test_shift_gadget(): 121 | # pylint: disable=pointless-string-statement 122 | """ 123 | 438a91 pop es 124 | 438a92 add esp, 0x9c 125 | 438a98 ret 126 | 127 | 454e75 push cs 128 | 454e76 add esp, 0x5c 129 | 454e79 pop ebx 130 | 454e7a pop esi 131 | 454e7b pop edi 132 | 454e7c pop ebp 133 | 454e7d ret 134 | 135 | 5622d5 push ss 136 | 5622d6 add esp, 0x74 137 | 5622d9 pop ebx 138 | 5622da pop edi 139 | 5622db ret 140 | 141 | 516fb2 clc 142 | 516fb3 pop ds 143 | 516fb4 add esp, 0x8 144 | 516fb7 pop ebx 145 | 516fb8 ret 146 | 147 | 490058 push ds 148 | 490059 add esp, 0x2c 149 | 49005c ret 150 | """ 151 | proj = angr.Project(os.path.join(tests_dir, "i386", "i386_glibc_2.35"), auto_load_libs=False) 152 | rop = proj.analyses.ROP() 153 | 154 | assert all(not gadget_exists(rop, x) for x in [0x438a91, 0x516fb2]) 155 | assert all(gadget_exists(rop, x) for x in [0x454e75, 0x5622d5, 0x490058]) 156 | 157 | def test_i386_syscall(): 158 | """ 159 | in 32-bit world, syscall instruction is only valid for AMD CPUs, we consider it invalid in angrop for 160 | better portability, see https://github.com/angr/angrop/issues/104 161 | """ 162 | # pylint: disable=pointless-string-statement 163 | proj = angr.Project(os.path.join(tests_dir, "i386", "angrop_syscall_test"), auto_load_libs=False) 164 | 165 | rop = proj.analyses.ROP() 166 | """ 167 | 804918c int 0x80 168 | """ 169 | 170 | assert all(gadget_exists(rop, x) for x in [0x804918c]) 171 | 172 | """ 173 | 8049189 syscall 174 | """ 175 | assert all(not gadget_exists(rop, x) for x in [0x8049189]) 176 | 177 | def test_gadget_timeout(): 178 | # pylint: disable=pointless-string-statement 179 | proj = angr.Project(os.path.join(tests_dir, "x86_64", "datadep_test"), auto_load_libs=False) 180 | rop = proj.analyses.ROP() 181 | """ 182 | 0x4005d5 ret 0xc148 183 | """ 184 | gadget = rop.analyze_gadget(0x4005d5) 185 | assert gadget 186 | 187 | def local_multiprocess_analyze_gadget_list(): 188 | # pylint: disable=pointless-string-statement 189 | proj = angr.Project(os.path.join(tests_dir, "x86_64", "datadep_test"), auto_load_libs=False) 190 | rop = proj.analyses.ROP() 191 | """ 192 | 0x4006d8, 0x400864 good gadgets 193 | 0x4005d8 bad instruction 194 | """ 195 | gadgets = rop.analyze_gadget_list([0x4006d8, 0x4005d8, 0x400864]) 196 | assert len(gadgets) == 2 197 | assert gadgets[0].addr == 0x4006d8 198 | assert gadgets[1].addr == 0x400864 199 | 200 | def test_gadget_filtering(): 201 | proj = angr.Project(os.path.join(tests_dir, "armel", "libc-2.31.so"), auto_load_libs=False) 202 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False, is_thumb=True) 203 | rop.analyze_gadget(0x42bca5) 204 | rop.analyze_gadget(0x42c3c1) 205 | rop.chain_builder.bootstrap() 206 | assert len(rop.chain_builder._reg_setter._reg_setting_gadgets) == 1 207 | 208 | def test_aarch64_svc(): 209 | proj = angr.Project(os.path.join(tests_dir, "aarch64", "libc.so.6"), auto_load_libs=False) 210 | rop = proj.analyses.ROP(fast_mode=True, only_check_near_rets=False) 211 | g = rop.analyze_gadget(0x0000000000463820) 212 | assert g is not None 213 | 214 | def test_aarch64_reg_setter(): 215 | proj = angr.Project(os.path.join(tests_dir, "aarch64", "libc.so.6"), auto_load_libs=False) 216 | rop = proj.analyses.ROP(fast_mode=True, only_check_near_rets=False) 217 | g = rop.analyze_gadget(0x00000000004c29a0) 218 | assert g is not None 219 | 220 | def test_enter(): 221 | proj = angr.Project(os.path.join(tests_dir, "x86_64", "libc.so.6"), auto_load_libs=False) 222 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False) 223 | g = rop.analyze_gadget(0x00000000004f83f3) 224 | assert g is not None 225 | 226 | def test_jmp_mem_gadget(): 227 | proj = angr.Project(os.path.join(tests_dir, "x86_64", "libc.so.6"), auto_load_libs=False) 228 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False) 229 | # 0x0000000000031f6d : jmp qword ptr [rax] 230 | # 0x000000000004bec2 : call qword ptr [r11 + rax*8] 231 | g = rop.analyze_gadget(0x0000000000431f6d) 232 | assert g is not None 233 | assert g.transit_type == 'jmp_mem' 234 | g = rop.analyze_gadget(0x000000000044bec2) 235 | assert g is not None 236 | assert g.transit_type == 'jmp_mem' 237 | 238 | def test_syscall_next_block(): 239 | proj = angr.Project(os.path.join(tests_dir, "cgc", "sc1_0b32aa01_01"), auto_load_libs=False) 240 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False) 241 | g = rop.analyze_gadget(0x804843c) 242 | assert g 243 | assert g.isn_count < 20 244 | 245 | g = rop.analyze_gadget(0x8048441) 246 | assert g.can_return is True 247 | 248 | g = rop.analyze_gadget(0x080484d4) 249 | assert g.can_return is True 250 | 251 | rop.find_gadgets_single_threaded(show_progress=False) 252 | chain = rop.do_syscall(2, [1, 0x41414141, 0x42424242, 0], preserve_regs={'eax'}, needs_return=True) 253 | assert chain 254 | 255 | def run_all(): 256 | functions = globals() 257 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 258 | for f in sorted(all_functions.keys()): 259 | if hasattr(all_functions[f], '__call__'): 260 | all_functions[f]() 261 | local_multiprocess_find_gadgets() 262 | local_multiprocess_analyze_gadget_list() 263 | 264 | if __name__ == "__main__": 265 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 266 | 267 | import sys 268 | if len(sys.argv) > 1: 269 | globals()['test_' + sys.argv[1]]() 270 | else: 271 | run_all() 272 | -------------------------------------------------------------------------------- /tests/test_gadgets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import angr 4 | import angrop # pylint: disable=unused-import 5 | from angrop.rop_gadget import RopGadget, PivotGadget, SyscallGadget 6 | 7 | BIN_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "binaries") 8 | CACHE_DIR = os.path.join(BIN_DIR, 'tests_data', 'angrop_gadgets_cache') 9 | 10 | def get_rop(path): 11 | cache_path = os.path.join(CACHE_DIR, os.path.basename(path)) 12 | proj = angr.Project(path, auto_load_libs=False) 13 | rop = proj.analyses.ROP() 14 | if os.path.exists(cache_path): 15 | rop.load_gadgets(cache_path) 16 | else: 17 | rop.find_gadgets() 18 | rop.save_gadgets(cache_path) 19 | return rop 20 | 21 | def test_arm_conditional(): 22 | """ 23 | Currently, we don't model conditional execution in arm. So we don't allow 24 | conditional execution in arm at this moment. 25 | """ 26 | rop = get_rop(os.path.join(BIN_DIR, "tests", "armel", "helloworld")) 27 | 28 | cond_gadget_addrs = [0x10368, 0x1036c, 0x10370, 0x10380, 0x10384, 0x1038c, 0x1039c, 29 | 0x103a0, 0x103b8, 0x103bc, 0x103c4, 0x104e8, 0x104ec] 30 | 31 | assert all(x.addr not in cond_gadget_addrs for x in rop._all_gadgets) 32 | 33 | def test_jump_gadget(): 34 | """ 35 | Ensure it finds gadgets ending with jumps 36 | Ensure angrop can use jump gadgets to build ROP chains 37 | """ 38 | rop = get_rop(os.path.join(BIN_DIR, "tests", "mipsel", "fauxware")) 39 | 40 | jump_gadgets = [x for x in rop._all_gadgets if x.transit_type == "jmp_reg"] 41 | assert len(jump_gadgets) > 0 42 | 43 | jump_regs = [x.pc_reg for x in jump_gadgets] 44 | assert 't9' in jump_regs 45 | assert 'ra' in jump_regs 46 | 47 | def test_arm_mem_change_gadget(): 48 | # pylint: disable=pointless-string-statement 49 | 50 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "armel", "libc-2.31.so"), auto_load_libs=False) 51 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False, is_thumb=True) 52 | 53 | """ 54 | 0x0004f08c <+28>: ldr r2, [r4, #48] ; 0x30 55 | 0x0004f08e <+30>: asrs r3, r3, #2 56 | 0x0004f090 <+32>: str r3, [r5, #8] 57 | 0x0004f092 <+34>: str r2, [r5, #0] 58 | 0x0004f094 <+36>: str r5, [r4, #48] ; 0x30 59 | 0x0004f096 <+38>: pop {r3, r4, r5, pc} 60 | """ 61 | gadget = rop.analyze_gadget(0x44f08c+1) # thumb mode 62 | assert gadget 63 | assert not gadget.mem_changes 64 | 65 | gadget = rop.analyze_gadget(0x459eea+1) # thumb mode 66 | assert gadget 67 | assert not gadget.mem_changes 68 | 69 | """ 70 | 4b1e30 ldr r1, [r6] 71 | 4b1e32 add r4, r1 72 | 4b1e34 str r4, [r6] 73 | 4b1e36 pop {r3, r4, r5, r6, r7, pc} 74 | """ 75 | gadget = rop.analyze_gadget(0x4b1e30+1) # thumb mode 76 | assert gadget.mem_changes 77 | 78 | """ 79 | 4c1e78 ldr r1, [r4,#0x14] 80 | 4c1e7a add r1, r5 81 | 4c1e7c str r1, [r4,#0x14] 82 | 4c1e7e pop {r3, r4, r5, pc} 83 | """ 84 | gadget = rop.analyze_gadget(0x4c1e78+1) # thumb mode 85 | assert gadget.mem_changes 86 | 87 | """ 88 | 4c1ea4 ldr r2, [r3,#0x14] 89 | 4c1ea6 adds r2, #0x4 90 | 4c1ea8 str r2, [r3,#0x14] 91 | 4c1eaa bx lr 92 | """ 93 | gadget = rop.analyze_gadget(0x4c1ea4+1) # thumb mode 94 | assert not gadget.mem_changes 95 | 96 | """ 97 | 4c1e8e ldr r1, [r4,#0x14] 98 | 4c1e90 str r5, [r4,#0x10] 99 | 4c1e92 add r1, r5 100 | 4c1e94 str r1, [r4,#0x14] 101 | 4c1e96 pop {r3, r4, r5, pc} 102 | """ 103 | gadget = rop.analyze_gadget(0x4c1e8e+1) # thumb mode 104 | assert gadget.mem_changes 105 | 106 | def test_pivot_gadget(): 107 | # pylint: disable=pointless-string-statement 108 | 109 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "i386", "i386_glibc_2.35"), auto_load_libs=False) 110 | rop = proj.analyses.ROP() 111 | 112 | """ 113 | 5719da pop esp 114 | 5719db ret 115 | """ 116 | gadget = rop.analyze_gadget(0x5719da) 117 | assert gadget.stack_change == 0x4 118 | assert gadget.stack_change_after_pivot == 0x4 119 | assert len(gadget.sp_controllers) == 1 120 | assert len(gadget.sp_reg_controllers) == 0 121 | 122 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "i386", "bronze_ropchain"), auto_load_libs=False) 123 | rop = proj.analyses.ROP() 124 | 125 | """ 126 | 80488e8 leave 127 | 80488e9 ret 128 | """ 129 | gadget = rop.analyze_gadget(0x80488e8) 130 | assert type(gadget) == PivotGadget 131 | assert gadget.stack_change == 0 132 | assert gadget.stack_change_after_pivot == 0x8 133 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop() == 'ebp' 134 | 135 | 136 | """ 137 | 8048592 xchg esp, eax 138 | 8048593 ret 0xca21 139 | """ 140 | gadget = rop.analyze_gadget(0x8048592) 141 | assert not gadget 142 | 143 | """ 144 | 8048998 pop ecx 145 | 8048999 pop ebx 146 | 804899a pop ebp 147 | 804899b lea esp, [ecx-0x4] 148 | 804899e ret 149 | """ 150 | gadget = rop.analyze_gadget(0x8048998) 151 | assert type(gadget) == PivotGadget 152 | assert gadget.stack_change == 0xc 153 | assert gadget.stack_change_after_pivot == 0x4 154 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop().startswith('symbolic_stack_') 155 | 156 | """ 157 | 8048fd6 xchg esp, eax 158 | 8048fd7 ret 159 | """ 160 | gadget = rop.analyze_gadget(0x8048fd6) 161 | assert type(gadget) == PivotGadget 162 | assert gadget.stack_change == 0 163 | assert gadget.stack_change_after_pivot == 0x4 164 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop() == 'eax' 165 | 166 | """ 167 | 8052cac lea esp, [ebp-0xc] 168 | 8052caf pop ebx 169 | 8052cb0 pop esi 170 | 8052cb1 pop edi 171 | 8052cb2 pop ebp 172 | 8052cb3 ret 173 | """ 174 | gadget = rop.analyze_gadget(0x8052cac) 175 | assert type(gadget) == PivotGadget 176 | assert gadget.stack_change == 0 177 | assert gadget.stack_change_after_pivot == 0x14 178 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop() == 'ebp' 179 | 180 | """ 181 | 805658c add BYTE PTR [eax],al 182 | 805658e pop ebx 183 | 805658f pop esi 184 | 8056590 pop edi 185 | 8056591 ret 186 | """ 187 | gadget = rop.analyze_gadget(0x805658c) 188 | assert type(gadget) == RopGadget 189 | assert gadget.stack_change == 0x10 # 3 pops + 1 ret 190 | 191 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "armel", "libc-2.31.so"), auto_load_libs=False) 192 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False, is_thumb=True) 193 | 194 | """ 195 | 4c7b5a mov sp, r7 196 | 4c7b5c pop.w {r4, r5, r6, r7, r8, sb, sl, fp, pc} 197 | """ 198 | 199 | #rop.find_gadgets(show_progress=False) 200 | gadget = rop.analyze_gadget(0x4c7b5a+1) 201 | assert type(gadget) == PivotGadget 202 | assert gadget.stack_change == 0 203 | assert gadget.stack_change_after_pivot == 0x24 204 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop() == 'r7' 205 | 206 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "armel", "manysum"), load_options={"auto_load_libs": False}) 207 | rop = proj.analyses.ROP() 208 | 209 | """ 210 | 1040c mov r0, r3 211 | 10410 sub sp, fp, #0x0 212 | 10414 pop {fp} 213 | 10418 bx lr 214 | """ 215 | gadget = rop.analyze_gadget(0x1040c) 216 | assert type(gadget) == PivotGadget 217 | assert gadget.stack_change == 0 218 | assert gadget.stack_change_after_pivot == 0x4 219 | assert len(gadget.sp_controllers) == 1 and gadget.sp_controllers.pop() == 'r11' 220 | 221 | def test_syscall_gadget(): 222 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "i386", "i386_glibc_2.35"), auto_load_libs=False) 223 | rop = proj.analyses.ROP() 224 | 225 | gadget = rop.analyze_gadget(0x437765) 226 | assert type(gadget) == SyscallGadget 227 | assert gadget.stack_change == 0 228 | assert not gadget.can_return 229 | 230 | gadget = rop.analyze_gadget(0x5212f6) 231 | assert type(gadget) == SyscallGadget 232 | assert gadget.stack_change == 0 233 | assert not gadget.can_return 234 | 235 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "i386", "bronze_ropchain"), auto_load_libs=False) 236 | rop = proj.analyses.ROP() 237 | 238 | gadget = rop.analyze_gadget(0x0806f860) 239 | assert type(gadget) == SyscallGadget 240 | assert gadget.stack_change == 0x4 241 | assert gadget.can_return 242 | 243 | gadget = rop.analyze_gadget(0x0806f85e) 244 | assert type(gadget) == SyscallGadget 245 | assert gadget.stack_change == 0x4 246 | assert gadget.can_return 247 | 248 | gadget = rop.analyze_gadget(0x080939e3) 249 | assert type(gadget) == SyscallGadget 250 | assert gadget.stack_change == 0x0 251 | assert not gadget.can_return 252 | 253 | gadget = rop.analyze_gadget(0x0806f2f1) 254 | assert type(gadget) == SyscallGadget 255 | assert gadget.stack_change == 0x0 256 | assert not gadget.can_return 257 | 258 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "roptest"), auto_load_libs=False) 259 | rop = proj.analyses.ROP() 260 | gadget = rop.analyze_gadget(0x4000c1) 261 | assert type(gadget) == SyscallGadget 262 | assert gadget.stack_change == 0 263 | assert not gadget.can_return 264 | 265 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "libc.so.6"), auto_load_libs=False) 266 | rop = proj.analyses.ROP(fast_mode=False) 267 | 268 | gadget = rop.analyze_gadget(0x4c1330) 269 | assert type(gadget) == SyscallGadget 270 | assert gadget.stack_change == 0 271 | assert not gadget.can_return 272 | assert len(gadget.concrete_regs) == 1 and gadget.concrete_regs.pop('rax') == 0x3b 273 | 274 | gadget = rop.analyze_gadget(0x521cef) 275 | assert type(gadget) == RopGadget 276 | assert len(gadget.mem_writes) == 1 277 | mem_write = gadget.mem_writes[0] 278 | assert mem_write.addr_offset == 0x68 279 | assert len(mem_write.addr_controllers) == 1 and 'rdx' in mem_write.addr_controllers 280 | assert len(mem_write.data_controllers) == 1 and 'rcx' in mem_write.data_controllers 281 | 282 | gadget = rop.analyze_gadget(0x4c1437) 283 | assert type(gadget) == SyscallGadget 284 | assert gadget.stack_change == 0 285 | assert not gadget.can_return 286 | assert len(gadget.concrete_regs) == 1 and gadget.concrete_regs.pop('rax') == 0x3b 287 | 288 | gadget = rop.analyze_gadget(0x536715) 289 | assert type(gadget) == SyscallGadget 290 | assert gadget.stack_change == 0 291 | assert not gadget.can_return 292 | assert len(gadget.concrete_regs) == 1 and gadget.concrete_regs.pop('rsi') == 0x81 293 | 294 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "cgc", "sc1_0b32aa01_01"), auto_load_libs=False) 295 | rop = proj.analyses.ROP() 296 | g = rop.analyze_gadget(0x0804843c) 297 | assert g.prologue and isinstance(g, RopGadget) 298 | 299 | def test_pop_pc_gadget(): 300 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "mipsel", "darpa_ping"), auto_load_libs=False) 301 | rop = proj.analyses.ROP() 302 | gadget = rop.analyze_gadget(0x404e98) 303 | assert gadget.transit_type == 'pop_pc' 304 | assert gadget.pc_offset == 0x28 305 | 306 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "angrop_retn_test"), auto_load_libs=False) 307 | rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False) 308 | gadget = rop.analyze_gadget(0x40113a) 309 | assert gadget.transit_type == 'pop_pc' 310 | assert gadget.pc_offset == 0 311 | assert gadget.stack_change == 0x18 312 | 313 | def run_all(): 314 | functions = globals() 315 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 316 | for f in sorted(all_functions.keys()): 317 | if hasattr(all_functions[f], '__call__'): 318 | all_functions[f]() 319 | 320 | if __name__ == "__main__": 321 | import sys 322 | import logging 323 | 324 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 325 | #logging.getLogger("angrop.gadget_analyzer").setLevel(logging.DEBUG) 326 | 327 | if len(sys.argv) > 1: 328 | globals()['test_' + sys.argv[1]]() 329 | else: 330 | run_all() 331 | -------------------------------------------------------------------------------- /tests/test_rop.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import logging 4 | 5 | import claripy 6 | import angr 7 | import angrop # pylint: disable=unused-import 8 | 9 | l = logging.getLogger("angrop.tests.test_rop") 10 | 11 | public_bin_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../binaries/tests') 12 | test_data_location = os.path.join(public_bin_location, "..", "tests_data", "angrop_gadgets_cache") 13 | 14 | 15 | """ 16 | Suggestions on how to debug angr changes that break angrop. 17 | 18 | If the gadget is completely missing after your changes. Pick the address that didn't work and run the following. 19 | The logging should say why the gadget was discarded. 20 | 21 | rop = p.analyses.ROP() 22 | angrop.gadget_analyzer.l.setLevel("DEBUG") 23 | rop._gadget_analyzer.analyze_gadget(addr) 24 | 25 | If a gadget is missing memory reads / memory writes / memory changes, the actions are probably missing. 26 | Memory changes require a read action followed by a write action to the same address. 27 | """ 28 | 29 | 30 | def assert_mem_access_equal(m1, m2): 31 | assert set(m1.addr_dependencies) ==set(m2.addr_dependencies) 32 | assert set(m1.addr_controllers) == set(m2.addr_controllers) 33 | assert set(m1.data_dependencies) == set(m2.data_dependencies) 34 | assert set(m1.data_controllers) == set(m2.data_controllers) 35 | assert m1.addr_constant == m2.addr_constant 36 | assert m1.data_constant == m2.data_constant 37 | assert m1.addr_size == m2.addr_size 38 | assert m1.data_size == m2.data_size 39 | 40 | 41 | def assert_gadgets_equal(known_gadget, test_gadget): 42 | assert known_gadget.addr == test_gadget.addr 43 | assert known_gadget.changed_regs == test_gadget.changed_regs 44 | assert known_gadget.popped_regs == test_gadget.popped_regs 45 | assert known_gadget.reg_dependencies == test_gadget.reg_dependencies 46 | assert known_gadget.reg_controllers == test_gadget.reg_controllers 47 | assert known_gadget.stack_change == test_gadget.stack_change 48 | 49 | assert len(known_gadget.mem_reads) == len(test_gadget.mem_reads) 50 | for m1, m2 in zip(known_gadget.mem_reads, test_gadget.mem_reads): 51 | assert_mem_access_equal(m1, m2) 52 | assert len(known_gadget.mem_writes) == len(test_gadget.mem_writes) 53 | for m1, m2 in zip(known_gadget.mem_writes, test_gadget.mem_writes): 54 | assert_mem_access_equal(m1, m2) 55 | assert len(known_gadget.mem_changes) == len(test_gadget.mem_changes) 56 | for m1, m2 in zip(known_gadget.mem_changes, test_gadget.mem_changes): 57 | assert_mem_access_equal(m1, m2) 58 | 59 | assert known_gadget.addr == test_gadget.addr 60 | assert known_gadget.changed_regs == test_gadget.changed_regs 61 | 62 | 63 | def compare_gadgets(test_gadgets, known_gadgets): 64 | test_gadgets = sorted(test_gadgets, key=lambda x: x.addr) 65 | known_gadgets = sorted(known_gadgets, key=lambda x: x.addr) 66 | 67 | # we allow new gadgets to be found, but only check the correctness of those that were there in the known_gadgets 68 | # so filter new gadgets found 69 | expected_addrs = set(g.addr for g in known_gadgets) 70 | test_gadgets = [g for g in test_gadgets if g.addr in expected_addrs] 71 | 72 | # check that each of the expected gadget addrs was found as a gadget 73 | # if it wasn't the best way to debug is to run: 74 | # angrop.gadget_analyzer.l.setLevel("DEBUG"); rop._gadget_analyzer.analyze_gadget(addr) 75 | test_gadget_dict = {} 76 | for g in test_gadgets: 77 | test_gadget_dict.setdefault(g.addr, []).append(g) 78 | 79 | found_addrs = set(g.addr for g in test_gadgets) 80 | for g in known_gadgets: 81 | assert g.addr in found_addrs 82 | 83 | # So now we should have 84 | assert len(test_gadgets) == len(known_gadgets) 85 | 86 | # check gadgets 87 | for g in known_gadgets: 88 | matching_gadgets = [ 89 | test_gadget 90 | for test_gadget in test_gadget_dict[g.addr] 91 | if test_gadget.bbl_addrs == g.bbl_addrs 92 | ] 93 | assert len(matching_gadgets) == 1 94 | assert_gadgets_equal(g, matching_gadgets[0]) 95 | 96 | 97 | def execute_chain(project, chain): 98 | s = project.factory.blank_state() 99 | s.memory.store(s.regs.sp, chain.payload_str()) 100 | goal_idx = chain.next_pc_idx() 101 | s.memory.store( 102 | s.regs.sp 103 | + (chain.payload_len if goal_idx is None else goal_idx * project.arch.bytes), 104 | b"A" * project.arch.bytes, 105 | ) 106 | s.ip = s.stack_pop() 107 | p = project.factory.simulation_manager(s) 108 | goal_addr = 0x4141414141414141 % (1 << project.arch.bits) 109 | while p.one_active.addr != goal_addr: 110 | p.step() 111 | assert len(p.active) == 1 112 | 113 | return p.one_active 114 | 115 | def verify_execve_chain(chain): 116 | state = chain._blank_state.copy() 117 | proj = state.project 118 | state.memory.store(state.regs.sp, chain.payload_str()) 119 | state.ip = state.stack_pop() 120 | 121 | # step to the system call 122 | simgr = proj.factory.simgr(state) 123 | while simgr.active: 124 | assert len(simgr.active) == 1 125 | state = simgr.active[0] 126 | obj = proj.loader.find_object_containing(state.ip.concrete_value) 127 | if obj and obj.binary == 'cle##kernel': 128 | break 129 | simgr.step() 130 | 131 | # verify the syscall arguments 132 | state = simgr.active[0] 133 | cc = angr.SYSCALL_CC[proj.arch.name]["default"](proj.arch) 134 | assert cc.syscall_num(state).concrete_value == chain._builder.arch.execve_num 135 | ptr = state.registers.load(cc.ARG_REGS[0]) 136 | assert state.solver.is_true(state.memory.load(ptr, 8) == b'/bin/sh\0') 137 | assert state.registers.load(cc.ARG_REGS[1]).concrete_value == 0 138 | assert state.registers.load(cc.ARG_REGS[2]).concrete_value == 0 139 | 140 | def test_roptest_mips(): 141 | proj = angr.Project(os.path.join(public_bin_location, "mipsel/darpa_ping"), auto_load_libs=False) 142 | rop = proj.analyses.ROP(fast_mode=True) 143 | rop.find_gadgets_single_threaded(show_progress=False) 144 | 145 | chain = rop.set_regs(s0=0x41414142, s1=0x42424243, v0=0x43434344) 146 | result_state = execute_chain(proj, chain) 147 | assert result_state.solver.eval(result_state.regs.s0) == 0x41414142 148 | assert result_state.solver.eval(result_state.regs.s1) == 0x42424243 149 | assert result_state.solver.eval(result_state.regs.v0) == 0x43434344 150 | 151 | 152 | def test_rop_x86_64(): 153 | b = angr.Project(os.path.join(public_bin_location, "x86_64/datadep_test"), auto_load_libs=False) 154 | rop = b.analyses.ROP() 155 | rop.find_gadgets_single_threaded(show_progress=False) 156 | 157 | cache_path = os.path.join(test_data_location, "datadep_test_gadgets") 158 | if not os.path.exists(cache_path): 159 | rop.save_gadgets(cache_path) 160 | 161 | # check gadgets 162 | with open(cache_path, "rb") as f: 163 | tup = pickle.load(f) 164 | compare_gadgets(rop._all_gadgets, tup[0]) 165 | 166 | # test creating a rop chain 167 | chain = rop.set_regs(rbp=0x1212, rbx=0x1234567890123456) 168 | # smallest possible chain 169 | assert chain.payload_len == 32 170 | # chain is correct 171 | result_state = execute_chain(b, chain) 172 | assert result_state.solver.eval(result_state.regs.rbp) == 0x1212 173 | assert result_state.solver.eval(result_state.regs.rbx) == 0x1234567890123456 174 | 175 | # test setting the filler value 176 | rop.set_roparg_filler(0x4141414141414141) 177 | chain = rop.set_regs(rbx=0x121212) 178 | assert chain._concretize_chain_values()[2][0] == 0x4141414141414141 179 | 180 | 181 | def test_rop_i386_cgc(): 182 | b = angr.Project(os.path.join(public_bin_location, "cgc/sc1_0b32aa01_01"), auto_load_libs=False) 183 | rop = b.analyses.ROP() 184 | rop.find_gadgets_single_threaded(show_progress=False) 185 | 186 | cache_path = os.path.join(test_data_location, "0b32aa01_01_gadgets") 187 | if not os.path.exists(cache_path): 188 | rop.save_gadgets(cache_path) 189 | 190 | # check gadgets 191 | with open(os.path.join(test_data_location, "0b32aa01_01_gadgets"), "rb") as f: 192 | tup = pickle.load(f) 193 | compare_gadgets(rop._all_gadgets, tup[0]) 194 | 195 | # test creating a rop chain 196 | chain = rop.set_regs(ebx=0x98765432, ecx=0x12345678) 197 | # smallest possible chain 198 | assert chain.payload_len == 16 199 | # chain is correct 200 | result_state = execute_chain(b, chain) 201 | assert result_state.solver.eval(result_state.regs.ebx) == 0x98765432 202 | assert result_state.solver.eval(result_state.regs.ecx) == 0x12345678 203 | 204 | # test memwrite chain 205 | chain = rop.write_to_mem(0x41414141, b"ABCDEFGH") 206 | result_state = execute_chain(b, chain) 207 | assert result_state.solver.eval(result_state.memory.load(0x41414141, 8), cast_to=bytes) == b"ABCDEFGH" 208 | 209 | def test_rop_arm(): 210 | b = angr.Project(os.path.join(public_bin_location, "armel/manysum"), load_options={"auto_load_libs": False}) 211 | rop = b.analyses.ROP() 212 | rop.find_gadgets_single_threaded(show_progress=False) 213 | 214 | cache_path = os.path.join(test_data_location, "arm_manysum_test_gadgets") 215 | if not os.path.exists(cache_path): 216 | rop.save_gadgets(cache_path) 217 | 218 | # check gadgets 219 | with open(os.path.join(test_data_location, "arm_manysum_test_gadgets"), "rb") as f: 220 | tup = pickle.load(f) 221 | compare_gadgets(rop._all_gadgets, tup[0]) 222 | 223 | # test creating a rop chain 224 | chain = rop.set_regs(r11=0x99887766) 225 | # smallest possible chain 226 | assert chain.payload_len == 12 227 | # correct chains, using a more complicated chain here 228 | chain = rop.set_regs(r4=0x99887766, r9=0x44556677, r11=0x11223344) 229 | result_state = execute_chain(b, chain) 230 | assert result_state.solver.eval(result_state.regs.r4) == 0x99887766 231 | assert result_state.solver.eval(result_state.regs.r9) == 0x44556677 232 | assert result_state.solver.eval(result_state.regs.r11) == 0x11223344 233 | 234 | # test memwrite chain 235 | chain = rop.write_to_mem(0x41414141, b"ABCDEFGH") 236 | result_state = execute_chain(b, chain) 237 | assert result_state.solver.eval(result_state.memory.load(0x41414141, 8), cast_to=bytes) == b"ABCDEFGH" 238 | 239 | def test_roptest_x86_64(): 240 | p = angr.Project(os.path.join(public_bin_location, "x86_64/roptest"), auto_load_libs=False) 241 | r = p.analyses.ROP(only_check_near_rets=False) 242 | r.find_gadgets_single_threaded(show_progress=False) 243 | c = r.execve(path=b"/bin/sh") 244 | verify_execve_chain(c) 245 | 246 | def test_roptest_aarch64(): 247 | cache_path = os.path.join(test_data_location, "aarch64_glibc_2.19") 248 | proj = angr.Project(os.path.join(public_bin_location, "aarch64", "libc.so.6"), auto_load_libs=False) 249 | rop = proj.analyses.ROP(fast_mode=True, only_check_near_rets=False) 250 | 251 | rop.analyze_gadget(0x4b7ca8) 252 | rop.analyze_gadget(0x4ebad4) 253 | 254 | data = claripy.BVS("data", 64) 255 | chain = rop.set_regs(x0=data) 256 | assert chain is not None 257 | chain._blank_state.solver.add(data == 0x41414141) 258 | assert b'\xe1\x3eAA' in chain.payload_str() 259 | 260 | if os.path.exists(cache_path): 261 | rop.load_gadgets(cache_path) 262 | else: 263 | rop.find_gadgets() 264 | rop.save_gadgets(cache_path) 265 | 266 | chain = rop.write_to_mem(0x41414140, b'AAAAAAA') 267 | assert chain is not None 268 | 269 | chain = rop.execve(path=b'/bin/sh') 270 | verify_execve_chain(chain) 271 | 272 | def run_all(): 273 | functions = globals() 274 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 275 | for f in sorted(all_functions.keys()): 276 | if hasattr(all_functions[f], '__call__'): 277 | print(f) 278 | all_functions[f]() 279 | 280 | 281 | if __name__ == "__main__": 282 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 283 | 284 | import sys 285 | if len(sys.argv) > 1: 286 | globals()['test_' + sys.argv[1]]() 287 | else: 288 | run_all() 289 | -------------------------------------------------------------------------------- /tests/test_ropblock.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import angr 4 | import angrop # pylint: disable=unused-import 5 | from angrop.rop_block import RopBlock 6 | from angrop.errors import RopException 7 | 8 | BIN_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "binaries") 9 | CACHE_DIR = os.path.join(BIN_DIR, 'tests_data', 'angrop_gadgets_cache') 10 | 11 | def test_ropblock(): 12 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "mipsel", "darpa_ping"), auto_load_libs=False) 13 | rop = proj.analyses.ROP() 14 | gadget = rop.analyze_gadget(0x404e98) 15 | rb = RopBlock.from_gadget(gadget, rop.chain_builder._reg_setter) 16 | assert rb.next_pc_idx() == 11 17 | 18 | def test_reg_mover(): 19 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "libc.so.6"), auto_load_libs=False) 20 | rop = proj.analyses.ROP(fast_mode=True, only_check_near_rets=False) 21 | 22 | g1 = rop.analyze_gadget(0x46f562) # mov rax, rbp; pop rbp; pop r12; ret 23 | g2 = rop.analyze_gadget(0x524a50) # push rax; mov eax, 1; pop rbx; pop rbp; pop r12; ret 24 | assert g1 is not None and g2 is not None 25 | 26 | rb = RopBlock.from_gadget_list([g1, g2], rop.chain_builder._reg_mover) 27 | assert len(rb.reg_moves) == 1 28 | move = rb.reg_moves[0] 29 | assert move.from_reg == 'rbp' 30 | assert move.to_reg == 'rbx' 31 | assert move.bits == 64 32 | 33 | chain = rop.move_regs(rbx='rbp') 34 | chain._blank_state.regs.rbp = 0x41414141 35 | 36 | state = chain.exec() 37 | assert state.regs.rbx.concrete_value == 0x41414141 38 | 39 | # this should fail 40 | try: 41 | chain = rop.move_regs(rbx='rbp', rax='rbp') 42 | assert chain is None 43 | except RopException: 44 | pass 45 | 46 | def run_all(): 47 | functions = globals() 48 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 49 | for f in sorted(all_functions.keys()): 50 | if hasattr(all_functions[f], '__call__'): 51 | all_functions[f]() 52 | 53 | if __name__ == "__main__": 54 | import sys 55 | import logging 56 | 57 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 58 | #logging.getLogger("angrop.gadget_analyzer").setLevel(logging.DEBUG) 59 | 60 | if len(sys.argv) > 1: 61 | globals()['test_' + sys.argv[1]]() 62 | else: 63 | run_all() 64 | -------------------------------------------------------------------------------- /tests/test_ropchain.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import angr 4 | import angrop # pylint: disable=unused-import 5 | 6 | BIN_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "binaries") 7 | CACHE_DIR = os.path.join(BIN_DIR, 'tests_data', 'angrop_gadgets_cache') 8 | 9 | def test_chain_exec(): 10 | """ 11 | Ensure the chain executor is correct 12 | """ 13 | cache_path = os.path.join(CACHE_DIR, "1after909") 14 | proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "1after909"), auto_load_libs=False) 15 | rop = proj.analyses.ROP() 16 | 17 | if os.path.exists(cache_path): 18 | rop.load_gadgets(cache_path) 19 | else: 20 | rop.find_gadgets() 21 | rop.save_gadgets(cache_path) 22 | 23 | # make sure the target gadget exist 24 | gadgets = [x for x in rop._all_gadgets if x.addr == 0x402503] 25 | assert len(gadgets) == 1 26 | gadget = gadgets[0] 27 | 28 | # build a ROP chain using the gadget 29 | chain = angrop.rop_chain.RopChain(proj, rop.chain_builder) 30 | chain.add_gadget(gadget) 31 | chain.add_value(0x41414141) 32 | 33 | # make sure the execution succeeds 34 | state = chain.exec() 35 | assert not state.regs.rdi.symbolic 36 | assert state.solver.eval(state.regs.rdi == 0x41414141) 37 | 38 | def run_all(): 39 | functions = globals() 40 | all_functions = {x:y for x, y in functions.items() if x.startswith('test_')} 41 | for f in sorted(all_functions.keys()): 42 | if hasattr(all_functions[f], '__call__'): 43 | all_functions[f]() 44 | 45 | if __name__ == "__main__": 46 | import sys 47 | import logging 48 | 49 | logging.getLogger("angrop.rop").setLevel(logging.DEBUG) 50 | 51 | if len(sys.argv) > 1: 52 | globals()['test_' + sys.argv[1]]() 53 | else: 54 | run_all() 55 | --------------------------------------------------------------------------------