├── .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 | [](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 | 
39 |
40 | #### In Pseudocode View
41 | 
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 | 
64 |
65 | #### In Pseudocode View
66 | 
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 | 
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
--------------------------------------------------------------------------------