├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── docs ├── IDAPython_on_IDADemo.md ├── bap-view.png ├── bir-attr-saluki-decompiler.png ├── bir-attr-saluki.png ├── taint-decompiler.png └── taint.png ├── plugins ├── bap │ ├── __init__.py │ ├── plugins │ │ ├── __init__.py │ │ ├── bap_bir_attr.py │ │ ├── bap_clear_comments.py │ │ ├── bap_comments.py │ │ ├── bap_functions.py │ │ ├── bap_taint_ptr.py │ │ ├── bap_taint_reg.py │ │ ├── bap_task_manager.py │ │ ├── bap_trace.py │ │ ├── bap_view.py │ │ ├── pseudocode_bap_comment.py │ │ └── pseudocode_bap_taint.py │ └── utils │ │ ├── __init__.py │ │ ├── _comment_handler.py │ │ ├── _ctyperewriter.py │ │ ├── _service.py │ │ ├── abstract_ida_plugins.py │ │ ├── bap_comment.py │ │ ├── bap_taint.py │ │ ├── config.py │ │ ├── hexrays.py │ │ ├── ida.py │ │ ├── run.py │ │ ├── sexp.py │ │ └── trace.py └── plugin_loader_bap.py ├── setup.py ├── tests ├── conftest.py ├── mockidaapi.py ├── mockidautils.py ├── mockidc.py ├── test_bap_comment.py ├── test_config.py ├── test_ida.py ├── test_run.py ├── test_sexp.py └── test_trace.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.5" 5 | install: pip install tox 6 | script: tox 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | ----- 3 | * fixes IDA loader 4 | * more robust comment parser 5 | 6 | 1.1.0 7 | ----- 8 | * call BAP asynchronously (without blocking IDA) 9 | * run several instances of BAP in parallel 10 | * special attribute view (instead of `Alt-T` search) 11 | * neater comment syntax (attr=value instead of sexp) 12 | * task manager for primitive job control 13 | * plugins are now callable from the menu (try `Ctrl-3`) 14 | * each instance has its own view 15 | * view selector can switch between views 16 | * stderr and stdout are properly dumped into the view 17 | * cross-platform implementation (Docker, Windows should work) 18 | * more robust type emition 19 | * new generic ida service integration (for calls to IDA from BAP) 20 | * added unit tests 21 | * Travis-CI integration 22 | * code refactoring: more pythonic, PEP8 compilant, pylint-happy 23 | 24 | 0.1.0 25 | ----- 26 | * initial release 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 BinaryAnalysisPlatform 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/BinaryAnalysisPlatform/bap.svg?branch=master)](https://travis-ci.org/BinaryAnalysisPlatform/bap-ida-python) 2 | 3 | BAP IDA Python 4 | ============== 5 | 6 | This package provides the necessary IDAPython scripts required for 7 | interoperatibility between BAP and IDA Pro. It also provides many 8 | useful feature additions to IDA, by leveraging power from BAP. 9 | 10 | Features 11 | -------- 12 | 13 | BAP-IDA integration package installs several plugins into IDA 14 | distribution. Some plugins works automatically, and do not require 15 | user intervention, while others are invoked with keybindings, or via 16 | the `Edit->Plugins` menu, that can be popped at with the `Ctrl-3` 17 | bindging. 18 | 19 | 20 | ### Function information augmentation 21 | 22 | By just hitting the `Shift+P` key, IDA will call BAP which will use 23 | its own analysis (and all the information sources that it knows of) to 24 | obtain all the locations where there are functions. This information 25 | is then propagated to IDA and used to create functions there 26 | automatically. This is especially useful in scenarios where there are 27 | a lot of indirect calls etc and BAP (using its different plugins) is 28 | able to detect functions in the code which IDA is unable to do so. 29 | 30 | ### Taint Propagation 31 | 32 | By choosing a taint source and hitting either `Ctrl+A` (for tainting 33 | an immediate value) or `Ctrl+Shift+A` (for data pointed by a value), 34 | one can easily see how taint propagates through the code, in both 35 | disassembly and decompilation views. 36 | 37 | #### In Text/Graph View 38 | ![taint](docs/taint.png) 39 | 40 | #### In Pseudocode View 41 | ![taint-decompiler](docs/taint-decompiler.png) 42 | 43 | ### BIR Attribute Tagging, with arbitrary BAP plugins 44 | 45 | BAP has the ability to tag a lot of possible attributes to 46 | instructions. These BIR attributes can be tagged automatically as 47 | comments in IDA, by running arbitrary plugins in BAP. Just hit 48 | `Shift+S`. 49 | 50 | Here's an example of output for Saluki showing that a certain malloc 51 | is unchecked (pointing to a potential vulnerability). 52 | 53 | Clearing all BAP comments (without affecting your own personal 54 | comments in IDA) can be done by pressing `Ctrl+Shift+S`. 55 | 56 | To view all current attributes in the single window hit `Shift-B`. 57 | You can sort attributes by clicking the columns or you can search 58 | through them using IDA's extensive search facilities (hit the `Help` 59 | bottom on the list to get more information). You can jump directly 60 | to the attribute location by selecting it. 61 | 62 | #### In Text/Graph View 63 | ![bir-attr-saluki](docs/bir-attr-saluki.png) 64 | 65 | #### In Pseudocode View 66 | ![bir-attr-saluki-decompiler](docs/bir-attr-saluki-decompiler.png) 67 | 68 | ### BAP Task Manager and Viewer 69 | 70 | Every instance of BAP will have a corresponding view, that will 71 | accumulate all data written by BAP. The BAP Viewer (`Ctrl-Shift-F5`) 72 | provides an easy way to switch between multiple BAP Views. 73 | 74 | Since you can run multiple instances of BAP asynchronously, it is 75 | useful to have an ability to view the state of currently running 76 | processes, and, even, to terminate those who take too much time or 77 | memory. The BAP Task Manager (accessible via the `Ctrl-Alt-Shift-F5` 78 | keybinding, or via the `Ctrl-3` plugin menu) provides such 79 | functionality. 80 | 81 | ![bap-view](docs/bap-view.png) 82 | 83 | ### Symbol and Type Information 84 | 85 | Whenever possible, `bap-ida-python` passes along the latest symbol and 86 | type information from IDA (including changes you might have made 87 | manually), so as to aid better and more accurate analysis in BAP. For 88 | example, let's say you recognize that a function is a malloc in a 89 | stripped binary, by just using IDA's rename feature (Keybinding: `N`), 90 | you can inform BAP of this change during the next run of, say, saluki, 91 | without needing to do anything extra. It works automagically! 92 | 93 | Installation 94 | ------------ 95 | 96 | Copy all of the files and directories from the `plugins` directory 97 | into `$IDADIR/plugins`. 98 | 99 | The first run of IDA after that will prompt you to provide the path to 100 | BAP (along with a default if IDA is able to automatically detect 101 | BAP). If you wish to edit the path to BAP manually later, you can edit 102 | the file `$IDADIR/cfg/bap.cfg`. 103 | 104 | #### Opam? 105 | 106 | It is usually much easier to install through opam if you have already 107 | followed all the installation steps in the 108 | [bap repository](https://github.com/BinaryAnalysisPlatform/bap). Just 109 | run: 110 | 111 | ``` 112 | opam install bap-ida 113 | ``` 114 | After the installation you'll see few commands that you need to run, e.g.: 115 | ``` 116 | => In order to install bap-ida-python plugin: 117 | rm -rf $IDA_PATH/plugins/bap/ 118 | cp $(opam config var prefix)/share/bap-ida-python/plugin_loader_bap.py $IDA_PATH/plugins/ 119 | cp -r $(opam config var prefix)/share/bap-ida-python/bap $IDA_PATH/plugins/ 120 | cp $(opam config var prefix)/share/bap-ida-python/bap.cfg $IDA_PATH/cfg/ 121 | ... 122 | ``` 123 | where `IDA_PATH` denotes the root of IDA Pro installation. 124 | 125 | You need to run all the commands manually because of the sandboxing enabled in 126 | modern versions of opam, that doesn't allow to install files outside 127 | the opam directory. 128 | 129 | Debugging 130 | --------- 131 | 132 | The integration package is still in alpha stage, so there are a few 133 | bugs lurking in the codebase. If you have any issues, then, please, 134 | enable the debug mode, by typing the following command in the IDA's 135 | python console: 136 | 137 | ```python 138 | BapIda.DEBUG=True 139 | ``` 140 | 141 | This will increase the verbosity level, so that you can see what commands 142 | were actually issued to the bap backend. In the debug mode, the temporary 143 | files will not be removed, so they can be archived and sent to us, for the 144 | ease of debugging. 145 | 146 | 147 | #### IDA Demo? 148 | 149 | You can also use parts of the functionality (i.e. most of everything 150 | except for the decompiler outputs, and batch processing from bap) with 151 | IDA Free/Demo. However, you would need to install IDAPython. See 152 | [here](docs/IDAPython_on_IDADemo.md) for what one of our users 153 | reported to work. 154 | -------------------------------------------------------------------------------- /docs/IDAPython_on_IDADemo.md: -------------------------------------------------------------------------------- 1 | How to get IDA Python to work with IDA Demo 2 | =========================================== 3 | 4 | Go to the IDAPython [binaries page](https://github.com/idapython/bin). 5 | Download the latest `_linux.zip` file and extract it. In my case, it was `idapython-6.9.0-python2.7-linux.zip`. 6 | Follow the instructions in its `README.txt`. 7 | 8 | For simplicity, I have copy pasted the relevant portions here: 9 | 10 | ``` 11 | 1. Install 2.6 or 2.7 from http://www.python.org/ 12 | 2. Copy the whole "python" directory to %IDADIR% 13 | 3. Copy the contents of the "plugins" directory to the %IDADIR%\plugins\ 14 | 4. Copy "python.cfg" to %IDADIR%\cfg 15 | ``` 16 | 17 | In order to do step 1 correctly on a 64-bit Ubuntu 14.04, I had to run `sudo apt-get install libpython2.7:i386` and get all the libraries needed with their 32 bit versions. 18 | Rest of the steps are quite straight forward. 19 | BTW, `%IDADIR%` is the directory where you have IDA extracted/installed. 20 | 21 | Now, whenever you open IDA, you will have access to IDA Python. 22 | 23 | Note: Some of the functions like `idaapi.init_hexrays_plugin()` will obviously not work (since you don't have a decompiler in Demo), but most things should work otherwise. -------------------------------------------------------------------------------- /docs/bap-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryAnalysisPlatform/bap-ida-python/d8d4679de2f50bb75f556419565821d95404034e/docs/bap-view.png -------------------------------------------------------------------------------- /docs/bir-attr-saluki-decompiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryAnalysisPlatform/bap-ida-python/d8d4679de2f50bb75f556419565821d95404034e/docs/bir-attr-saluki-decompiler.png -------------------------------------------------------------------------------- /docs/bir-attr-saluki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryAnalysisPlatform/bap-ida-python/d8d4679de2f50bb75f556419565821d95404034e/docs/bir-attr-saluki.png -------------------------------------------------------------------------------- /docs/taint-decompiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryAnalysisPlatform/bap-ida-python/d8d4679de2f50bb75f556419565821d95404034e/docs/taint-decompiler.png -------------------------------------------------------------------------------- /docs/taint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryAnalysisPlatform/bap-ida-python/d8d4679de2f50bb75f556419565821d95404034e/docs/taint.png -------------------------------------------------------------------------------- /plugins/bap/__init__.py: -------------------------------------------------------------------------------- 1 | """This module contains all of BAP-IDA-Python's code in one directory.""" 2 | 3 | __all__ = ('plugins', 'utils') 4 | -------------------------------------------------------------------------------- /plugins/bap/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """This module contains all the plugins that will be loaded by BAP-Loader.""" 2 | 3 | __all__ = ('bap_bir_attr', 'bap_taint_ptr', 'bap_taint_reg' 'bap_view', 'pseudocode_bap_comment', 4 | 'pseudocode_bap_taint') 5 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_bir_attr.py: -------------------------------------------------------------------------------- 1 | """ 2 | IDA Python Plugin to get BIR attributes from an arbitrary BAP execution. 3 | 4 | Allows user to run any BAP plugins and get information from BIR attributes, 5 | into comments in IDA. Use the selected line's address in the command using 6 | "{screen_ea}". 7 | 8 | Keybindings: 9 | Shift-S : Open up a window to accept arbitrary BAP commands, 10 | and select arbitrary BIR attributes to output to IDA comments 11 | 12 | Comment Format: 13 | Comments in the Text/Graph Views are added using a key-value storage 14 | with the format (BAP (k1 v1) (k2 v2) ...) 15 | """ 16 | import idaapi 17 | import idc 18 | 19 | from bap.utils.run import BapIda 20 | 21 | 22 | class BapScripter(BapIda): 23 | 24 | def __init__(self, user_args, attrs): 25 | super(BapScripter, self).__init__() 26 | if attrs: 27 | self.action = 'extracting ' + ','.join(attrs) 28 | else: 29 | self.action = 'running bap ' + user_args 30 | self.script = self.tmpfile('py') 31 | self.args += user_args.split(' ') 32 | self.args += [ 33 | '--emit-ida-script', 34 | '--emit-ida-script-file', self.script.name 35 | ] 36 | self.args += [ 37 | '--emit-ida-script-attr='+attr.strip() 38 | for attr in attrs 39 | ] 40 | 41 | 42 | # perfectly random numbers 43 | ARGS_HISTORY = 324312 44 | ATTR_HISTORY = 234345 45 | 46 | 47 | class BapBirAttr(idaapi.plugin_t): 48 | """ 49 | Plugin to get BIR attributes from arbitrary BAP executions. 50 | 51 | Also supports installation of callbacks using install_callback() 52 | """ 53 | flags = idaapi.PLUGIN_FIX | idaapi.PLUGIN_DRAW 54 | comment = "Run BAP " 55 | help = "Runs BAP and extracts data from the output" 56 | wanted_name = "BAP: Run" 57 | wanted_hotkey = "Shift-S" 58 | 59 | _callbacks = [] 60 | 61 | recipes = {} 62 | 63 | def _do_callbacks(self, ea): 64 | for callback in self._callbacks: 65 | callback({'ea': ea}) 66 | 67 | def run(self, arg): 68 | """ 69 | Ask user for BAP args to pass, BIR attributes to print; and run BAP. 70 | 71 | Allows users to also use {screen_ea} in the BAP args to get the 72 | address at the location pointed to by the cursor. 73 | """ 74 | 75 | args_msg = "Arguments that will be passed to `bap'" 76 | # If a user is not fast enough in providing the answer 77 | # IDA Python will popup a modal window that will block 78 | # a user from providing the answer. 79 | idaapi.disable_script_timeout() 80 | args = idaapi.askstr(ARGS_HISTORY, '--passes=', args_msg) 81 | if args is None: 82 | return 83 | attr_msg = "A comma separated list of attributes,\n" 84 | attr_msg += "that should be propagated to comments" 85 | attr_def = self.recipes.get(args, '') 86 | attr = idaapi.askstr(ATTR_HISTORY, attr_def, attr_msg) 87 | 88 | if attr is None: 89 | return 90 | 91 | # store a choice of attributes for the given set of arguments 92 | # TODO: store recipes in IDA's database 93 | self.recipes[args] = attr 94 | ea = idc.ScreenEA() 95 | attrs = [] 96 | if attr != '': 97 | attrs = attr.split(',') 98 | analysis = BapScripter(args, attrs) 99 | analysis.on_finish(lambda bap: self.load_script(bap, ea)) 100 | analysis.run() 101 | 102 | def load_script(self, bap, ea): 103 | idc.SetStatus(idc.IDA_STATUS_WORK) 104 | idaapi.IDAPython_ExecScript(bap.script.name, globals()) 105 | self._do_callbacks(ea) 106 | idc.Refresh() 107 | # do we really need to call this? 108 | idaapi.refresh_idaview_anyway() 109 | idc.SetStatus(idc.IDA_STATUS_READY) 110 | 111 | def init(self): 112 | """Initialize Plugin.""" 113 | return idaapi.PLUGIN_KEEP 114 | 115 | def term(self): 116 | """Terminate Plugin.""" 117 | pass 118 | 119 | @classmethod 120 | def install_callback(cls, callback_fn): 121 | """ 122 | Install callback to be run when the user calls for BAP execution. 123 | 124 | Callback must take a dict and must return nothing. 125 | 126 | Dict is guaranteed to get the following keys: 127 | 'ea': The value of EA at point where user propagated taint from. 128 | """ 129 | idc.Message('a callback is installed\n') 130 | cls._callbacks.append(callback_fn) 131 | 132 | 133 | def PLUGIN_ENTRY(): 134 | """Install BAP_BIR_Attr upon entry.""" 135 | return BapBirAttr() 136 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_clear_comments.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | from idaapi import ASKBTN_YES 3 | 4 | from bap.utils import bap_comment 5 | from bap.utils import ida 6 | 7 | 8 | class BapClearComments(idaapi.plugin_t): 9 | flags = idaapi.PLUGIN_DRAW | idaapi.PLUGIN_FIX 10 | comment = "removes all BAP comments" 11 | help = "" 12 | wanted_name = "BAP: Clear comments" 13 | wanted_hotkey = "Ctrl-Shift-S" 14 | 15 | def clear_bap_comments(self): 16 | """Ask user for confirmation and then clear (BAP ..) comments.""" 17 | 18 | if idaapi.askyn_c(ASKBTN_YES, 19 | "Delete all `BAP: ..` comments?") != ASKBTN_YES: 20 | return 21 | 22 | for ea in ida.addresses(): # TODO: store actually commented addresses 23 | comm = idaapi.get_cmt(ea, 0) 24 | if comm and comm.startswith('BAP:'): 25 | idaapi.set_cmt(ea, '', 0) 26 | 27 | def init(self): 28 | return idaapi.PLUGIN_KEEP 29 | 30 | def run(self, arg): 31 | self.clear_bap_comments() 32 | 33 | def term(self): pass 34 | 35 | 36 | def PLUGIN_ENTRY(): 37 | return BapClearComments() 38 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_comments.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import idc 3 | 4 | from bap.utils import bap_comment, ida 5 | 6 | 7 | class Attributes(idaapi.Choose2): 8 | def __init__(self, comms): 9 | idaapi.Choose2.__init__(self, 'Select an attribute', [ 10 | ['name', 8], 11 | ['addr', 8], 12 | ['data', 64] 13 | ]) 14 | self.comms = [ 15 | [name, '{:#x}'.format(addr), ' '.join(data)] 16 | for (name, addr, data) in comms 17 | ] 18 | 19 | def OnClose(self): 20 | pass 21 | 22 | def OnGetSize(self): 23 | return len(self.comms) 24 | 25 | def OnGetLine(self, n): 26 | return self.comms[n] 27 | 28 | 29 | class BapComment(idaapi.plugin_t): 30 | flags = idaapi.PLUGIN_FIX 31 | help = 'propagate comments to IDA Views' 32 | comment = '' 33 | wanted_name = 'BAP: View BAP Attributes' 34 | wanted_hotkey = 'Shift-B' 35 | 36 | def __init__(self): 37 | self.comms = {} 38 | 39 | def init(self): 40 | ida.comment.register_handler(self.update) 41 | return idaapi.PLUGIN_KEEP 42 | 43 | def run(self, arg): 44 | comms = {} 45 | for addr in ida.addresses(): 46 | comm = idaapi.get_cmt(addr, 0) 47 | if comm: 48 | try: 49 | parsed = bap_comment.parse(comm) 50 | if parsed: 51 | for (name, data) in parsed.items(): 52 | comms[(addr, name)] = data 53 | except: 54 | idc.Message("BAP> failed to parse string {0}\n{1}". 55 | format(comm, str(sys.exc_info()[1]))) 56 | comms = [(name, addr, data) 57 | for ((addr, name), data) in comms.items()] 58 | attrs = Attributes(comms) 59 | choice = attrs.Show(modal=True) 60 | if choice >= 0: 61 | idc.Jump(comms[choice][1]) 62 | 63 | def term(self): 64 | pass 65 | 66 | def update(self, ea, key, value): 67 | """Add key=values to comm string at EA.""" 68 | cmt = idaapi.get_cmt(ea, 0) 69 | comm = cmt and bap_comment.parse(cmt) or {} 70 | values = comm.setdefault(key, []) 71 | if value and value != '()' and value not in values: 72 | values.append(value) 73 | idaapi.set_cmt(ea, bap_comment.dumps(comm), 0) 74 | 75 | 76 | def PLUGIN_ENTRY(): 77 | return BapComment() 78 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | IDA Python Plugin to get information about functions from BAP into IDA. 3 | 4 | Finds all the locations in the executable that BAP knows to be functions and 5 | marks them as such in IDA. 6 | 7 | Keybindings: 8 | Shift-P : Run BAP and mark code as functions in IDA 9 | """ 10 | import idaapi 11 | import idc 12 | 13 | from heapq import heappush, heappop 14 | from bap.utils.run import BapIda 15 | 16 | 17 | class FunctionFinder(BapIda): 18 | def __init__(self): 19 | super(FunctionFinder, self).__init__(symbols=False) 20 | self.action = 'looking for function starts' 21 | self.syms = self.tmpfile('syms', mode='r') 22 | self.args += [ 23 | '--print-symbol-format', 'addr', 24 | '--dump', 'symbols:{0}'.format(self.syms.name) 25 | ] 26 | 27 | # we can be a little bit more promiscuous since IDA will ignore 28 | # function starts that occur in the middle of a function 29 | if 'byteweight' in self.plugins and not \ 30 | '--no-byteweight' in self.args: 31 | self.args += [ 32 | '--byteweight-threshold', '0.5', 33 | '--byteweight-length', '4', 34 | ] 35 | 36 | 37 | class BAP_Functions(idaapi.plugin_t): 38 | """Uses BAP to find missed functions""" 39 | 40 | flags = idaapi.PLUGIN_FIX 41 | comment = "BAP Functions Plugin" 42 | help = "BAP Functions Plugin" 43 | wanted_name = "BAP: Discover functions" 44 | wanted_hotkey = "Shift-P" 45 | 46 | def mark_functions(self): 47 | """Run BAP, get functions, and mark them in IDA.""" 48 | analysis = FunctionFinder() 49 | analysis.on_finish(lambda x: self.add_starts(x)) 50 | analysis.run() 51 | 52 | def add_starts(self, bap): 53 | syms = [] 54 | for line in bap.syms: 55 | heappush(syms, int(line, 16)) 56 | for i in range(len(syms)): 57 | idaapi.add_func(heappop(syms), idaapi.BADADDR) 58 | idc.Refresh() 59 | idaapi.refresh_idaview_anyway() 60 | 61 | def init(self): 62 | """Initialize Plugin.""" 63 | return idaapi.PLUGIN_KEEP 64 | 65 | def term(self): 66 | """Terminate Plugin.""" 67 | pass 68 | 69 | def run(self, arg): 70 | self.mark_functions() 71 | 72 | 73 | def PLUGIN_ENTRY(): 74 | """Install BAP_Functions upon entry.""" 75 | return BAP_Functions() 76 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_taint_ptr.py: -------------------------------------------------------------------------------- 1 | from bap.utils.bap_taint import BapTaint 2 | 3 | class BapTaintPtr(BapTaint): 4 | wanted_hotkey = "Ctrl-Shift-A" 5 | def __init__(self): 6 | super(BapTaintPtr,self).__init__('ptr') 7 | 8 | 9 | def PLUGIN_ENTRY(): 10 | return BapTaintPtr() 11 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_taint_reg.py: -------------------------------------------------------------------------------- 1 | from bap.utils.bap_taint import BapTaint 2 | 3 | class BapTaintReg(BapTaint): 4 | wanted_hotkey = "Shift-A" 5 | def __init__(self): 6 | super(BapTaintReg,self).__init__('reg') 7 | 8 | 9 | def PLUGIN_ENTRY(): 10 | return BapTaintReg() 11 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_task_manager.py: -------------------------------------------------------------------------------- 1 | """ BAP Task Manager Form """ 2 | 3 | #pylint: disable=missing-docstring,unused-argument,no-self-use,invalid-name 4 | 5 | from __future__ import print_function 6 | 7 | import idaapi #pylint: disable=import-error 8 | 9 | from bap.utils.run import BapIda 10 | 11 | class BapSelector(idaapi.Choose2): 12 | #pylint: disable=invalid-name,missing-docstring,no-self-use 13 | def __init__(self): 14 | idaapi.Choose2.__init__(self, 'Choose instances to kill', [ 15 | ['#', 2], 16 | ['PID', 4], 17 | ['Action', 40], 18 | ], flags=idaapi.Choose2.CH_MULTI) 19 | self.selection = [] 20 | self.instances = list(BapIda.instances) 21 | 22 | def select(self): 23 | choice = self.Show(modal=True) 24 | if choice < 0: 25 | return [self.instances[i] for i in self.selection] 26 | else: 27 | return [self.instances[choice]] 28 | 29 | def OnClose(self): 30 | pass 31 | 32 | def OnGetLine(self, n): 33 | bap = self.instances[n] 34 | return [str(n), str(bap.proc.pid), bap.action] 35 | 36 | def OnGetSize(self): 37 | return len(self.instances) 38 | 39 | def OnSelectionChange(self, selected): 40 | self.selection = selected 41 | 42 | 43 | class BapTaskManager(idaapi.plugin_t): 44 | #pylint: disable=no-init 45 | flags = idaapi.PLUGIN_FIX | idaapi.PLUGIN_DRAW 46 | wanted_hotkey = "Ctrl-Alt-Shift-F5" 47 | comment = "bap task manager" 48 | help = "Open BAP Task Manager" 49 | wanted_name = "BAP: Task Manager" 50 | 51 | def run(self, arg): 52 | chooser = BapSelector() 53 | selected = chooser.select() 54 | for bap in selected: 55 | if bap in BapIda.instances: 56 | print('BAP> terminating '+str(bap.proc.pid)) 57 | bap.cancel() 58 | else: 59 | print("BAP> instance {0} has already finised". 60 | format(bap.proc.pid)) 61 | 62 | def term(self): 63 | pass 64 | 65 | def init(self): 66 | return idaapi.PLUGIN_KEEP 67 | 68 | 69 | def PLUGIN_ENTRY(): 70 | return BapTaskManager() 71 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_trace.py: -------------------------------------------------------------------------------- 1 | # noqa: ignore=F811 2 | 3 | import idaapi 4 | 5 | import os 6 | 7 | 8 | from bap.utils import trace 9 | from PyQt5 import QtWidgets 10 | from PyQt5 import QtGui 11 | 12 | from PyQt5.QtCore import ( 13 | Qt, 14 | QFile, 15 | QIODevice, 16 | QCryptographicHash as QCrypto, 17 | QRegExp, 18 | QTimer, 19 | QAbstractItemModel, 20 | QModelIndex, 21 | QVariant, 22 | pyqtSignal, 23 | QSortFilterProxyModel) 24 | 25 | 26 | def add_insn_to_trace_view(ea, tid=1): 27 | idaapi.dbg_add_tev(1, tid, ea) 28 | 29 | 30 | @trace.handler('pc-changed', requires=['machine-id', 'pc']) 31 | def tev_insn(state, ev): 32 | "stores each visited instruction to the IDA Trace Window" 33 | add_insn_to_trace_view(state['pc'], tid=state['machine-id']) 34 | 35 | 36 | @trace.handler('pc-changed', requires=['pc']) 37 | def tev_insn0(state, ev): 38 | """stores each visited instruction to the IDA Trace Window. 39 | 40 | But doesn't set the pid/tid field, and keep it equal to 0 41 | (This enables interoperation with the debugger) 42 | """ 43 | add_insn_to_trace_view(state['pc']) 44 | 45 | 46 | @trace.handler('call', requires=['machine-id', 'pc']) 47 | def tev_call(state, call): 48 | "stores call events to the IDA Trace Window" 49 | caller = state['pc'] 50 | callee = idaapi.get_name_ea(0, call[0]) 51 | idaapi.dbg_add_call_tev(state['machine-id'], caller, callee) 52 | 53 | 54 | incidents = [] 55 | locations = {} 56 | 57 | 58 | @trace.handler('incident') 59 | def incident(state, data): 60 | incidents.append(Incident(data[0], [int(x) for x in data[1:]])) 61 | 62 | 63 | @trace.handler('incident-location') 64 | def incident_location(state, data): 65 | id = int(data[0]) 66 | locations[id] = [parse_point(p) for p in data[1]] 67 | 68 | 69 | # we are using PyQt5 here, because IDAPython relies on a system 70 | # openssl 0.9.8 which is quite outdated and not available on most 71 | # modern installations 72 | def md5sum(filename): 73 | """computes md5sum of a file with the given ``filename`` 74 | 75 | The return value is a 32 byte hexadecimal ASCII representation of 76 | the md5 sum (same value as returned by the ``md5sum filename`` command) 77 | """ 78 | stream = QFile(filename) 79 | if not stream.open(QIODevice.ReadOnly | QIODevice.Text): 80 | raise IOError("Can't open file: " + filename) 81 | hasher = QCrypto(QCrypto.Md5) 82 | if not hasher.addData(stream): 83 | raise ValueError('Unable to hash file: ' + filename) 84 | stream.close() 85 | return str(hasher.result().toHex()) 86 | 87 | 88 | class HandlerSelector(QtWidgets.QGroupBox): 89 | def __init__(self, parent=None): 90 | super(HandlerSelector, self).__init__("Trace Event Processors", parent) 91 | self.setFlat(True) 92 | box = QtWidgets.QVBoxLayout(self) 93 | self.options = {} 94 | for name in trace.handlers: 95 | btn = QtWidgets.QCheckBox(name) 96 | btn.setToolTip(trace.handlers[name].__doc__) 97 | box.addWidget(btn) 98 | self.options[name] = btn 99 | box.addStretch(1) 100 | self.setCheckable(True) 101 | self.setChecked(True) 102 | self.setLayout(box) 103 | 104 | 105 | class MachineSelector(QtWidgets.QWidget): 106 | def __init__(self, parent=None): 107 | super(MachineSelector, self).__init__(parent) 108 | box = QtWidgets.QHBoxLayout(self) 109 | label = MonitoringLabel('List of &machines (threads)') 110 | self.is_ready = label.is_ready 111 | self.updated = label.updated 112 | box.addWidget(label) 113 | self._machines = QtWidgets.QLineEdit('all') 114 | self._machines.setToolTip('an integer, \ 115 | a comma-separated list of integers, or "all"') 116 | grammar = QRegExp(r'\s*(all|\d+\s*(,\s*\d+\s*)*)\s*') 117 | valid = QtGui.QRegExpValidator(grammar) 118 | self._machines.setValidator(valid) 119 | label.setBuddy(self._machines) 120 | box.addWidget(self._machines) 121 | box.addStretch(1) 122 | self.setLayout(box) 123 | 124 | def selected(self): 125 | if not self._machines.hasAcceptableInput(): 126 | raise ValueError('invalid input') 127 | data = self._machines.text().strip() 128 | if data == 'all': 129 | return None 130 | else: 131 | return [int(x) for x in data.split(',')] 132 | 133 | 134 | class MonitoringLabel(QtWidgets.QLabel): 135 | "a label that will monitors the validity of its buddy" 136 | 137 | updated = pyqtSignal() 138 | 139 | def __init__(self, text='', buddy=None, parent=None): 140 | super(MonitoringLabel, self).__init__(parent) 141 | self.setText(text) 142 | if buddy: 143 | self.setBuddy(buddy) 144 | 145 | def setText(self, text): 146 | super(MonitoringLabel, self).setText(text) 147 | self.text = text 148 | 149 | def setBuddy(self, buddy): 150 | super(MonitoringLabel, self).setBuddy(buddy) 151 | buddy.textChanged.connect(lambda x: self.update()) 152 | self.update() 153 | 154 | def is_ready(self): 155 | return not self.buddy() or self.buddy().hasAcceptableInput() 156 | 157 | def update(self): 158 | self.updated.emit() 159 | if self.is_ready(): 160 | super(MonitoringLabel, self).setText(self.text) 161 | else: 162 | super(MonitoringLabel, self).setText( 163 | ''+self.text+'') 164 | 165 | 166 | class ExistingFileValidator(QtGui.QValidator): 167 | def __init__(self, parent=None): 168 | super(ExistingFileValidator, self).__init__(parent) 169 | 170 | def validate(self, name, pos): 171 | if os.path.isfile(name): 172 | return (self.Acceptable, name, pos) 173 | else: 174 | return (self.Intermediate, name, pos) 175 | 176 | 177 | class TraceFileSelector(QtWidgets.QWidget): 178 | 179 | def __init__(self, parent=None): 180 | super(TraceFileSelector, self).__init__(parent) 181 | box = QtWidgets.QHBoxLayout(self) 182 | label = MonitoringLabel('Trace &file:') 183 | self.is_ready = label.is_ready 184 | self.updated = label.updated 185 | box.addWidget(label) 186 | self.location = QtWidgets.QLineEdit('incidents') 187 | self.text = self.location.text 188 | must_exist = ExistingFileValidator() 189 | self.location.setValidator(must_exist) 190 | label.setBuddy(self.location) 191 | box.addWidget(self.location) 192 | openfile = QtWidgets.QPushButton(self) 193 | openfile.setIcon(self.style().standardIcon( 194 | QtWidgets.QStyle.SP_DialogOpenButton)) 195 | dialog = QtWidgets.QFileDialog(self) 196 | openfile.clicked.connect(dialog.open) 197 | dialog.fileSelected.connect(self.location.setText) 198 | box.addWidget(openfile) 199 | box.addStretch(1) 200 | self.setLayout(box) 201 | 202 | 203 | class IncidentView(QtWidgets.QWidget): 204 | def __init__(self, parent=None): 205 | super(IncidentView, self).__init__(parent) 206 | self.view = QtWidgets.QTreeView() 207 | self.view.setAllColumnsShowFocus(True) 208 | self.view.setUniformRowHeights(True) 209 | box = QtWidgets.QVBoxLayout() 210 | box.addWidget(self.view) 211 | self.load_trace = QtWidgets.QPushButton('&Trace') 212 | self.load_trace.setToolTip('Load into the Trace Window') 213 | self.load_trace.setEnabled(False) 214 | for activation_signal in [ 215 | self.view.activated, 216 | self.view.entered, 217 | self.view.pressed]: 218 | activation_signal.connect(lambda _: self.update_controls_state()) 219 | self.load_trace.clicked.connect(self.load_current_trace) 220 | self.view.doubleClicked.connect(self.jump_to_index) 221 | hbox = QtWidgets.QHBoxLayout() 222 | self.filter = QtWidgets.QLineEdit() 223 | self.filter.textChanged.connect(self.filter_model) 224 | filter_label = QtWidgets.QLabel('&Search') 225 | filter_label.setBuddy(self.filter) 226 | hbox.addWidget(filter_label) 227 | hbox.addWidget(self.filter) 228 | hbox.addWidget(self.load_trace) 229 | box.addLayout(hbox) 230 | self.setLayout(box) 231 | self.model = None 232 | self.proxy = None 233 | 234 | def display(self, incidents, locations): 235 | self.model = IncidentModel(incidents, locations, self) 236 | self.proxy = QSortFilterProxyModel(self) 237 | self.proxy.setSourceModel(self.model) 238 | self.proxy.setFilterRole(self.model.filter_role) 239 | self.proxy.setFilterRegExp(QRegExp(self.filter.text())) 240 | self.view.setModel(self.proxy) 241 | 242 | def filter_model(self, txt): 243 | if self.proxy: 244 | self.proxy.setFilterRegExp(QRegExp(txt)) 245 | 246 | def update_controls_state(self): 247 | curr = self.view.currentIndex() 248 | self.load_trace.setEnabled(curr.isValid() and 249 | curr.parent().isValid()) 250 | 251 | def load_current_trace(self): 252 | idx = self.proxy.mapToSource(self.view.currentIndex()) 253 | if not idx.isValid() or index_level(idx) not in (1, 2): 254 | raise ValueError('load_current_trace: invalid index') 255 | 256 | if index_level(idx) == 2: 257 | idx = idx.parent() 258 | 259 | incident = self.model.incidents[idx.parent().row()] 260 | location = incident.locations[idx.row()] 261 | backtrace = self.model.locations[location] 262 | 263 | for p in reversed(backtrace): 264 | self.load_trace_point(p) 265 | 266 | def jump_to_index(self, idx): 267 | idx = self.proxy.mapToSource(idx) 268 | if index_level(idx) != 2: 269 | # don't mess with parents, they are used to create children 270 | return 271 | grandpa = idx.parent().parent() 272 | incident = self.model.incidents[grandpa.row()] 273 | location = incident.locations[idx.parent().row()] 274 | trace = self.model.locations[location] 275 | point = trace[idx.row()] 276 | self.show_trace_point(point) 277 | 278 | def load_trace_point(self, p): 279 | add_insn_to_trace_view(p.addr) 280 | 281 | def show_trace_point(self, p): 282 | idaapi.jumpto(p.addr) 283 | 284 | 285 | class TraceLoaderController(QtWidgets.QWidget): 286 | finished = pyqtSignal() 287 | 288 | def __init__(self, parent=None): 289 | super(TraceLoaderController, self).__init__(parent) 290 | self.loader = None 291 | box = QtWidgets.QVBoxLayout(self) 292 | self.location = TraceFileSelector(self) 293 | self.handlers = HandlerSelector(self) 294 | self.machines = MachineSelector(self) 295 | box.addWidget(self.location) 296 | box.addWidget(self.handlers) 297 | box.addWidget(self.machines) 298 | self.load = QtWidgets.QPushButton('&Load') 299 | self.load.setDefault(True) 300 | self.load.setEnabled(self.location.is_ready()) 301 | self.cancel = QtWidgets.QPushButton('&Stop') 302 | self.cancel.setVisible(False) 303 | hor = QtWidgets.QHBoxLayout() 304 | hor.addWidget(self.load) 305 | hor.addWidget(self.cancel) 306 | self.progress = QtWidgets.QProgressBar() 307 | self.progress.setVisible(False) 308 | hor.addWidget(self.progress) 309 | hor.addStretch(2) 310 | box.addLayout(hor) 311 | 312 | def enable_load(): 313 | self.load.setEnabled(self.location.is_ready() and 314 | self.machines.is_ready()) 315 | self.location.updated.connect(enable_load) 316 | self.machines.updated.connect(enable_load) 317 | enable_load() 318 | self.processor = QTimer() 319 | self.processor.timeout.connect(self.process) 320 | self.load.clicked.connect(self.processor.start) 321 | self.cancel.clicked.connect(self.stop) 322 | self.setLayout(box) 323 | 324 | def start(self): 325 | self.cancel.setVisible(True) 326 | self.load.setVisible(False) 327 | filename = self.location.text() 328 | self.loader = trace.Loader(file(filename)) 329 | self.progress.setVisible(True) 330 | stat = os.stat(filename) 331 | self.progress.setRange(0, stat.st_size) 332 | machines = self.machines.selected() 333 | if machines is not None: 334 | self.loader.enable_filter('filter-machine', id=machines) 335 | 336 | for name in self.handlers.options: 337 | if self.handlers.options[name].isChecked(): 338 | self.loader.enable_handlers([name]) 339 | 340 | def stop(self): 341 | self.processor.stop() 342 | self.progress.setVisible(False) 343 | self.cancel.setVisible(False) 344 | self.load.setVisible(True) 345 | self.loader = None 346 | self.finished.emit() 347 | 348 | def process(self): 349 | if not self.loader: 350 | self.start() 351 | try: 352 | self.loader.next() 353 | self.progress.setValue(self.loader.parser.lexer.instream.tell()) 354 | except StopIteration: 355 | self.stop() 356 | 357 | 358 | def index_level(idx): 359 | if idx.isValid(): 360 | return 1 + index_level(idx.parent()) 361 | else: 362 | return -1 363 | 364 | 365 | def index_up(idx, level=0): 366 | if level == 0: 367 | return idx 368 | else: 369 | return index_up(idx.parent(), level=level-1) 370 | 371 | 372 | class IncidentIndex(object): 373 | def __init__(self, model, index): 374 | self.model = model 375 | self.index = index 376 | 377 | @property 378 | def incidents(self): 379 | return self.model.incidents 380 | 381 | @property 382 | def level(self): 383 | return index_level(self.index) 384 | 385 | @property 386 | def column(self): 387 | return self.index.column() 388 | 389 | @property 390 | def row(self): 391 | return self.index.row() 392 | 393 | @property 394 | def incident(self): 395 | top = index_up(self.index, self.level) 396 | return self.incidents[top.row()] 397 | 398 | @property 399 | def location(self): 400 | if self.level in (1, 2): 401 | top = self.index 402 | if self.level == 2: 403 | top = index_up(self.index, 1) 404 | location_id = self.incident.locations[top.row()] 405 | if self.model.locations is None: 406 | return None 407 | else: 408 | return self.model.locations.get(location_id) 409 | 410 | @property 411 | def point(self): 412 | if self.level == 2: 413 | return self.location[self.index.row()] 414 | 415 | 416 | class IncidentModel(QAbstractItemModel): 417 | filter_role = Qt.UserRole 418 | sort_role = Qt.UserRole + 1 419 | 420 | handlers = [] 421 | 422 | def __init__(self, incidents, locations, parent=None): 423 | super(IncidentModel, self).__init__(parent) 424 | self.incidents = incidents 425 | self.locations = locations 426 | self.parents = {0: QModelIndex()} 427 | self.child_ids = 0 428 | 429 | def dispatch(self, role, index): 430 | for handler in self.handlers: 431 | def sat(c, v): 432 | if c == 'roles': 433 | return role in v 434 | if c == 'level': 435 | return index.level == v 436 | if c == 'column': 437 | return index.column == v 438 | 439 | for (c, v) in handler['constraints'].items(): 440 | if not sat(c, v): 441 | break 442 | else: 443 | return handler['accept'](index) 444 | 445 | def index(self, row, col, parent): 446 | if parent.isValid(): 447 | self.child_ids += 1 448 | index = self.createIndex(row, col, self.child_ids) 449 | self.parents[self.child_ids] = parent 450 | return index 451 | else: 452 | return self.createIndex(row, col, 0) 453 | 454 | def parent(self, child): 455 | return self.parents[child.internalId()] 456 | 457 | def rowCount(self, parent): 458 | n = self.dispatch('row-count', IncidentIndex(self, parent)) 459 | return 0 if n is None else n 460 | 461 | def columnCount(self, parent): 462 | return 2 if not parent.isValid() or parent.column() == 0 else 0 463 | 464 | def data(self, index, role): 465 | role = { 466 | Qt.DisplayRole: 'display', 467 | self.sort_role: 'sort', 468 | self.filter_role: 'filter' 469 | }.get(role) 470 | 471 | if role: 472 | return QVariant(self.dispatch(role, IncidentIndex(self, index))) 473 | else: 474 | return QVariant() 475 | 476 | 477 | def defmethod(*args, **kwargs): 478 | def register(method): 479 | kwargs['roles'] = args 480 | IncidentModel.handlers.append({ 481 | 'name': method.__name__, 482 | 'constraints': kwargs, 483 | 'accept': method}) 484 | return register 485 | 486 | 487 | @defmethod('display', level=2, column=0) 488 | def display_point(msg): 489 | return '{:x}'.format(msg.point.addr) 490 | 491 | 492 | @defmethod('display', level=2, column=1) 493 | def display_point_machine(msg): 494 | return msg.point.machine 495 | 496 | 497 | @defmethod('display', level=1, column=0) 498 | def display_incident_location(msg): 499 | return 'location-{}'.format(msg.row) 500 | 501 | 502 | @defmethod('display', level=0, column=0) 503 | def display_incident_name(msg): 504 | return msg.incident.name 505 | 506 | 507 | @defmethod('display', level=0, column=1) 508 | def display_incident_id(msg): 509 | return msg.row 510 | 511 | 512 | @defmethod('sort', 'filter', column=0) 513 | def incident_name(msg): 514 | return msg.incident.name 515 | 516 | 517 | @defmethod('row-count', level=-1) 518 | def number_of_incidents(msg): 519 | return len(msg.incidents) 520 | 521 | 522 | @defmethod('row-count', level=0, column=0) 523 | def number_of_locations(msg): 524 | return len(msg.incident.locations) 525 | 526 | 527 | @defmethod('row-count', level=1, column=0) 528 | def backtrace_length(msg): 529 | return 0 if msg.location is None else len(msg.location) 530 | 531 | 532 | class Incident(object): 533 | __slots__ = ['name', 'locations'] 534 | 535 | def __init__(self, name, locations): 536 | self.name = name 537 | self.locations = locations 538 | 539 | def __repr__(self): 540 | return 'Incident({}, {})'.format(repr(self.name), 541 | repr(self.locations)) 542 | 543 | 544 | class Point(object): 545 | __slots__ = ['addr', 'machine'] 546 | 547 | def __init__(self, addr, machine=None): 548 | self.addr = addr 549 | self.machine = machine 550 | 551 | def __str__(self): 552 | if self.machine: 553 | return '{}:{}'.format(self.machine, self.addr) 554 | else: 555 | return str(self.addr) 556 | 557 | def __repr__(self): 558 | if self.machine: 559 | return 'Point({},{})'.format(self.machine, self.addr) 560 | else: 561 | return 'Point({})'.format(repr(self.addr)) 562 | 563 | 564 | def parse_point(data): 565 | parts = data.split(':') 566 | if len(parts) == 1: 567 | return Point(int(data, 16)) 568 | else: 569 | return Point(int(parts[1], 16), int(parts[0])) 570 | 571 | 572 | class BapTraceMain(idaapi.PluginForm): 573 | def OnCreate(self, form): 574 | form = self.FormToPyQtWidget(form) 575 | self.control = TraceLoaderController(form) 576 | self.incidents = IncidentView(form) 577 | 578 | def display(): 579 | self.incidents.display(incidents, locations) 580 | self.control.finished.connect(display) 581 | box = QtWidgets.QHBoxLayout() 582 | split = QtWidgets.QSplitter() 583 | split.addWidget(self.control) 584 | split.addWidget(self.incidents) 585 | box.addWidget(split) 586 | form.setLayout(box) 587 | 588 | 589 | class BapTracePlugin(idaapi.plugin_t): 590 | wanted_name = 'BAP: Load Observations' 591 | wanted_hotkey = '' 592 | flags = idaapi.PLUGIN_FIX 593 | comment = 'Load Primus Observations' 594 | help = """ 595 | Loads Primus Observations into IDA for further analysis 596 | """ 597 | 598 | def __init__(self): 599 | self.form = None 600 | self.name = 'Primus Observations' 601 | 602 | def init(self): 603 | return idaapi.PLUGIN_KEEP 604 | 605 | def term(self): 606 | pass 607 | 608 | def run(self, arg): 609 | if not self.form: 610 | self.form = BapTraceMain() 611 | return self.form.Show(self.name, options=( 612 | self.form.FORM_PERSIST | 613 | self.form.FORM_SAVE)) 614 | 615 | 616 | def PLUGIN_ENTRY(): 617 | return BapTracePlugin() 618 | 619 | 620 | main = None 621 | 622 | 623 | def bap_trace_test(): 624 | global main 625 | main = BapTraceMain() 626 | main.Show('Primus Observations') 627 | -------------------------------------------------------------------------------- /plugins/bap/plugins/bap_view.py: -------------------------------------------------------------------------------- 1 | """BAP View Plugin to read latest BAP execution trace.""" 2 | 3 | from __future__ import print_function 4 | from bap.utils.run import BapIda 5 | import re 6 | 7 | import idaapi # pylint: disable=import-error 8 | 9 | 10 | class BapViews(idaapi.Choose2): 11 | # pylint: disable=invalid-name,missing-docstring,no-self-use 12 | def __init__(self, views): 13 | idaapi.Choose2.__init__(self, 'Choose BAP view', [ 14 | ['PID', 4], 15 | ['Status', 5], 16 | ['Action', 40] 17 | ]) 18 | self.views = views 19 | 20 | def OnClose(self): 21 | pass 22 | 23 | def OnGetLine(self, n): 24 | view = self.views[self.views.keys()[n]] 25 | code = view.instance.proc.returncode 26 | return [ 27 | str(view.instance.proc.pid), 28 | "running" if code is None else str(code), 29 | view.instance.action 30 | ] 31 | 32 | def OnGetSize(self): 33 | return len(self.views) 34 | 35 | 36 | class View(idaapi.simplecustviewer_t): 37 | # pylint: disable=invalid-name,missing-docstring,no-self-use 38 | # pylint: disable=super-on-old-class,no-member 39 | def __init__(self, caption, instance, on_close=None): 40 | super(View, self).__init__() 41 | self.Create(caption) 42 | self.instance = instance 43 | self.on_close = on_close 44 | 45 | def update(self): 46 | self.ClearLines() 47 | with open(self.instance.out.name, 'r') as src: 48 | for line in src.read().split('\n'): 49 | self.AddLine(recolorize(line)) 50 | self.Refresh() # Ensure latest information gets to the screen 51 | 52 | def OnClose(self): 53 | self.ClearLines() 54 | if self.on_close: 55 | self.on_close() 56 | 57 | 58 | class BapView(idaapi.plugin_t): 59 | """ 60 | BAP View Plugin. 61 | 62 | Keybindings: 63 | Ctrl-Shift-F5 : Open/Refresh BAP View 64 | """ 65 | flags = idaapi.PLUGIN_FIX | idaapi.PLUGIN_DRAW 66 | wanted_hotkey = "Ctrl-Shift-F5" 67 | comment = "bap output viewer" 68 | help = "View BAP output" 69 | wanted_name = "BAP: Show output" 70 | 71 | def __init__(self): 72 | self.views = {} 73 | 74 | def create_view(self, bap): 75 | "creates a new view" 76 | pid = bap.proc.pid 77 | name = 'BAP-{0}'.format(pid) 78 | view = View(name, bap, on_close=lambda: self.delete_view(pid)) 79 | view.instance = bap 80 | curr = idaapi.get_current_tform() 81 | self.views[pid] = view 82 | view.Show() # pylint: disable=no-member 83 | idaapi.switchto_tform(curr, True) 84 | 85 | def delete_view(self, pid): 86 | "deletes a view associated with the provided pid" 87 | del self.views[pid] 88 | 89 | def update_view(self, bap): 90 | """updates the view associated with the given bap instance""" 91 | view = self.views.get(bap.proc.pid, None) 92 | if view: 93 | view.update() 94 | 95 | def finished(self, bap): 96 | "final update" 97 | self.update_view(bap) 98 | if bap.proc.pid in self.views: # because a user could close the view 99 | if bap.proc.returncode > 0: 100 | self.views[bap.proc.pid].Show() # pylint: disable=no-member 101 | 102 | def init(self): 103 | """Initialize BAP view to load whenever hotkey is pressed.""" 104 | BapIda.observers['instance_created'].append(self.create_view) 105 | BapIda.observers['instance_updated'].append(self.update_view) 106 | BapIda.observers['instance_finished'].append(self.finished) 107 | BapIda.observers['instance_failed'].append(self.finished) 108 | return idaapi.PLUGIN_KEEP 109 | 110 | def term(self): 111 | """Close BAP View, if it exists.""" 112 | for pid in self.views: 113 | self.views[pid].Close() 114 | 115 | def show_view(self): 116 | "Switch to one of the BAP views" 117 | chooser = BapViews(self.views) 118 | choice = chooser.Show(modal=True) # pylint: disable=no-member 119 | if choice >= 0: 120 | view = self.views[self.views.keys()[choice]] 121 | view.Show() 122 | 123 | def run(self, arg): # pylint: disable=unused-argument 124 | "invokes the plugin" 125 | self.show_view() 126 | 127 | 128 | def recolorize(line): 129 | """fix ansi colors""" 130 | ansi_escape = re.compile(r'\x1b[^m]*m([^\x1b]*)\x1b[^m]*m') 131 | return ansi_escape.sub('\1\x22\\1\2\x22', line) 132 | 133 | 134 | def PLUGIN_ENTRY(): # pylint: disable=invalid-name 135 | """Install BAP_View upon entry.""" 136 | return BapView() 137 | -------------------------------------------------------------------------------- /plugins/bap/plugins/pseudocode_bap_comment.py: -------------------------------------------------------------------------------- 1 | """Hex-Rays Plugin to propagate comments to Pseudocode View.""" 2 | 3 | import idc 4 | import idaapi 5 | 6 | from bap.utils import hexrays 7 | from bap.utils import bap_comment 8 | 9 | 10 | COLOR_START = '\x01\x0c // \x01\x0c' 11 | COLOR_END = '\x02\x0c\x02\x0c' 12 | 13 | 14 | def union(lhs, rhs): 15 | for (key, rvalues) in rhs.items(): 16 | lvalues = lhs.setdefault(key, []) 17 | for value in rvalues: 18 | if value not in lvalues: 19 | lvalues.append(value) 20 | 21 | 22 | class PseudocodeBapComment(hexrays.PseudocodeVisitor): 23 | """Propagate comments from Text/Graph view to Pseudocode view.""" 24 | flags = idaapi.PLUGIN_HIDE 25 | comment = "" 26 | help = "Propagate BAP comments to pseudocode view" 27 | wanted_name = "BAP: " 28 | 29 | def visit_line(self, line): 30 | comm = {} 31 | for address in line.extract_addresses(): 32 | idacomm = idc.Comment(address) 33 | newcomm = idacomm and bap_comment.parse(idacomm) or {} 34 | union(comm, newcomm) 35 | if comm: 36 | line.widget.line += COLOR_START 37 | line.widget.line += bap_comment.dumps(comm) 38 | line.widget.line += COLOR_END 39 | 40 | 41 | def PLUGIN_ENTRY(): 42 | """Install Pseudocode_BAP_Comment upon entry.""" 43 | return PseudocodeBapComment() 44 | -------------------------------------------------------------------------------- /plugins/bap/plugins/pseudocode_bap_taint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hex-Rays Plugin to propagate taint information to Pseudocode View. 3 | 4 | Requires BAP_Taint plugin, and installs callbacks into it. 5 | """ 6 | 7 | import idc 8 | import idaapi 9 | 10 | from bap.utils import hexrays 11 | from bap.utils.bap_taint import BapTaint 12 | 13 | colors = { 14 | 'black': 0x000000, 15 | 'red': 0xCCCCFF, 16 | 'green': 0x99FF99, 17 | 'yellow': 0xC2FFFF, 18 | 'blue': 0xFFB2B2, 19 | 'magenta': 0xFFB2FF, 20 | 'cyan': 0xFFFFB2, 21 | 'white': 0xFFFFFF, 22 | 'gray': 0xEAEAEA, 23 | } 24 | 25 | 26 | def next_color(current_color, ea): 27 | coloring_order = [ 28 | colors[c] for c in [ 29 | 'gray', 30 | 'white', 31 | 'red', 32 | 'yellow', 33 | ] 34 | ] 35 | BGR_MASK = 0xffffff 36 | ea_color = idaapi.get_item_color(ea) 37 | if ea_color & BGR_MASK not in coloring_order: 38 | return current_color 39 | assert(current_color & BGR_MASK in coloring_order) 40 | ea_idx = coloring_order.index(ea_color & BGR_MASK) 41 | current_idx = coloring_order.index(current_color & BGR_MASK) 42 | if ea_idx >= current_idx: 43 | return ea_color 44 | else: 45 | return current_color 46 | 47 | 48 | class PseudocodeBapTaint(hexrays.PseudocodeVisitor): 49 | """Propagate taint information from Text/Graph view to Pseudocode view.""" 50 | 51 | flags = idaapi.PLUGIN_HIDE 52 | comment = "BAP Taint Plugin for Pseudocode View" 53 | help = "BAP Taint Plugin for Pseudocode View" 54 | wanted_name = "BAP Taint Pseudocode" 55 | 56 | def visit_line(self, line): 57 | line.widget.bgcolor = colors['gray'] 58 | for addr in line.extract_addresses(): 59 | line.widget.bgcolor = next_color(line.widget.bgcolor, addr) 60 | 61 | 62 | def PLUGIN_ENTRY(): 63 | """Install Pseudocode_BAP_Taint upon entry.""" 64 | return PseudocodeBapTaint() 65 | -------------------------------------------------------------------------------- /plugins/bap/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Commonly used utilities.""" 2 | 3 | __all__ = ('sexpr', 'bap_comment', 'run', 'ida', 'abstract_ida_plugins', 4 | 'config', 'bap_taint') 5 | -------------------------------------------------------------------------------- /plugins/bap/utils/_comment_handler.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CommentHandlers(object): 4 | def __init__(self): 5 | self.handlers = [] 6 | self.comments = {} 7 | 8 | def handler(self): 9 | def wrapped(func): 10 | self.register(func) 11 | return func 12 | return wrapped 13 | 14 | def register_handler(self, func): 15 | for handler in self.handlers: 16 | print(handler.__name__) 17 | self.handlers.append(func) 18 | 19 | def add(self, addr, key, value): 20 | if (addr, key) in self.comments: 21 | self.comments[(addr, key)].append(value) 22 | else: 23 | self.comments[(addr, key)] = [value] 24 | for handler in self.handlers: 25 | handler(addr, key, value) 26 | -------------------------------------------------------------------------------- /plugins/bap/utils/_ctyperewriter.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | _REWRITERS = ( 4 | (r'(struct|enum|union) ([^{} ]*);', r'\1 \2; typedef \1 \2 \2;'), 5 | (r'unsigned __int(8|16|32|64)', r'uint\1_t'), 6 | (r'(signed )?__int(8|16|32|64)', r'int\2_t'), 7 | (r'__(cdecl|noreturn)', r'__attribute__((\1))'), 8 | ('r^%', r'__'), 9 | (r'_QWORD', r'int64_t'), 10 | (r'_DWORD', r'int32_t'), 11 | (r'_WORD', r'int16_t'), 12 | (r'_BYTE', r'int8_t'), 13 | ) 14 | 15 | 16 | class Rewriter(object): 17 | def __init__(self): 18 | self.rewriters = [] 19 | for (patt, subst) in _REWRITERS: 20 | self.rewriters.append((re.compile(patt), subst)) 21 | 22 | def translate(self, expr): 23 | for (regex, subst) in self.rewriters: 24 | expr = regex.subs(subst, expr) 25 | return expr 26 | -------------------------------------------------------------------------------- /plugins/bap/utils/_service.py: -------------------------------------------------------------------------------- 1 | import string 2 | import idc 3 | 4 | 5 | class Service(object): 6 | def __init__(self): 7 | self.services = {} 8 | 9 | def provider(self, name): 10 | def wrapped(func): 11 | self.register(name, func) 12 | return func 13 | return wrapped 14 | 15 | def register(self, name, func): 16 | if name in self.services: 17 | raise ServiceAlreadyRegistered(name) 18 | if not is_valid_service_name(name): 19 | raise ServiceNameIsNotValid(name) 20 | self.services[name] = func 21 | 22 | def request(self, service, output): 23 | if service not in self.services: 24 | raise ServiceIsNotRegistered(service) 25 | 26 | idc.Wait() 27 | with open(output, 'w') as out: 28 | self.services[service](out) 29 | idc.Exit(0) 30 | 31 | 32 | class ServiceError(Exception): 33 | pass 34 | 35 | 36 | class ServiceRegistrationError(ServiceError): 37 | pass 38 | 39 | 40 | class ServiceAlreadyRegistered(ServiceRegistrationError): 41 | def __init__(self, name): 42 | self.name = name 43 | 44 | 45 | class ServiceNameIsNotValid(ServiceRegistrationError): 46 | def __init__(self, name): 47 | self.name = name 48 | 49 | 50 | class ServiceIsNotRegistered(ServiceError): 51 | def __init__(self, name): 52 | self.name = name 53 | 54 | 55 | def is_valid_service_name(name): 56 | valid_syms = string.ascii_letters + '-_' + string.digits 57 | return set(name).issubset(valid_syms) 58 | -------------------------------------------------------------------------------- /plugins/bap/utils/abstract_ida_plugins.py: -------------------------------------------------------------------------------- 1 | """Defines a few abstract plugin classes that can be subclassed for usage.""" 2 | 3 | import idaapi 4 | 5 | 6 | class DoNothing(idaapi.plugin_t): 7 | """ 8 | Do Nothing. 9 | 10 | This plugin does absolutely nothing. It is created for the sole purpose of 11 | being able to keep multiple non-plugin Python files which may then be used 12 | as utilities by other plugins. 13 | 14 | Usage: 15 | class DoNothing(DoNothing): 16 | pass 17 | 18 | def PLUGIN_ENTRY(): 19 | return DoNothing() 20 | """ 21 | 22 | flags = idaapi.PLUGIN_HIDE 23 | comment = "Does Nothing" 24 | help = "Does Nothing" 25 | wanted_name = "Do Nothing" 26 | wanted_hotkey = "" 27 | 28 | def init(self): 29 | """Skip plugin.""" 30 | return idaapi.PLUGIN_SKIP 31 | 32 | def term(self): 33 | """Do nothing.""" 34 | pass 35 | 36 | def run(self, arg): 37 | """Do nothing.""" 38 | pass 39 | -------------------------------------------------------------------------------- /plugins/bap/utils/bap_comment.py: -------------------------------------------------------------------------------- 1 | """BAP Comment. 2 | 3 | We use comments to annotate code in IDA with the semantic information 4 | extracted from BAP analyses. The comments are machine-readable, and a 5 | simple syntax is used, to make the parser robust and comments human 6 | readable. We will define the syntax formally later, but we will start 7 | with an example: 8 | 9 | BAP: saluki-sat,saluki-unsat, beagle-strings="hello, world",nice 10 | 11 | Basically, the comment string includes an arbitrary amount of 12 | key=value pairs. If a value contains whitespaces, punctuation or any 13 | non-word character, then it should be delimited with double quotes. If 14 | a value contains a quote character, then it should be escaped with the 15 | backslash character (the backslash character can escape 16 | itself). Properties that doesn't have values (or basically has a 17 | property of a unit type, so called boolean properties) are represented 18 | with their names only, e.g., ``saluki-sat``. A property can have 19 | multiple values, separated by a comma. Properties wihtout values, can 20 | be also separated with the comma. In fact you can always trade off 21 | space for comma, if you like, e.g., ``saluki-sat,saluki-unsat`` is 22 | equivalent to ``saluki-sat saluki-unsat``: 23 | 24 | >>> assert(parse('BAP: saluki-sat,saluki-unsat') == \ 25 | parse('BAP: saluki-sat saluki-unsat')) 26 | 27 | 28 | Comments are parsed into a dictionary, that maps properties into their 29 | values. A property that doesn't have a value is mapped to an empty 30 | list. 31 | 32 | >>> parse('BAP: saluki-sat,saluki-unsat beagle-chars=ajdladasn,asd \ 33 | beagle-strings="{hello world}"') 34 | {'saluki-sat': [], 'beagle-chars': ['ajdladasn', 'asd'], 35 | 'saluki-unsat': [], 'beagle-strings': ['{hello world}']} 36 | 37 | They can be modifed, and dumped back into a string: 38 | 39 | >>> dumps({'saluki-sat': [], 'beagle-chars': ['ajdladasn', 'asd'], 40 | 'saluki-unsat': [], 'beagle-strings': ['{hello world}']}) 41 | 'BAP: saluki-sat,saluki-unsat beagle-chars=ajdladasn,asd \ 42 | beagle-strings="{hello world}"' 43 | 44 | 45 | Any special characters inside the property value must be properly 46 | escaped: 47 | 48 | >>> parse('BAP: beagle-chars="abc\\'"') 49 | {'beagle-chars': ["abc'"]} 50 | 51 | Note: In the examples, we need to escape the backslash, as they are 52 | intended to be run by the doctest system, that will perform one layer 53 | of the expension. So, in the real life, to escape a quote we will 54 | write only one backslash, e.g., "abc\'". Probably, this should be 55 | considered as a bug on the doctest side, as it is assumed, that you 56 | can copy paste an example from the doc to the interpreter and see the 57 | identical results. Here we will get a syntax error from the python 58 | interpreter. 59 | 60 | >>> dumps(parse('BAP: beagle-chars="abc\\'"')) 61 | 'BAP: beagle-chars="abc\\'"' 62 | 63 | Syntactically incorrect code will raise the ``SyntaxError`` exception, 64 | e.g., 65 | 66 | >>> parse('BAP: beagle-words=hello=world') 67 | Traceback (most recent call last): 68 | ... 69 | SyntaxError: in state key expected got = 70 | 71 | ## Grammar 72 | 73 | comm ::= "BAP:" 74 | props ::= 75 | | 76 | prop ::= 77 | | = 78 | values ::= | "," 79 | value ::= 80 | key ::= 81 | 82 | 83 | Where ```` is any sequence of word-characters (see WORDCHARS) 84 | constant (letters, numbers and the following two characters: "-" and 85 | ":"), e.g., `my-property-name`, or `analysis:property`. 86 | 87 | 88 | Note: the parser usually accepts more languages that are formally recognized 89 | by the grammar. 90 | 91 | """ 92 | 93 | import string 94 | from shlex import shlex 95 | 96 | WORDCHARS = ''.join(['-:', string.ascii_letters, string.digits]) 97 | 98 | 99 | def parse(comment, debug=0): 100 | """ Parse comment string. 101 | 102 | Returns a dictionary that maps properties to their values. 103 | Raises SyntaxError if the comment is syntactically incorrect. 104 | Returns None if comment doesn't start with the `BAP:` prefix. 105 | """ 106 | lexer = shlex(comment, posix=True) 107 | lexer.wordchars = WORDCHARS 108 | lexer.debug = debug 109 | lexer.quotes = '"' 110 | result = {} 111 | key = '' 112 | values = [] 113 | state = 'init' 114 | 115 | def error(exp, token): 116 | "raise a nice error message" 117 | raise SyntaxError('in state {0} expected {1} got {2}'. 118 | format(state, exp, token)) 119 | 120 | def push(result, key, values): 121 | "push binding into the stack" 122 | if key != '': 123 | result[key] = values 124 | 125 | for token in lexer: 126 | if state == 'init': 127 | if token != 'BAP:': 128 | return None 129 | state = 'key' 130 | elif state == 'key': 131 | if token == '=': 132 | error('', token) 133 | elif token == ',': 134 | state = 'value' 135 | else: 136 | push(result, key, values) 137 | values = [] 138 | key = token 139 | state = 'eq' 140 | elif state == 'eq': 141 | if token == '=': 142 | state = 'value' 143 | else: 144 | push(result, key, values) 145 | key = '' 146 | values = [] 147 | if token == ',': 148 | state = 'key' 149 | else: 150 | key = token 151 | state = 'eq' 152 | elif state == 'value': 153 | values.append(unquote(token)) 154 | state = 'key' 155 | 156 | push(result, key, values) 157 | return result 158 | 159 | 160 | def is_valid(comm): 161 | try: 162 | return comm.startswith('BAP:') and parse(comm) 163 | except SyntaxError: 164 | return False 165 | 166 | 167 | def dumps(comm): 168 | """Dump dictionary into a comment string. 169 | 170 | The representation is parseable with the parse function. 171 | """ 172 | keys = [] 173 | elts = [] 174 | for (key, values) in comm.items(): 175 | if values: 176 | elts.append('{0}={1}'.format(key, ','.join( 177 | quote(x) for x in values))) 178 | else: 179 | keys.append(key) 180 | keys.sort() 181 | elts.sort() 182 | return ' '.join(x for x in 183 | ('BAP:', ','.join(keys), ' '.join(elts)) if x) 184 | 185 | 186 | def quote(token): 187 | """delimit a token with quotes if needed. 188 | 189 | The function guarantees that the string representation of the 190 | token will be parsed into the same token. In case if a token 191 | contains characters that are no in the set of WORDCHARS symbols, 192 | that will lead to the splittage of the token during the lexing, 193 | a pair of double quotes are added to prevent this. 194 | 195 | >>> quote('hello, world') 196 | '"hello, world"' 197 | """ 198 | if not token.startswith('"') and set(token) - set(WORDCHARS): 199 | return '"{}"'.format(''.join('\\'+c if c == '"' else c 200 | for c in token)) 201 | else: 202 | return token 203 | 204 | 205 | def unquote(word, quotes='\'"'): 206 | """removes quotes from both sides of the word. 207 | 208 | The quotes should occur on both sides of the word: 209 | 210 | >>> unquote('"hello"') 211 | 'hello' 212 | 213 | If a quote occurs only on one side of the word, then 214 | the word is left intact: 215 | 216 | >>> unquote('"hello') 217 | '"hello' 218 | 219 | The quotes that delimites the world should be equal, i.e., 220 | if the word is delimited with double quotes on the left and 221 | a quote on the right, then it is not considered as delimited, 222 | so it is not dequoted: 223 | 224 | >>> unquote('"hello\\'') 225 | '"hello\\'' 226 | 227 | Finally, only one layer of quotes is removed, 228 | 229 | >>> unquote('""hello""') 230 | '"hello"' 231 | """ 232 | if len(word) > 1 and word[0] == word[-1] \ 233 | and word[0] in quotes and word[-1] in quotes: 234 | return word[1:-1] 235 | else: 236 | return word 237 | 238 | 239 | if __name__ == "__main__": 240 | import doctest 241 | doctest.testmod() 242 | -------------------------------------------------------------------------------- /plugins/bap/utils/bap_taint.py: -------------------------------------------------------------------------------- 1 | """ 2 | IDA Python Plugin to use BAP to propagate taint information. 3 | 4 | Allows user to select any arbitrary line in Graph/Text view in IDA, 5 | and be able to taint that line and propagate taint information. 6 | 7 | Keybindings: 8 | Shift-A : Equivalent to `--taint-reg` in BAP 9 | Ctrl-Shift-A : Equivalent to `--taint-ptr` in BAP 10 | 11 | Color Scheme: 12 | "Nasty" Yellow : Taint source 13 | "Affected" Red : Lines that got tainted 14 | "Ignored" Gray : Lines that were not visited by propagate-taint 15 | "Normal" White : Lines that were visited, but didn't get tainted 16 | """ 17 | import idautils 18 | import idaapi 19 | import idc 20 | from bap.utils.run import BapIda 21 | 22 | patterns = [ 23 | ('true', 'gray'), 24 | ('is-visited', 'white'), 25 | ('has-taints', 'red'), 26 | ('taints', 'yellow') 27 | ] 28 | 29 | ENGINE_HISTORY=117342 30 | 31 | ask_engine='What engine would you like, primus or legacy?' 32 | ask_depth='For how many RTL instructions to propagate?' 33 | 34 | class PropagateTaint(BapIda): 35 | ENGINE='primus' 36 | DEPTH=4096 37 | LOOP_DEPTH=64 38 | 39 | "Propagate taint information using BAP" 40 | def __init__(self, addr, kind): 41 | super(PropagateTaint,self).__init__() 42 | # If a user is not fast enough in providing the answer 43 | # IDA Python will popup a modal window that will block 44 | # a user from providing the answer. 45 | idaapi.disable_script_timeout() 46 | 47 | engine = idaapi.askstr(ENGINE_HISTORY, self.ENGINE, ask_engine) \ 48 | or self.ENGINE 49 | depth = idaapi.asklong(self.DEPTH, ask_depth) \ 50 | or self.DEPTH 51 | 52 | # don't ask for the loop depth as a user is already annoyed. 53 | loop_depth = self.LOOP_DEPTH 54 | 55 | self.action = 'propagating taint from {:s}0x{:X}'.format( 56 | '*' if kind == 'ptr' else '', 57 | addr) 58 | propagate = 'run' if engine == 'primus' else 'propagate-taint' 59 | self.passes = ['taint', propagate, 'map-terms','emit-ida-script'] 60 | self.script = self.tmpfile('py') 61 | scheme = self.tmpfile('scm') 62 | stdin=self.tmpfile('stdin') 63 | stdout=self.tmpfile('stdout') 64 | for (pat,color) in patterns: 65 | scheme.write('(({0}) (color {1}))\n'.format(pat,color)) 66 | scheme.close() 67 | name = idc.GetFunctionName(addr) 68 | 69 | self.args += [ 70 | '--taint-'+kind, '0x{:X}'.format(addr), 71 | '--passes', ','.join(self.passes), 72 | '--map-terms-using', scheme.name, 73 | '--emit-ida-script-attr', 'color', 74 | '--emit-ida-script-file', self.script.name 75 | ] 76 | 77 | if engine == 'primus': 78 | self.args += [ 79 | '--run-entry-points={}'.format(name), 80 | '--primus-limit-max-length={}'.format(depth), 81 | '--primus-limit-max-visited={}'.format(loop_depth), 82 | '--primus-promiscuous-mode', 83 | '--primus-greedy-scheduler', 84 | '--primus-propagate-taint-from-attributes', 85 | '--primus-propagate-taint-to-attributes', 86 | '--primus-lisp-channel-redirect=:{0},:{1}'.format( 87 | stdin.name, 88 | stdout.name) 89 | ] 90 | 91 | 92 | 93 | class BapTaint(idaapi.plugin_t): 94 | flags = idaapi.PLUGIN_FIX 95 | comment = "BAP Taint Plugin" 96 | wanted_name = "BAP: Taint" 97 | 98 | 99 | help = "" 100 | """ 101 | Plugin to use BAP to propagate taint information. 102 | 103 | Also supports installation of callbacks using install_callback() 104 | """ 105 | 106 | _callbacks = { 107 | 'ptr': [], 108 | 'reg': [] 109 | } 110 | 111 | @classmethod 112 | def _do_callbacks(cls, ptr_or_reg): 113 | data = { 114 | 'ea': idc.ScreenEA(), 115 | 'ptr_or_reg': ptr_or_reg 116 | } 117 | for callback in cls._callbacks[ptr_or_reg]: 118 | callback(data) 119 | 120 | def start(self): 121 | tainter = PropagateTaint(idc.ScreenEA(), self.kind) 122 | tainter.on_finish(lambda bap: self.finish(bap)) 123 | tainter.run() 124 | 125 | def finish(self, bap): 126 | idaapi.IDAPython_ExecScript(bap.script.name, globals()) 127 | idaapi.refresh_idaview_anyway() 128 | BapTaint._do_callbacks(self.kind) 129 | idc.Refresh() 130 | 131 | def __init__(self, kind): 132 | assert(kind in ('ptr', 'reg')) 133 | self.kind = kind 134 | self.wanted_name += 'pointer' if kind == 'ptr' else 'value' 135 | 136 | def init(self): 137 | """Initialize Plugin.""" 138 | return idaapi.PLUGIN_KEEP 139 | 140 | def term(self): 141 | """Terminate Plugin.""" 142 | pass 143 | 144 | def run(self, arg): 145 | """ 146 | Run Plugin. 147 | """ 148 | self.start() 149 | 150 | @classmethod 151 | def install_callback(cls, callback_fn, ptr_or_reg=None): 152 | """ 153 | Install callback to be run when the user calls for taint propagation. 154 | 155 | Callback must take a dict and must return nothing. 156 | 157 | Dict is guaranteed to get the following keys: 158 | 'ea': The value of EA at point where user propagated taint from. 159 | 'ptr_or_reg': Either 'ptr' or 'reg' depending on user selection. 160 | """ 161 | if ptr_or_reg is None: 162 | cls.install_callback(callback_fn, 'ptr') 163 | cls.install_callback(callback_fn, 'reg') 164 | elif ptr_or_reg == 'ptr' or ptr_or_reg == 'reg': 165 | cls._callbacks[ptr_or_reg].append(callback_fn) 166 | else: 167 | idc.Fatal("Invalid ptr_or_reg value passed {}". 168 | format(repr(ptr_or_reg))) 169 | -------------------------------------------------------------------------------- /plugins/bap/utils/config.py: -------------------------------------------------------------------------------- 1 | """Module for reading from and writing to the bap.cfg config file.""" 2 | 3 | import os 4 | import idaapi # pylint: disable=import-error 5 | 6 | CFG_DIR = idaapi.idadir('cfg') 7 | CFG_PATH = os.path.join(CFG_DIR, 'bap.cfg') 8 | 9 | 10 | def _read(): 11 | "parse the config file" 12 | if not os.path.exists(CFG_PATH): 13 | return {} 14 | cfg = {'default': []} 15 | with open(CFG_PATH, 'r') as src: 16 | current_section = 'default' 17 | for line in src.read().split('\n'): 18 | if len(line) == 0: # Empty line 19 | continue 20 | elif line[0] == '.': # Section 21 | current_section = line[1:] 22 | if current_section not in cfg: 23 | cfg[current_section] = [] 24 | else: 25 | cfg[current_section].append(line) 26 | return cfg 27 | 28 | 29 | def _write(cfg): 30 | "dump config into the file" 31 | new_config = [] 32 | for section in cfg: 33 | new_config.append('.' + section) 34 | for line in cfg[section]: 35 | new_config.append(line) 36 | new_config.append('') 37 | if not os.path.exists(CFG_DIR): 38 | os.makedirs(CFG_DIR) 39 | with open(CFG_PATH, 'w') as out: 40 | out.write('\n'.join(new_config)) 41 | 42 | 43 | def get(path, default=None): 44 | """Get value from key:value in the config file.""" 45 | key = Key(path) 46 | cfg = _read() 47 | if key.section not in cfg: 48 | return default 49 | for line in cfg[key.section]: 50 | if line[0] == ';': # Comment 51 | continue 52 | elif line.split()[0] == key.value: 53 | return line.split()[1] 54 | return default 55 | 56 | 57 | def is_set(key): 58 | """returns True if the value is set, 59 | i.e., if it is `1`, `true` or `yes`. 60 | returns False, if key is not present in the dictionary, 61 | or has any other value. 62 | """ 63 | return get(key, default='0').lower() in ('1', 'true', 'yes') 64 | 65 | 66 | def set(path, value): # pylint: disable=redefined-builtin 67 | """Set key:value in the config file.""" 68 | cfg = _read() 69 | key = Key(path) 70 | 71 | if key.section not in cfg: 72 | cfg[key.section] = [] 73 | for i, line in enumerate(cfg[key.section]): 74 | if line[0] == ';': # Comment 75 | continue 76 | elif line.split()[0] == key.value: 77 | cfg[key.section][i] = '{}\t{}\t; Previously: {}'.format( 78 | key.value, value, line) 79 | break 80 | else: # Key not previously set 81 | cfg[key.section].append('{}\t{}'.format(key.value, value)) 82 | 83 | _write(cfg) 84 | 85 | 86 | class Key(object): # pylint: disable=too-few-public-methods 87 | "Configuration key" 88 | def __init__(self, path): 89 | elts = path.split('.') 90 | if len(elts) > 2: 91 | raise InvalidKey(path) 92 | simple = len(elts) == 1 93 | self.section = 'default' if simple else elts[0] 94 | self.value = elts[0] if simple else elts[1] 95 | 96 | 97 | class InvalidKey(Exception): 98 | "Raised when the key is badly formated" 99 | def __init__(self, path): 100 | super(InvalidKey, self).__init__() 101 | self.path = path 102 | 103 | def __str__(self): 104 | return 'Invalid key syntax. \ 105 | Expected `` or `
.`, got {0}'.format( 106 | self.path) 107 | -------------------------------------------------------------------------------- /plugins/bap/utils/hexrays.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from copy import copy 4 | 5 | import idaapi 6 | import idc 7 | 8 | 9 | def tag_addrcode(s): 10 | return (s[0] == idaapi.COLOR_ON and 11 | s[1] == chr(idaapi.COLOR_ADDR)) 12 | 13 | 14 | class PseudocodeLineWidget(object): 15 | 16 | def __init__(self, parent, widget): 17 | self.parent = parent 18 | self.widget = widget 19 | 20 | def extract_addresses(self): 21 | '''A set of addresses associated with the line''' 22 | anchor = idaapi.ctree_anchor_t() 23 | line = copy(self.widget.line) 24 | addresses = set() 25 | 26 | while len(line) > 0: 27 | skipcode_index = idaapi.tag_skipcode(line) 28 | if skipcode_index == 0: # No code found 29 | line = line[1:] # Skip one character ahead 30 | else: 31 | if tag_addrcode(line): 32 | addr_tag = int(line[2:skipcode_index], 16) 33 | anchor.value = addr_tag 34 | if anchor.is_citem_anchor() \ 35 | and not anchor.is_blkcmt_anchor(): 36 | address = self.parent.treeitems.at(addr_tag).ea 37 | if address != idaapi.BADADDR: 38 | addresses.add(address) 39 | line = line[skipcode_index:] # Skip the colorcodes 40 | return addresses 41 | 42 | 43 | class PseudocodeVisitor(idaapi.plugin_t): 44 | """ 45 | Abstract Base Plugin Class to modify simplelines in Pseudocode View. 46 | 47 | Methods that might be useful while implementing above methods: 48 | - get_ea_list(self, cfunc, sl) 49 | 50 | Note: You will need to add a PLUGIN_ENTRY() function, to your plugin code, 51 | that returns an object of your plugin, which uses this Class as a super 52 | class. 53 | """ 54 | 55 | flags = idaapi.PLUGIN_FIX | idaapi.PLUGIN_PROC 56 | wanted_hotkey = "" 57 | 58 | def visit_line(self, line): 59 | pass 60 | 61 | def visit_func(self, func): 62 | """Run the plugin over the given cfunc.""" 63 | for line in func.get_pseudocode(): 64 | self.visit_line(PseudocodeLineWidget(func, line)) 65 | 66 | def init(self): 67 | """ 68 | Ensure plugin's line modification function is called whenever needed. 69 | 70 | If Hex-Rays is not installed, or is not initialized yet, then plugin 71 | will not load. To ensure that the plugin loads after Hex-Rays, please 72 | name your plugin's .py file with a name that starts lexicographically 73 | after "hexx86f" 74 | """ 75 | try: 76 | if idaapi.init_hexrays_plugin(): 77 | def hexrays_event_callback(event, *args): 78 | if event == idaapi.hxe_refresh_pseudocode: 79 | # We use this event instead of hxe_text_ready because 80 | # MacOSX doesn't seem to work well with it 81 | # TODO: Look into this 82 | vu, = args 83 | self.visit_func(vu.cfunc) 84 | return 0 85 | idaapi.install_hexrays_callback(hexrays_event_callback) 86 | else: 87 | return idaapi.PLUGIN_SKIP 88 | except AttributeError: 89 | idc.Warning('''init_hexrays_plugin() not found. 90 | Skipping Hex-Rays plugin.''') 91 | return idaapi.PLUGIN_KEEP 92 | 93 | def term(self): 94 | pass 95 | 96 | def run(self, arg): 97 | pass 98 | 99 | 100 | def find_cfunc(ea): 101 | """Get cfuncptr_t from EA.""" 102 | func = idaapi.get_func(ea) 103 | if func: 104 | return idaapi.decompile(func) 105 | -------------------------------------------------------------------------------- /plugins/bap/utils/ida.py: -------------------------------------------------------------------------------- 1 | """Utilities that interact with IDA.""" 2 | import idaapi 3 | import idc 4 | import idautils 5 | 6 | from ._service import Service 7 | from ._comment_handler import CommentHandlers 8 | from ._ctyperewriter import Rewriter 9 | 10 | try: 11 | from idc import get_segm_name 12 | except ImportError: 13 | from idaapi import get_segm_name 14 | 15 | service = Service() 16 | comment = CommentHandlers() 17 | rewriter = Rewriter() 18 | 19 | 20 | def addresses(): 21 | """Generate all mapped addresses.""" 22 | for s in idautils.Segments(): 23 | ea = idc.SegStart(s) 24 | while ea < idc.SegEnd(s): 25 | yield ea 26 | ea = idaapi.nextaddr(ea) 27 | 28 | 29 | @service.provider('loader') 30 | def output_segments(out): 31 | """Dump binary segmentation.""" 32 | info = idaapi.get_inf_structure() 33 | size = "r32" if info.is_32bit else "r64" 34 | out.writelines(('(', info.get_proc_name()[1], ' ', size, ' (')) 35 | for seg in idautils.Segments(): 36 | out.write("\n({} {} {:d} ({:#x} {:d}))".format( 37 | get_segm_name(seg), 38 | "code" if idaapi.segtype(seg) == idaapi.SEG_CODE else "data", 39 | idaapi.get_fileregion_offset(seg), 40 | seg, idaapi.getseg(seg).size())) 41 | out.write("))\n") 42 | 43 | 44 | @service.provider('symbols') 45 | def output_symbols(out): 46 | """Dump symbols.""" 47 | try: 48 | from idaapi import get_func_name2 as get_func_name 49 | # Since get_func_name is deprecated (at least from IDA 6.9) 50 | except ImportError: 51 | from idaapi import get_func_name 52 | # Older versions of IDA don't have get_func_name2 53 | # so we just use the older name get_func_name 54 | 55 | def func_name_propagate_thunk(ea): 56 | current_name = get_func_name(ea) 57 | if current_name[0].isalpha(): 58 | return current_name 59 | func = idaapi.get_func(ea) 60 | temp_ptr = idaapi.ea_pointer() 61 | ea_new = idaapi.BADADDR 62 | if func.flags & idaapi.FUNC_THUNK == idaapi.FUNC_THUNK: 63 | ea_new = idaapi.calc_thunk_func_target(func, temp_ptr.cast()) 64 | if ea_new != idaapi.BADADDR: 65 | ea = ea_new 66 | propagated_name = get_func_name(ea) or '' # Ensure it is not `None` 67 | if len(current_name) > len(propagated_name) > 0: 68 | return propagated_name 69 | else: 70 | return current_name 71 | # Fallback to non-propagated name for weird times that IDA gives 72 | # a 0 length name, or finds a longer import name 73 | 74 | for ea in idautils.Segments(): 75 | fs = idautils.Functions(idc.SegStart(ea), idc.SegEnd(ea)) 76 | for f in fs: 77 | out.write('("%s" 0x%x 0x%x)\n' % ( 78 | func_name_propagate_thunk(f), 79 | idc.GetFunctionAttr(f, idc.FUNCATTR_START), 80 | idc.GetFunctionAttr(f, idc.FUNCATTR_END))) 81 | 82 | 83 | @service.provider('types') 84 | def output_types(out): 85 | """Dump type information.""" 86 | for line in local_types() + prototypes(): 87 | out.write(rewriter.translate(line) + '\n') 88 | 89 | 90 | @service.provider('brancher') 91 | def output_branches(out): 92 | """Dump static successors for each instruction """ 93 | out.write('(') 94 | for addr in addresses(): 95 | succs = Succs(addr) 96 | if succs.jmps or (succs.fall is not None): 97 | out.write('{}\n'.format(succs.dumps())) 98 | out.write(')') 99 | 100 | def set_color(addr, color): 101 | idc.SetColor(addr, idc.CIC_ITEM, color) 102 | 103 | 104 | class Printer(idaapi.text_sink_t): 105 | def __init__(self): 106 | try: 107 | idaapi.text_sink_t.__init__(self) 108 | except AttributeError: 109 | pass # Older IDA versions keep the text_sink_t abstract 110 | self.lines = [] 111 | 112 | def _print(self, thing): 113 | self.lines.append(thing) 114 | return 0 115 | 116 | 117 | def local_types(): 118 | printer = Printer() 119 | idaapi.print_decls(printer, idaapi.cvar.idati, [], 120 | idaapi.PDF_INCL_DEPS | idaapi.PDF_DEF_FWD) 121 | return printer.lines 122 | 123 | 124 | def prototypes(): 125 | types = set() 126 | for ea in idautils.Functions(): 127 | proto = idaapi.print_type(ea, True) 128 | if proto: 129 | types.append(proto + ';') 130 | return list(types) 131 | 132 | 133 | class Succs(object): 134 | def __init__(self, addr): 135 | self.addr = addr 136 | self.dests = set(idautils.CodeRefsFrom(addr, True)) 137 | self.jmps = set(idautils.CodeRefsFrom(addr, False)) 138 | falls = self.dests - self.jmps 139 | self.fall = list(falls)[0] if falls else None 140 | 141 | def dumps(self): 142 | return ''.join([ 143 | '({:#x} '.format(self.addr), 144 | ' ({:#x}) '.format(self.fall) if self.fall else '()', 145 | '{})'.format(sexps(self.jmps)) 146 | ]) 147 | 148 | 149 | def sexps(addrs): 150 | sexp = ['('] 151 | for addr in addrs: 152 | sexp.append('{:#x}'.format(addr)) 153 | sexp.append(')') 154 | return ' '.join(sexp) 155 | -------------------------------------------------------------------------------- /plugins/bap/utils/run.py: -------------------------------------------------------------------------------- 1 | """Utilities that interact with BAP.""" 2 | 3 | from __future__ import print_function 4 | 5 | import tempfile 6 | import subprocess 7 | import os 8 | import sys 9 | 10 | import traceback 11 | 12 | import idc # pylint: disable=import-error 13 | import idaapi # pylint: disable=import-error 14 | from bap.utils import ida, config 15 | 16 | 17 | # pylint: disable=missing-docstring 18 | 19 | class Bap(object): 20 | """Bap instance base class. 21 | 22 | Instantiate a subprocess with BAP. 23 | 24 | We will try to keep it clean from IDA 25 | specifics, so that later we can lift it to the bap-python library 26 | 27 | Attributes: 28 | 29 | DEBUG print executed commands and keep temporary files 30 | args default arguments, inserted after `bap ` 31 | plugins a list of available plugins 32 | """ 33 | 34 | DEBUG = False 35 | 36 | args = [] 37 | 38 | plugins = [] 39 | 40 | def __init__(self, bap, input_file): 41 | """Sandbox for the BAP process. 42 | 43 | Each process is sandboxed, so that all intermediate data are 44 | stored in a temporary directory. 45 | 46 | instance variables: 47 | 48 | - `tmpdir` -- a folder where we will put all our intermediate 49 | files. Might be removed on the cleanup (see cleanup for more); 50 | 51 | - `proc` an instance of `Popen` class if process has started, 52 | None otherwise 53 | 54 | - `args` an argument list that was passed to the `Popen`. 55 | 56 | - `action` a gerund describing the action, that is perfomed by 57 | the analysis 58 | 59 | - `fds` a list of opened filedescriptors to be closed on the exit 60 | 61 | """ 62 | self.tmpdir = tempfile.mkdtemp(prefix="bap") 63 | self.args = [bap, input_file] + self.args 64 | self.proc = None 65 | self.fds = [] 66 | self.out = self.tmpfile("out") 67 | self.action = "running bap" 68 | self.closed = False 69 | self.env = {'BAP_LOG_DIR': self.tmpdir} 70 | if self.DEBUG: 71 | self.env['BAP_DEBUG'] = 'yes' 72 | if not Bap.plugins: 73 | with os.popen(bap + ' list plugins') as out: 74 | Bap.plugins = [e.split()[0] for e in out] 75 | 76 | def run(self): 77 | "starts BAP process" 78 | if self.DEBUG: 79 | print("BAP> {0}\n".format(' '.join(self.args))) 80 | self.proc = subprocess.Popen( 81 | self.args, 82 | stdout=self.out, 83 | stderr=subprocess.STDOUT, 84 | env=self.env) 85 | 86 | def finished(self): 87 | "true if the process is no longer running" 88 | return self.proc and self.proc.poll() is not None 89 | 90 | def close(self): 91 | "terminate the process if needed and cleanup" 92 | if not self.finished(): 93 | if self.proc is not None: 94 | self.proc.terminate() 95 | self.proc.wait() 96 | self.cleanup() 97 | self.closed = True 98 | 99 | def cleanup(self): 100 | """Close and remove all created temporary files. 101 | 102 | For the purposes of debugging, files are not removed 103 | if BAP finished with a positive nonzero code. I.e., 104 | they are removed only if BAP terminated normally, or was 105 | killed by a signal (terminated). 106 | 107 | All opened file descriptros are closed in any case.""" 108 | for desc in self.fds: 109 | desc.close() 110 | 111 | if not self.DEBUG and (self.proc is None or 112 | self.proc.returncode <= 0): 113 | for path in os.listdir(self.tmpdir): 114 | os.remove(os.path.join(self.tmpdir, path)) 115 | os.rmdir(self.tmpdir) 116 | 117 | def tmpfile(self, suffix, *args, **kwargs): 118 | "creates a new temporary files in the self.tmpdir" 119 | if self.tmpdir is None: 120 | self.tmpdir = tempfile.mkdtemp(prefix="bap") 121 | tmp = tempfile.NamedTemporaryFile( 122 | delete=False, 123 | prefix='bap-ida', 124 | suffix="."+suffix, 125 | dir=self.tmpdir, 126 | *args, 127 | **kwargs) 128 | self.fds.append(tmp) 129 | return tmp 130 | 131 | 132 | class BapIda(Bap): 133 | """BAP instance in IDA. 134 | 135 | Uses timer even to poll the ready status of the process. 136 | 137 | """ 138 | instances = [] 139 | poll_interval_ms = 200 140 | 141 | # class level handlers to observe BAP instances, 142 | # useful, for handling gui. See also, on_finished 143 | # and on_cancel, for user specific handlers. 144 | observers = { 145 | 'instance_created': [], 146 | 'instance_updated': [], 147 | 'instance_canceled': [], 148 | 'instance_failed': [], 149 | 'instance_finished': [], 150 | } 151 | 152 | def __init__(self, symbols=True): 153 | try: 154 | check_and_configure_bap() 155 | except: 156 | idc.Message('BAP> configuration failed\n{0}\n'. 157 | format(str(sys.exc_info()))) 158 | traceback.print_exc() 159 | raise BapIdaError() 160 | bap = config.get('bap_executable_path') 161 | if bap is None or not os.access(bap, os.X_OK): 162 | idc.Warning(''' 163 | The bap application is either not found or is not an executable. 164 | Please install bap or, if it is installed, provide a path to it. 165 | Installation instructions are available at: http://bap.ece.cmu.edu. 166 | ''') 167 | raise BapNotFound() 168 | binary = idaapi.get_input_file_path() 169 | super(BapIda, self).__init__(bap, binary) 170 | # if you run IDA inside IDA you will crash IDA 171 | self.args.append('--no-ida') 172 | self._on_finish = [] 173 | self._on_cancel = [] 174 | self._on_failed = [] 175 | if symbols: 176 | self._setup_symbols() 177 | 178 | headers = config.is_set('ida_api.enabled') 179 | 180 | if headers: 181 | self._setup_headers(bap) 182 | 183 | def run(self): 184 | "run BAP instance" 185 | if len(BapIda.instances) > 0: 186 | answer = idaapi.askyn_c( 187 | idaapi.ASKBTN_YES, 188 | "Previous instances of BAP didn't finish yet.\ 189 | Do you really want to start a new one?". 190 | format(len(BapIda.instances))) 191 | if answer == idaapi.ASKBTN_YES: 192 | self._do_run() 193 | else: 194 | self._do_run() 195 | idc.Message("BAP> total number of running instances: {0}\n". 196 | format(len(BapIda.instances))) 197 | 198 | def _setup_symbols(self): 199 | "pass symbol information from IDA to BAP" 200 | with self.tmpfile("sym") as out: 201 | ida.output_symbols(out) 202 | self.args += [ 203 | "--read-symbols-from", out.name, 204 | ] 205 | 206 | def _setup_headers(self, bap): 207 | "pass type information from IDA to BAP" 208 | # this is very fragile, and may break in case 209 | # if we have several BAP instances, especially 210 | # when they are running on different binaries. 211 | # Will leave it as it is until issue #588 is 212 | # resolved in the upstream 213 | with self.tmpfile("h") as out: 214 | ida.output_types(out) 215 | subprocess.call(bap, [ 216 | '--api-add', 'c:"{0}"'.format(out.name), 217 | ]) 218 | 219 | def cleanup(): 220 | subprocess.call(bap, [ 221 | "--api-remove", "c:{0}". 222 | format(os.path.basename(out.name)) 223 | ]) 224 | self.on_cleanup(cleanup) 225 | 226 | def _do_run(self): 227 | try: 228 | super(BapIda, self).run() 229 | BapIda.instances.append(self) 230 | idaapi.register_timer(self.poll_interval_ms, self.update) 231 | idc.SetStatus(idc.IDA_STATUS_THINKING) 232 | self.run_handlers('instance_created') 233 | idc.Message("BAP> created new instance with PID {0}\n". 234 | format(self.proc.pid)) 235 | except: # pylint: disable=bare-except 236 | idc.Message("BAP> failed to create instance\nError: {0}\n". 237 | format(str(sys.exc_info()[1]))) 238 | traceback.print_exc() 239 | 240 | def run_handlers(self, event): 241 | assert event in self.observers 242 | handlers = [] 243 | instance_handlers = { 244 | 'instance_canceled': self._on_cancel, 245 | 'instance_failed': self._on_failed, 246 | 'instance_finished': self._on_finish, 247 | } 248 | 249 | handlers += self.observers[event] 250 | handlers += instance_handlers.get(event, []) 251 | 252 | failures = 0 253 | for handler in handlers: 254 | try: 255 | handler(self) 256 | except: # pylint: disable=bare-except 257 | failures += 1 258 | idc.Message("BAP> {0} failed because {1}\n". 259 | format(self.action, str(sys.exc_info()[1]))) 260 | traceback.print_exc() 261 | if failures != 0: 262 | idc.Warning("Some BAP handlers failed") 263 | 264 | def close(self): 265 | super(BapIda, self).close() 266 | BapIda.instances.remove(self) 267 | 268 | def update(self): 269 | if all(bap.finished() for bap in BapIda.instances): 270 | idc.SetStatus(idc.IDA_STATUS_READY) 271 | if self.finished(): 272 | if self.proc.returncode == 0: 273 | self.run_handlers('instance_finished') 274 | self.close() 275 | idc.Message("BAP> finished " + self.action + '\n') 276 | elif self.proc.returncode > 0: 277 | self.run_handlers('instance_failed') 278 | self.close() 279 | idc.Message("BAP> an error has occured while {0}\n". 280 | format(self.action)) 281 | else: 282 | if not self.closed: 283 | self.run_handlers('instance_canceled') 284 | idc.Message("BAP> was killed by signal {0}\n". 285 | format(-self.proc.returncode)) 286 | return -1 287 | else: 288 | self.run_handlers('instance_updated') 289 | return self.poll_interval_ms 290 | 291 | def cancel(self): 292 | self.run_handlers('instance_canceled') 293 | self.close() 294 | 295 | def on_cleanup(self, callback): 296 | self.on_finish(callback) 297 | self.on_cancel(callback) 298 | self.on_failed(callback) 299 | 300 | def on_finish(self, callback): 301 | self._on_finish.append(callback) 302 | 303 | def on_cancel(self, callback): 304 | self._on_cancel.append(callback) 305 | 306 | def on_failed(self, callback): 307 | self._on_failed.append(callback) 308 | 309 | 310 | class BapIdaError(Exception): 311 | pass 312 | 313 | 314 | class BapNotFound(BapIdaError): 315 | def __str__(self): 316 | return 'Unable to detect bap executable ' 317 | 318 | 319 | class BapFinder(object): 320 | def __init__(self): 321 | self.finders = [] 322 | 323 | def register(self, func): 324 | self.finders.append(func) 325 | 326 | def finder(self, func): 327 | self.register(func) 328 | return func 329 | 330 | def find(self): 331 | path = None 332 | for find in self.finders: 333 | path = find() 334 | break 335 | return path 336 | 337 | 338 | bap = BapFinder() 339 | 340 | 341 | def check_and_configure_bap(): 342 | """Ensures that bap location is known.""" 343 | if not config.get('bap_executable_path'): 344 | path = ask_user(bap.find()) 345 | if path and len(path) > 0: 346 | config.set('bap_executable_path', path) 347 | 348 | 349 | @bap.finder 350 | def system(): 351 | return preadline(['which', 'bap']) 352 | 353 | 354 | @bap.finder 355 | def opam(): 356 | try: 357 | cmd = ['opam', 'config', 'var', 'bap:bin'] 358 | res = preadline(cmd).strip() 359 | if 'undefined' not in res: 360 | return os.path.join(res, 'bap') 361 | else: 362 | return None 363 | except: 364 | return None 365 | 366 | 367 | def confirm(msg): 368 | return idaapi.askyn_c(idaapi.ASKBTN_YES, msg) == idaapi.ASKBTN_YES 369 | 370 | 371 | def ask_user(default_path): 372 | while True: 373 | bap_path = idaapi.askfile_c(False, default_path, 'Path to bap') 374 | if bap_path is None: 375 | if confirm('Are you sure you don\'t want to set path?'): 376 | return None 377 | else: 378 | continue 379 | if not bap_path.endswith('bap'): 380 | if not confirm("Path does not end with bap. Confirm?"): 381 | continue 382 | if not os.path.isfile(bap_path): 383 | if not confirm("Path does not point to a file. Confirm?"): 384 | continue 385 | return bap_path 386 | 387 | 388 | def preadline(cmd): 389 | try: 390 | res = subprocess.check_output(cmd, universal_newlines=True) 391 | return res.strip() 392 | except (OSError, subprocess.CalledProcessError): 393 | return None 394 | -------------------------------------------------------------------------------- /plugins/bap/utils/sexp.py: -------------------------------------------------------------------------------- 1 | from shlex import shlex 2 | 3 | 4 | class Parser(object): 5 | def __init__(self, instream=None, infile=None): 6 | self.lexer = shlex(instream, infile) 7 | self.lexer.wordchars += ":-/@#$%^&*+`\\'" 8 | self.lexer.commenters = ";" 9 | 10 | def __iter__(self): 11 | return self 12 | 13 | def __next__(self): 14 | return self.next() 15 | 16 | def next(self): 17 | token = self.lexer.get_token() 18 | if token == self.lexer.eof: 19 | raise StopIteration 20 | elif token == '(': 21 | return self._parse_list() 22 | else: 23 | return token 24 | 25 | def _parse_list(self): 26 | elts = [] 27 | for token in self.lexer: 28 | if token == ')': 29 | break 30 | elif token == '(': 31 | elts.append(self._parse_list()) 32 | else: 33 | elts.append(token) 34 | return elts 35 | 36 | 37 | def loads(ins): 38 | parser = Parser(ins) 39 | return [x for x in parser] 40 | 41 | 42 | def parse(ins): 43 | parser = Parser(ins) 44 | return parser.next() 45 | -------------------------------------------------------------------------------- /plugins/bap/utils/trace.py: -------------------------------------------------------------------------------- 1 | from .sexp import Parser 2 | 3 | handlers = {} 4 | filters = {} 5 | 6 | 7 | class Loader(object): 8 | 9 | def __init__(self, *args): 10 | self.parser = Parser(*args) 11 | self.state = {} 12 | # the following are private, as we need to maintain 13 | # several invariants on them. 14 | self._handlers = [] 15 | self._filters = [] 16 | self._filter_reqs = set() 17 | 18 | def __iter__(self): 19 | return self 20 | 21 | def __next__(self): 22 | return self.next() 23 | 24 | def enable_handlers(self, names): 25 | """enables the given trace event handler and all its requirements. 26 | Example: 27 | 28 | >>> loader.enable_handler('regs') 29 | """ 30 | self._handlers = satisfy_requirements(self._handlers + names) 31 | for name in self._handlers: 32 | handlers[name].init(self.state) 33 | 34 | def enable_filter(self, filter_name, *args, **kwargs): 35 | """turns on the specified filter. 36 | 37 | Passes all arguments to the filter with the given name. The 38 | returned predicate is checked on each event and if it returns 39 | ``False`` then event handlers will not be called for that 40 | event. 41 | 42 | If a filter has requirements then those requirements are 43 | invoked before the filter, and are not affected by the 44 | predicates of other filters. 45 | 46 | 47 | Example: 48 | >>> loader.enable_filter('filter-machine', id=3) 49 | 50 | Or: 51 | >>> loader.enable_filter('filter-range', lo=0x400000, hi=0x400100) 52 | 53 | """ 54 | filter = filters[filter_name] 55 | requires = satisfy_requirements(filter.requires) 56 | self.enable_handlers(requires) 57 | for req in requires: 58 | self._filter_reqs.add(req) 59 | self._filters.append(filter(*args, **kwargs)) 60 | 61 | def next(self): 62 | event = self.parser.next() 63 | if len(event) != 2: 64 | raise ValueError('Malformed Observation {}'.format(event)) 65 | event, payload = event 66 | completed = set() 67 | self.state['event'] = event 68 | 69 | # first run handlers that are required by filters 70 | for h in self._handlers: 71 | if h in self._filter_reqs and event in handlers[h].events: 72 | completed.add(h) 73 | handlers[h](self.state, payload) 74 | 75 | # now we can run all filters, 76 | for accept in self._filters: 77 | if not accept(self.state): 78 | break 79 | else: # and if nobody complains then run the rest of handlers 80 | for h in self._handlers: 81 | if h not in completed and event in handlers[h].events: 82 | handlers[h](self.state, payload) 83 | return self.state 84 | 85 | def run(self): 86 | for state in self: 87 | pass 88 | 89 | 90 | def attach_meta_attributes(h, **kwargs): 91 | """attaches meta attributes to the provided callable object ``h`` 92 | 93 | The following attributes are attached to the ``__dict__`` 94 | namespace (also available through the ``func_dict`` name): 95 | 96 | - ``'name'`` a normalized human readable name, 97 | computed from a function name with underscores substituted 98 | by dashes. If ``'name'`` is in ``kwargs``, then the provided 99 | name will be used instead. 100 | 101 | - ``'requires'`` a list handler dependencies. Will be empty if 102 | the ``requires`` keyword argument is not provided. Otherwise it 103 | will be intialized from the argument value, that could be a list 104 | or a string. 105 | 106 | - all other attributes from the ``kwargs`` argument. 107 | """ 108 | if 'name' not in kwargs: 109 | name = h.__name__.replace('_', '-') 110 | h.__dict__['name'] = name 111 | req = kwargs.get('requires', []) 112 | if 'requires' in kwargs: 113 | del kwargs['requires'] 114 | h.__dict__['requires'] = req if isinstance(req, list) else [req] 115 | h.__dict__.update(kwargs) 116 | 117 | 118 | def handler(*args, **kwargs): 119 | """a decorator that creates a trace event handler 120 | 121 | Registers the provided function as an event handler for the 122 | specified list of events. If enabled the function will be called 123 | every time one of the events occurs with two arguments - the 124 | trace loader state (which is a dictionary) and the payload of the 125 | occurred event, which is an s-expression represented as a list. 126 | 127 | The loader state is guaranteed to have the ``'event'`` attribute 128 | that will contain the name of the current event. 129 | 130 | Example: 131 | ``` 132 | @handler('switch', 'fork') 133 | def machine_id(state, fromto): 134 | state['machine-id'] = fromto[1] 135 | 136 | ``` 137 | """ 138 | def make_handler(f): 139 | f.__dict__['events'] = args 140 | if 'init' in kwargs: 141 | default = kwargs['init'] 142 | f.__dict__['init'] = lambda s: s.update(default) 143 | del kwargs['init'] 144 | else: 145 | f.__dict__['init'] = lambda x: None 146 | attach_meta_attributes(f, **kwargs) 147 | handlers[f.name] = f 148 | return make_handler 149 | 150 | 151 | def filter(**kwargs): 152 | """a decorator that creates a trace event filter 153 | 154 | The decorated function must accept the state dictionary 155 | and zero or more user provided arguments and return ``True`` 156 | or ``False`` depending on whether the current event should be 157 | accepted or, correspondingly rejected. 158 | 159 | If the ``requires`` argument is passed to the filter decorator 160 | then the loader will ensure that all event handlers in ``requires`` 161 | are run before the filter is called. 162 | 163 | Note: if a handler is required by any filter, then it will be 164 | always invoked, no matter whether its event is filtered or not. 165 | 166 | The decorator will also add several meta attributes to the 167 | decorated function (as described in ``attach_meta_attributes``) 168 | and update the global dictionary of available filters. 169 | 170 | Example: 171 | ``` 172 | @filter(requires='pc') 173 | def filter_range(state, lo, hi): 174 | return lo <= state['pc'] <= hi 175 | ``` 176 | """ 177 | def make_filter(f): 178 | def init(**kwargs): 179 | return lambda state: f(state, **kwargs) 180 | attach_meta_attributes(f, **kwargs) 181 | attach_meta_attributes(init, name=f.name, **kwargs) 182 | filters[init.name] = init 183 | return make_filter 184 | 185 | 186 | @handler('machine-switch', 'machine-fork', init={'machine-id': 0}) 187 | def machine_id(state, fromto): 188 | """tracks machine identifier 189 | 190 | Maintains the 'machine-id' field in the state. 191 | """ 192 | state['machine-id'] = int(fromto[1]) 193 | 194 | 195 | @handler('pc-changed', init={'pc': 0}) 196 | def pc(state, data): 197 | """tracks program counter 198 | 199 | Maintains the 'pc' field in the state. 200 | """ 201 | state['pc'] = word(data)['value'] 202 | 203 | 204 | @handler('enter-term', init={'term-id': None}) 205 | def term_id(state, data): 206 | """tracks term identifier 207 | 208 | Maintaints the 'term-id' field in the state. 209 | """ 210 | state['term-id'] = data 211 | 212 | 213 | @handler('pc-changed', 'written', init={'regs': {}}) 214 | def regs(state, data): 215 | """"tracks register assignments 216 | 217 | Provides the 'regs' field, which is a mapping from 218 | register names to values. 219 | """ 220 | if state['event'] == 'pc-changed': 221 | state['regs'] = {} 222 | else: 223 | state['regs'][data[0]] = value(data[1]) 224 | 225 | 226 | @handler('pc-changed', 'stored', init={'mems': {}}) 227 | def mems(state, data): 228 | """tracks memory writes 229 | 230 | Provides the 'mems' field that represents all updates made by 231 | the current instruction to the memory in a form of a mapping 232 | from addresses to bytes. Both are represented with the Python 233 | int type 234 | """ 235 | if state['event'] == 'pc-changed': 236 | state['mems'] = {} 237 | else: 238 | state['mems'][word(data[0])['value']] = value(data[1]) 239 | 240 | 241 | @filter(requires='pc') 242 | def filter_range(state, lo=0, hi=None): 243 | """masks events that do not fall into the specified region. 244 | 245 | interval bounds are included. 246 | """ 247 | pc = state['pc'] 248 | return lo <= pc and (hi is None or pc <= hi) 249 | 250 | 251 | @filter(requires='machine-id') 252 | def filter_machine(state, id): 253 | "masks events that do not belong to the specified machine identifier" 254 | cur = state['machine-id'] 255 | return cur == id if isinstance(id, int) else cur in id 256 | 257 | 258 | def word(x): 259 | "parses a Primus word into a ``value``, ``type`` dictionary" 260 | w, t = x.split(':') 261 | return { 262 | 'value': int(w, 0), 263 | 'type': t 264 | } 265 | 266 | 267 | def value(x): 268 | "parses a Primus value into a ``value``, ``type``, ``id`` dictionary" 269 | w, id = x.split('#') 270 | w = word(w) 271 | w['id'] = int(id) 272 | return w 273 | 274 | 275 | def satisfy_requirements(requests): 276 | """ensures that each request gets what it ``requires``. 277 | 278 | Accepts a list of handler names and returns a list of handler 279 | names that guarantees that if a handler has a non-empty 280 | ``requires`` field, then all names in this list will precede the 281 | name of this handler. It also guarantees that each handler will 282 | occur at most once. 283 | """ 284 | solution = [] 285 | for name in requests: 286 | solution += satisfy_requirements(handlers[name].requires) 287 | solution.append(name) 288 | 289 | # now we need to dedup the solution - a handler must occur at most once 290 | result = [] 291 | for h in solution: 292 | if h not in result: 293 | result.append(h) 294 | return result 295 | -------------------------------------------------------------------------------- /plugins/plugin_loader_bap.py: -------------------------------------------------------------------------------- 1 | """Loads all possible BAP IDA Python plugins.""" 2 | import os 3 | import bap.plugins 4 | import bap.utils.run 5 | import idaapi 6 | 7 | 8 | class bap_loader(idaapi.plugin_t): 9 | """Loads plugins from the bap/plugins directory.""" 10 | 11 | flags = idaapi.PLUGIN_FIX 12 | comment = "BAP Plugin Loader" 13 | help = "BAP Plugin Loader" 14 | wanted_name = "BAP_Plugin_Loader" 15 | wanted_hotkey = "" 16 | 17 | def init(self): 18 | """Read directory and load as many plugins as possible.""" 19 | self.plugins = [] 20 | 21 | idaapi.msg("BAP Loader activated\n") 22 | 23 | bap.utils.run.check_and_configure_bap() 24 | 25 | plugin_path = os.path.dirname(bap.plugins.__file__) 26 | idaapi.msg("BAP> Loading plugins from {}\n".format(plugin_path)) 27 | 28 | for plugin in sorted(os.listdir(plugin_path)): 29 | path = os.path.join(plugin_path, plugin) 30 | if not plugin.endswith('.py') or plugin.startswith('__'): 31 | continue # Skip non-plugins 32 | idaapi.msg('BAP> Loading {}\n'.format(plugin)) 33 | self.plugins.append(idaapi.load_plugin(path)) 34 | return idaapi.PLUGIN_KEEP 35 | 36 | def term(self): 37 | """Ignored.""" 38 | pass 39 | 40 | def run(self, arg): 41 | """Ignored.""" 42 | pass 43 | 44 | 45 | def PLUGIN_ENTRY(): 46 | """Load the bap_loader.""" 47 | return bap_loader() 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='bap-ida-python', 7 | version='1.2.0', 8 | description='BAP IDA Plugin', 9 | author='BAP Team', 10 | url='https://github.com/BinaryAnalysisPlatform/bap-ida-python', 11 | maintainer='Ivan Gotovchits', 12 | maintainer_email='ivg@ieee.org', 13 | license='MIT', 14 | package_dir={'': 'plugins'}, 15 | packages=['bap', 'bap.utils', 'bap.plugins'], 16 | classifiers=[ 17 | 'Development Status :: 3 - Alpha', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Topic :: Software Development :: Disassemblers', 20 | 'Topic :: Security' 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | from time import sleep 4 | 5 | import pytest 6 | 7 | 8 | sys.modules['idaapi'] = __import__('mockidaapi') 9 | sys.modules['idc'] = __import__('mockidc') 10 | sys.modules['idautils'] = __import__('mockidautils') 11 | 12 | 13 | @pytest.fixture 14 | def idapatch(monkeypatch): 15 | def patch(attrs, ns='idaapi'): 16 | for (k, v) in attrs.items(): 17 | monkeypatch.setattr(ns + '.' + k, v, raising=False) 18 | return patch 19 | 20 | 21 | @pytest.fixture 22 | def addresses(monkeypatch): 23 | addresses = (0xDEADBEAF, 0xDEADBEEF) 24 | monkeypatch.setattr('bap.utils.ida.addresses', lambda: addresses) 25 | return addresses 26 | 27 | 28 | @pytest.fixture 29 | def comments(idapatch): 30 | cmts = {} 31 | 32 | def set_cmt(ea, val, off): 33 | cmts[ea] = val 34 | idapatch({ 35 | 'get_cmt': lambda ea, off: cmts.get(ea), 36 | 'set_cmt': set_cmt 37 | }) 38 | yield cmts 39 | 40 | 41 | @pytest.fixture(scope='session') 42 | def load(): 43 | def load(module): 44 | plugin = module.PLUGIN_ENTRY() 45 | plugin.init() 46 | return plugin 47 | return load 48 | 49 | 50 | @pytest.fixture(params=['yes', 'no', 'cancel']) 51 | def choice(request, idapatch): 52 | choice = request.param 53 | idapatch({ 54 | 'ASKBTN_YES': 'yes', 55 | 'ASKBTN_NO': 'no', 56 | 'ASKBTN_CANCEL': 'cancel', 57 | 'askyn_c': lambda d, t: request.param 58 | }) 59 | return choice 60 | 61 | 62 | BAP_PATH = '/opt/bin/bap' 63 | 64 | 65 | @pytest.fixture(params=[ 66 | ('stupid', None, 'what?', 'oh, okay', 'bap'), 67 | ('clever', )]) 68 | def askbap(request, idapatch, monkeypatch): 69 | param = list(request.param) 70 | user = param.pop(0) 71 | 72 | monkeypatch.setattr('os.path.isfile', lambda p: p == BAP_PATH) 73 | idapatch({'ASKBTN_YES': 'yes', 'askyn_c': lambda d, t: 'yes'}) 74 | 75 | def ask(unk, path, msg): 76 | if user == 'clever': 77 | return path 78 | elif user == 'stupid': 79 | if len(param) > 0: 80 | return param.pop(0) 81 | idapatch({'askfile_c': ask}) 82 | return {'user': user, 'path': BAP_PATH} 83 | 84 | 85 | @pytest.fixture 86 | def idadir(idapatch, tmpdir): 87 | idapatch({'idadir': lambda x: str(tmpdir.mkdir(x))}) 88 | return tmpdir.dirname 89 | 90 | 91 | class Popen(subprocess.Popen): 92 | patches = [] 93 | 94 | def __init__(self, args, **kwargs): 95 | for patch in Popen.patches: 96 | script = patch(args) 97 | if script: 98 | super(Popen, self).__init__(script, shell=True, **kwargs) 99 | break 100 | else: 101 | super(Popen, self).__init__(args, **kwargs) 102 | 103 | 104 | @pytest.fixture 105 | def popenpatch(monkeypatch): 106 | monkeypatch.setattr('subprocess.Popen', Popen) 107 | 108 | def same_cmd(cmd, args): 109 | return cmd == ' '.join(args) 110 | 111 | def add(patch): 112 | Popen.patches.append(patch) 113 | 114 | def patch(*args): 115 | if len(args) == 1: 116 | add(args[0]) 117 | elif len(args) == 2: 118 | add(lambda pargs: args[1] if same_cmd(args[0], pargs) else None) 119 | else: 120 | raise TypeError('popenpatch() takes 1 or two arguments ({} given)'. 121 | format(len(args))) 122 | yield patch 123 | Popen.patches = [] 124 | 125 | 126 | @pytest.fixture(params=[None, BAP_PATH]) 127 | def bappath(request, popenpatch): 128 | path = request.param 129 | if path: 130 | popenpatch('which bap', 'echo {}'.format(path)) 131 | else: 132 | popenpatch('which bap', 'false') 133 | popenpatch('opam config var bap:bin', 'echo undefind; false') 134 | return path 135 | 136 | 137 | class Ida(object): 138 | '''IDA instance imitator. ''' 139 | 140 | def __init__(self): 141 | self.time = 0 142 | self.callbacks = [] 143 | self.log = [] 144 | self.warnings = [] 145 | self.status = 'ready' 146 | 147 | def register_timer(self, interval, cb): 148 | '''add a recurrent callback. 149 | 150 | Registers a function that will be called after the specified 151 | ``interval``. The function may return a positive value, that 152 | will effectively re-register itself. If a negative value is 153 | returned, then the callback will not be called anymore. 154 | 155 | Note: the realtime clocks are imitated with the ``sleep`` 156 | function using 10ms increments. 157 | ''' 158 | self.callbacks.append({ 159 | 'time': self.time + interval, 160 | 'call': cb 161 | }) 162 | 163 | def message(self, msg): 164 | self.log.append(msg) 165 | 166 | def warning(self, msg): 167 | self.warnings.append(msg) 168 | 169 | def set_status(self, status): 170 | self.status = status 171 | 172 | def run(self): 173 | '''Runs IDA event cycle. 174 | The function will return if there are no more callbacks. 175 | ''' 176 | while self.callbacks: 177 | sleep(0.1) 178 | self.time += 100 179 | for cb in self.callbacks: 180 | if cb['time'] < self.time: 181 | time = cb['call']() 182 | if time is None or time < 0: 183 | self.callbacks.remove(cb) 184 | else: 185 | cb['time'] = self.time + time 186 | 187 | 188 | class Bap(object): 189 | '''BAP utility mock. 190 | 191 | From the perspective of the IDA integration, the bap frontend 192 | utility is considered a backend. So, we will refer to it as a 193 | backend here and later. 194 | 195 | This mock class mimicks the behavior of the bap backend, as the 196 | unit tests must not dependend on the presence of the bap 197 | framework. 198 | 199 | The instances of the backend has the following attributes: 200 | 201 | - ``path`` a path to bap executable 202 | - ``calls`` a list of calls made to backend, each call is 203 | a dictionary that has at least these fields: 204 | - args - arguments passed to the Popen call 205 | - ``on_call`` a list of the call event handlers. An event 206 | handler should be a callable, that accepts two arguments. 207 | The first argument is a reference to the bap backend instance, 208 | the second is a reference to the ``proc`` dictionary (as described 209 | above). If a callback returns a non None value, then this value is 210 | used as a return value of the call to BAP. See ``call`` method 211 | description for more information about the return values. 212 | ''' 213 | 214 | def __init__(self, path): 215 | '''initializes BAP backend instance. 216 | 217 | Once instance corresponds to one bap installation (not to a 218 | process instance). See class descriptions for information about 219 | the instance attributes. 220 | ''' 221 | self.path = path 222 | self.calls = [] 223 | self.on_call = [] 224 | 225 | def call(self, args): 226 | '''mocks a call to a bap executable. 227 | 228 | If a call returns with an integer, then it is passed to the 229 | shell's exit command, otherwise a string representation of a 230 | returned value is used to form a command, that is then passed 231 | to a Popen constructor. 232 | ''' 233 | proc = {'args': args} 234 | self.calls.append(proc) 235 | for call in self.on_call: 236 | res = call(self, proc) 237 | if res is not None: 238 | return res 239 | return 0 240 | 241 | 242 | @pytest.fixture 243 | def bapida(idapatch, popenpatch, monkeypatch, idadir): 244 | from bap.utils import config 245 | ida = Ida() 246 | bap = Bap(BAP_PATH) 247 | 248 | def run_bap(args): 249 | if args[0] == BAP_PATH: 250 | res = bap.call(args) or 0 251 | if isinstance(res, int): 252 | return 'exit ' + str(res) 253 | else: 254 | return str(res) 255 | 256 | config.set('bap_executable_path', bap.path) 257 | monkeypatch.setattr('os.access', lambda p, x: p == BAP_PATH) 258 | 259 | idapatch({ 260 | 'register_timer': ida.register_timer, 261 | 'get_input_ida_file_path': lambda: 'true' 262 | }) 263 | idapatch(ns='idc', attrs={ 264 | 'Message': ida.message, 265 | 'Warning': ida.warning, 266 | 'SetStatus': ida.set_status, 267 | }) 268 | popenpatch(run_bap) 269 | monkeypatch.setattr('bap.utils.ida.output_symbols', lambda out: None) 270 | return (bap, ida) 271 | -------------------------------------------------------------------------------- /tests/mockidaapi.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | ASKBTN_YES = 0 4 | ASKBTN_NO = 0 5 | ASKBTN_CANCEL = 0 6 | PLUGIN_DRAW = 0 7 | PLUGIN_HIDE = 0 8 | PLUGIN_KEEP = 0 9 | PLUGIN_FIX = 0 10 | class plugin_t(object): pass 11 | class text_sink_t(object): pass 12 | class Choose2(object): pass 13 | def idadir(sub): return NotImplemented 14 | def get_cmt(ea, off): return NotImplemented 15 | def set_cmt(ea, off): return NotImplemented 16 | def askyn_c(dflt, title): return NotImplemented 17 | def get_input_file_path() : return NotImplemented 18 | def get_segm_name(ea): return NotImplemented 19 | -------------------------------------------------------------------------------- /tests/mockidautils.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/mockidc.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | IDA_STATUS_THINKING="thinking" 4 | IDA_STATUS_READY='ready' 5 | IDA_STATUS_WAITING='waiting' 6 | IDA_STATUS_WORK='work' 7 | 8 | def Message(msg): NotImplemented 9 | def Warning(msg): NotImplemented 10 | def SetStatus(s): NotImplemented 11 | -------------------------------------------------------------------------------- /tests/test_bap_comment.py: -------------------------------------------------------------------------------- 1 | from bap.utils.bap_comment import parse, dumps, is_valid 2 | 3 | 4 | def test_parse(): 5 | assert parse('hello') is None 6 | assert parse('BAP: hello') == {'hello': []} 7 | assert parse('BAP: hello,world') == {'hello': [], 'world': []} 8 | assert parse('BAP: hello=cruel,world') == {'hello': ['cruel', 'world']} 9 | assert parse('BAP: hello="hello, world"') == {'hello': ['hello, world']} 10 | assert parse('BAP: hello=cruel,world goodbye=real,life') == { 11 | 'hello': ['cruel', 'world'], 12 | 'goodbye': ['real', 'life'] 13 | } 14 | assert parse('BAP: hello="f\'"') == {'hello': ["f'"]} 15 | 16 | 17 | def test_dumps(): 18 | assert 'BAP:' in dumps({'hello': []}) 19 | assert dumps({'hello': ['cruel', 'world'], 'nice': [], 'thing': []}) == \ 20 | 'BAP: nice,thing hello=cruel,world' 21 | assert dumps({'hello': ["world'"]}) == 'BAP: hello="world\'"' 22 | 23 | 24 | def test_is_valid(): 25 | assert is_valid('BAP: hello') 26 | assert is_valid('BAP: hello,world') 27 | assert not is_valid('some comment') 28 | 29 | 30 | def test_roundup(): 31 | comm = { 32 | 'x': [], 'y': [], 'z': [], 33 | 'a': ['1', '2', '3'], 34 | 'b': ['thing\''], 35 | 'c': ['many things'], 36 | 'd': ['strange \\ things'], 37 | } 38 | assert parse(dumps(parse(dumps(comm)))) == comm 39 | 40 | 41 | def test_quotation(): 42 | data = 'BAP: chars="{\\\"a\\\", \\\"b\\\", \\\"c\\\"}"' 43 | assert parse(data) == {'chars': ['{"a", "b", "c"}']} 44 | assert parse(data) == parse(dumps(parse(data))) 45 | 46 | 47 | def test_single_quote(): 48 | data = 'BAP: key="{can\\\'t do}"' 49 | assert parse(data) == {'key': ["{can\\'t do}"]} 50 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | def test_set_and_get(idadir): 2 | from bap.utils.config import get, set, is_set 3 | for path in ('foo', 'foo.bar'): 4 | assert get(path) is None 5 | set(path, 'hello') 6 | assert get(path) == 'hello' 7 | assert not is_set(path) 8 | set(path, 'true') 9 | assert is_set(path) 10 | -------------------------------------------------------------------------------- /tests/test_ida.py: -------------------------------------------------------------------------------- 1 | def test_comments(addresses, comments, choice, load): 2 | from bap.utils import ida 3 | from bap.plugins import bap_clear_comments 4 | from bap.plugins import bap_comments 5 | 6 | ida.comment.handlers = [] 7 | ida.comment.comments.clear() 8 | 9 | load(bap_comments) 10 | clear = load(bap_clear_comments) 11 | 12 | assert len(ida.comment.handlers) == 1 13 | 14 | for addr in addresses: 15 | ida.comment.add(addr, 'foo', 'bar') 16 | assert comments[addr] == 'BAP: foo=bar' 17 | ida.comment.add(addr, 'foo', 'baz') 18 | assert comments[addr] == 'BAP: foo=bar,baz' 19 | ida.comment.add(addr, 'bar', '()') 20 | assert comments[addr] == 'BAP: bar foo=bar,baz' 21 | 22 | clear.run(0) 23 | bap_cmts = [c for c in comments.values() if 'BAP:' in c] 24 | expected = { 25 | 'yes': 0, 26 | 'no': len(addresses), 27 | 'cancel': len(addresses), 28 | } 29 | assert len(bap_cmts) == expected[choice] 30 | -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | 4 | def test_check_and_configure_bap(bappath, askbap, idadir): 5 | from bap.utils.run import check_and_configure_bap 6 | from bap.utils import config 7 | check_and_configure_bap() 8 | bap = config.get('bap_executable_path') 9 | expected = { 10 | 'clever': bappath, 11 | 'stupid': None 12 | } 13 | assert bap == expected[askbap['user']] 14 | 15 | 16 | def test_run_without_args(bapida): 17 | from bap.utils.run import BapIda 18 | backend, frontend = bapida 19 | bap = BapIda() 20 | bap.run() 21 | frontend.run() 22 | assert len(backend.calls) == 1 23 | args = backend.calls[0]['args'] 24 | assert args[0] == backend.path 25 | assert '--no-ida' in args 26 | assert '--read-symbols-from' in args 27 | 28 | 29 | def test_disable_symbols(bapida): 30 | from bap.utils.run import BapIda 31 | backend, frontend = bapida 32 | bap = BapIda(symbols=False) 33 | bap.run() 34 | frontend.run() 35 | assert len(backend.calls) == 1 36 | args = backend.calls[0]['args'] 37 | assert args[0] == backend.path 38 | assert '--no-ida' in args 39 | assert '--read-symbols-from' not in args 40 | 41 | 42 | def test_event_handlers(bapida): 43 | from bap.utils.run import BapIda 44 | backend, frontend = bapida 45 | bap = BapIda() 46 | bap.events = [] 47 | 48 | def occured(bap, event): 49 | bap.events.append(event) 50 | 51 | events = ('instance_created', 'instance_updated', 'instance_finished') 52 | for event in events: 53 | BapIda.observers[event].append(partial(occured, event=event)) 54 | 55 | backend.on_call.append(lambda bap, args: 'sleep 1') 56 | bap.on_finish(lambda bap: occured(bap, 'success')) 57 | 58 | bap.run() 59 | frontend.run() 60 | 61 | for msg in frontend.log: 62 | print(msg) 63 | 64 | for event in events: 65 | assert event in bap.events 66 | 67 | assert 'success' in bap.events 68 | 69 | 70 | def test_failure(bapida): 71 | from bap.utils.run import BapIda 72 | backend, frontend = bapida 73 | bap = BapIda() 74 | bap.events = [] 75 | 76 | backend.on_call.append(lambda bap, args: 1) 77 | bap.on_finish(lambda bap: bap.events.append('success')) 78 | 79 | bap.run() 80 | frontend.run() 81 | 82 | for msg in frontend.log: 83 | print(msg) 84 | 85 | assert 'success' not in bap.events 86 | assert len(BapIda.instances) == 0 87 | 88 | 89 | def test_cancel(bapida): 90 | from bap.utils.run import BapIda 91 | backend, frontend = bapida 92 | bap = BapIda() 93 | bap.events = [] 94 | 95 | backend.on_call.append(lambda bap, args: 'sleep 100') 96 | frontend.register_timer(600, lambda: bap.cancel()) 97 | 98 | bap.on_finish(lambda bap: bap.events.append('success')) 99 | bap.on_cancel(lambda bap: bap.events.append('canceled')) 100 | 101 | bap.run() 102 | frontend.run() 103 | 104 | assert 'success' not in bap.events 105 | assert 'canceled' in bap.events 106 | assert len(BapIda.instances) == 0 107 | -------------------------------------------------------------------------------- /tests/test_sexp.py: -------------------------------------------------------------------------------- 1 | from bap.utils.sexp import parse 2 | 3 | 4 | def test_parse(): 5 | assert parse('()') == [] 6 | assert parse('hello') == 'hello' 7 | assert parse('"hello world"') == '"hello world"' 8 | assert parse('(hello world)') == ['hello', 'world'] 9 | assert parse('(() () ())') == [[], [], []] 10 | assert parse("hi'") == "hi'" 11 | assert parse('hello"') == 'hello"' 12 | assert parse('(hello\" cruel world\")') == ['hello"', 'cruel', 'world"'] 13 | assert parse('(a (b c) c (d (e f) g) h') == [ 14 | 'a', 15 | ['b', 'c'], 16 | 'c', 17 | ['d', ['e', 'f'], 'g'], 18 | 'h' 19 | ] 20 | -------------------------------------------------------------------------------- /tests/test_trace.py: -------------------------------------------------------------------------------- 1 | from bap.utils import trace 2 | 3 | testdata = [ 4 | { 5 | 'input': '(pc-changed 0x10:64u)', 6 | 'state': { 7 | 'event': 'pc-changed', 8 | 'pc': 0x10, 9 | 'machine-id': 0, 10 | 'regs': {}, 11 | 'mems': {}, 12 | } 13 | }, 14 | 15 | { 16 | 'input': '(written (RAX 1:64u#1))', 17 | 'state': { 18 | 'event': 'written', 19 | 'pc': 0x10, 20 | 'machine-id': 0, 21 | 'regs': { 22 | 'RAX': {'value': 1, 'type': '64u', 'id': 1} 23 | }, 24 | 'mems': {}, 25 | } 26 | }, 27 | 28 | { 29 | 'input': '(written (RBX 2:64u#4))', 30 | 'state': { 31 | 'event': 'written', 32 | 'pc': 0x10, 33 | 'machine-id': 0, 34 | 'regs': { 35 | 'RAX': {'value': 1, 'type': '64u', 'id': 1}, 36 | 'RBX': {'value': 2, 'type': '64u', 'id': 4} 37 | }, 38 | 'mems': {}, 39 | } 40 | }, 41 | 42 | { 43 | 'input': '(machine-fork (0 1))', 44 | 'state': { 45 | 'event': 'machine-fork', 46 | 'pc': 0x10, 47 | 'machine-id': 1, 48 | 'regs': { 49 | 'RAX': {'value': 1, 'type': '64u', 'id': 1}, 50 | 'RBX': {'value': 2, 'type': '64u', 'id': 4} 51 | }, 52 | 'mems': {}, 53 | } 54 | }, 55 | 56 | { 57 | 'input': '(written (ZF 1:1u#32))', 58 | 'state': { 59 | 'event': 'written', 60 | 'pc': 0x10, 61 | 'machine-id': 1, 62 | 'regs': { 63 | 'RAX': {'value': 1, 'type': '64u', 'id': 1}, 64 | 'RBX': {'value': 2, 'type': '64u', 'id': 4}, 65 | 'ZF': {'value': 1, 'type': '1u', 'id': 32} 66 | }, 67 | 'mems': {}, 68 | } 69 | }, 70 | 71 | { 72 | 'input': '(machine-switch (1 0))', 73 | 'state': { 74 | 'event': 'machine-switch', 75 | 'pc': 0x10, 76 | 'machine-id': 0, 77 | 'regs': { 78 | 'RAX': {'value': 1, 'type': '64u', 'id': 1}, 79 | 'RBX': {'value': 2, 'type': '64u', 'id': 4}, 80 | 'ZF': {'value': 1, 'type': '1u', 'id': 32} 81 | }, 82 | 'mems': {}, 83 | } 84 | }, 85 | 86 | { 87 | 'input': '(pc-changed 0x11:64u)', 88 | 'state': { 89 | 'event': 'pc-changed', 90 | 'pc': 0x11, 91 | 'machine-id': 0, 92 | 'regs': {}, 93 | 'mems': {}, 94 | } 95 | }, 96 | 97 | { 98 | 'input': '(stored (0x400:64u#42 0xDE:8u#706))', 99 | 'state': { 100 | 'event': 'stored', 101 | 'pc': 0x11, 102 | 'machine-id': 0, 103 | 'regs': {}, 104 | 'mems': { 105 | 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} 106 | }, 107 | } 108 | }, 109 | 110 | { 111 | 'input': """ 112 | (incident-location (2602 113 | (677:27ee3 677:27e85 677:27e74 677:27e6b 677:27e60 114 | 677:27edc 677:27ed0 677:27ee3 677:27e85 677:27e74 115 | 677:27e6b 677:27e60 677:27edc 677:27ed0 677:27ee3 116 | 677:27e85 677:27e74 677:27e6b 677:27e60 677:27edc))) 117 | """, 118 | 'state': { 119 | 'event': 'incident-location', 120 | 'pc': 0x11, 121 | 'machine-id': 0, 122 | 'regs': {}, 123 | 'mems': { 124 | 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} 125 | }, 126 | } 127 | }, 128 | 129 | { 130 | 'input': '(machine-switch (0 1))', 131 | 'state': { 132 | 'event': 'machine-switch', 133 | 'pc': 0x11, 134 | 'machine-id': 1, 135 | 'regs': {}, 136 | 'mems': { 137 | 0x400: {'value': 0xDE, 'type': '8u', 'id': 706} 138 | }, 139 | } 140 | }, 141 | 142 | { 143 | 'input': '(pc-changed 0x10:64u)', 144 | 'state': { 145 | 'event': 'pc-changed', 146 | 'pc': 0x10, 147 | 'machine-id': 1, 148 | 'regs': {}, 149 | 'mems': {}, 150 | } 151 | }, 152 | 153 | { 154 | 'input': '(written (RAX 2:64u#3))', 155 | 'state': { 156 | 'event': 'written', 157 | 'pc': 0x10, 158 | 'machine-id': 1, 159 | 'regs': { 160 | 'RAX': {'value': 2, 'type': '64u', 'id': 3} 161 | }, 162 | 'mems': {}, 163 | } 164 | }, 165 | 166 | { 167 | 'input': '(pc-changed 0x11:64u)', 168 | 'state': { 169 | 'event': 'pc-changed', 170 | 'pc': 0x11, 171 | 'machine-id': 1, 172 | 'regs': {}, 173 | 'mems': {}, 174 | } 175 | }, 176 | 177 | 178 | { 179 | 'input': '(call (malloc 2:64u#12))', 180 | 'state': { 181 | 'event': 'call', 182 | 'pc': 0x11, 183 | 'machine-id': 1, 184 | 'regs': {}, 185 | 'mems': {}, 186 | } 187 | }, 188 | 189 | { 190 | 'input': '(pc-changed 0x1:64u)', 191 | 'state': { 192 | 'event': 'pc-changed', 193 | 'pc': 0x1, 194 | 'machine-id': 1, 195 | 'regs': {}, 196 | 'mems': {}, 197 | } 198 | }, 199 | 200 | { 201 | 'input': '(written (RAX 2:64u#3))', 202 | 'state': { 203 | 'event': 'written', 204 | 'pc': 0x1, 205 | 'machine-id': 1, 206 | 'regs': {}, 207 | 'mems': {}, 208 | } 209 | }, 210 | 211 | { 212 | 'input': '(pc-changed 0x12:64u)', 213 | 'state': { 214 | 'event': 'pc-changed', 215 | 'pc': 0x12, 216 | 'machine-id': 1, 217 | 'regs': {}, 218 | 'mems': {}, 219 | } 220 | }, 221 | 222 | { 223 | 'input': '(written (RAX 2:64u#3))', 224 | 'state': { 225 | 'event': 'written', 226 | 'pc': 0x12, 227 | 'machine-id': 1, 228 | 'regs': { 229 | 'RAX': {'value': 2, 'type': '64u', 'id': 3} 230 | }, 231 | 'mems': {}, 232 | } 233 | }, 234 | 235 | { 236 | 'input': '(machine-fork (1 2))', 237 | 'state': { 238 | 'event': 'machine-fork', 239 | 'pc': 0x12, 240 | 'machine-id': 2, 241 | 'regs': { 242 | 'RAX': {'value': 2, 'type': '64u', 'id': 3} 243 | }, 244 | 'mems': {}, 245 | } 246 | }, 247 | 248 | { 249 | 'input': '(stored (0x600:64u#76 0xDE:64u#))', 250 | 'state': { 251 | 'event': 'stored', 252 | 'pc': 0x12, 253 | 'machine-id': 2, 254 | 'regs': { 255 | 'RAX': {'value': 2, 'type': '64u', 'id': 3} 256 | }, 257 | 'mems': {}, 258 | } 259 | } 260 | ] 261 | 262 | 263 | def test_loader(): 264 | loader = trace.Loader('\n'.join(s['input'] for s in testdata)) 265 | loader.enable_handlers(['regs', 'mems']) 266 | loader.enable_filter('filter-machine', id=[0, 1]) 267 | loader.enable_filter('filter-range', lo=0x10, hi=0x20) 268 | step = 0 269 | for state in loader: 270 | assert step >= 0 and state == testdata[step]['state'] 271 | step += 1 272 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py3 3 | 4 | [testenv] 5 | deps=pytest 6 | commands=py.test --------------------------------------------------------------------------------