├── .gitattributes ├── .gitignore ├── AUTHORS.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __root__.py ├── application ├── __quicktime.py ├── delphi.py ├── quicktime.py └── windbg.py ├── base ├── _comment.py ├── _declaration.py ├── _exceptions.py ├── _interface.py ├── _netnode.py ├── _utils.py ├── database.py ├── enumeration.py ├── function.py ├── instruction.py ├── segment.py └── structure.py ├── ida-plugin.json ├── idapythonrc.py ├── misc ├── hooks.py └── ui.py ├── plugins └── minsc.py ├── requirements.txt └── tools ├── general.py ├── tagfix.py └── tags.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.py text eol=lf 3 | 4 | *.pyc binary 5 | *.txt text eol=lf 6 | *.cfg text eol=lf 7 | *.lst text eol=lf 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Ali Rizvi-Santiago 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to IDA-minsc 2 | 3 | Contribution to the IDA-minsc plugin is pretty much exclusively done with 4 | GitHub's interface. Hence, there are numerous places in which one can to 5 | contribute to the IDA-minsc plugin. Some of these places can be the 6 | documentation, the plugin itself, writing tutorials, blogposts, adding to 7 | the project wiki, helping with user issues, or even idling on the irc 8 | channel. 9 | 10 | ## Issues 11 | 12 | If you're unsure of how to do something or the documentation isn't clear, 13 | feel free to create an issue and someone will try and help you figure it 14 | out, or the documentation can be updated with your concern. 15 | 16 | If you find yourself constantly typing something and discovering it not 17 | being there, or if you desire a shorter alias for some common namespace 18 | that you use, feel free to create an issue requesting an alias be made. 19 | This can then be discussed and possibly merged if it doesn't conflict 20 | with something in the future and it's (of course) reasonable. 21 | 22 | ### Reporting an issue 23 | 24 | Before reporting an issue, search the current issue tracker to see if 25 | someone else has already reported your issue. If it is related to 26 | another issue, feel free to reference that in your new issue's details. 27 | Any duplicate issues will be re-referenced and closed so that related 28 | discussion can occur in the original issue that was reported. 29 | 30 | When creating your issue be sure to include the version of IDA you're using, 31 | the platform it's running on (32-bit or 64-bit), the architecture of your 32 | database, and whatever code you're running that doesn't appear to work. 33 | 34 | ## Pull requests 35 | 36 | When submitting a pull request, please rebase it against `master` before 37 | submitting. If you are creating a fix for a particular issue, please name 38 | the branch according to the issue number as `GH-#`. This causes GitHub 39 | to automatically create a reference which allows for simple correlation 40 | of PRs to the issues that they are for. 41 | 42 | When submitting a pull request, keep in mind about what version of IDA 43 | your PR doesn't work on. This plugin aims to be backwards compatible 44 | with IDA all the way back to 6.9. This is right before the `idaapi` 45 | module was split up into its current state. If your addition does not 46 | support one of these versions, please make sure you mention it in 47 | the details of the PR you're trying to get merged upstream so a 48 | committer can come up with a solution or workaround if possible. 49 | 50 | Another thing to pay attention to is which module (or context) your 51 | addition is for. If your addition warrants the creation of a new base 52 | module, please create a separate PR with details about why you feel 53 | that a new module is necessary along with a reference to the PR that 54 | includes your addition. This way the creation of new modules can be 55 | discussed or possibly staged inside a different path before your new 56 | functionality gets added to them. 57 | 58 | The last thing to note is keep in mind how multicase functions work. 59 | If you're not sure of what these are, please refer to the documentation. 60 | When you are adding a new function, try and consider what other types 61 | your function might be able to take, and the different ways the plugin 62 | can automatically determine those types using IDA. 63 | 64 | ### Pull requests in progress 65 | 66 | If you're still working on a pull request, you can prefix the title of 67 | your pull request with "[WIP]". This will notify any user with commit 68 | access that your PR is still in development. When you're ready for it 69 | to be merged, remove the prefix or mention it in the discussion so 70 | that way it can be reviewed before it gets merged. 71 | 72 | ### Titling pull requests 73 | 74 | When titling the pull request, prefix it with the path that the user 75 | needs to type at the IDAPython prompt in order to navigate to the 76 | module that contains your addition. If the addition is within a file 77 | in the root directory, include the full filename. 78 | 79 | As an example, the `base/database.py` module might have a title of: 80 | 81 | database: added blah blah function 82 | 83 | Another example, say for `idapythonrc.py` could be: 84 | 85 | idapythonrc.py: added support for external plugins 86 | 87 | This way it is immediately apparent to the committer what component 88 | of IDA the addition is for. 89 | 90 | ### Pull requests for documentation 91 | 92 | The `docs` branch contains the current state of documentation for this 93 | plugin. Each module's documentation is actually generated by the 94 | `docs/docparse.py` file based on some magic decorators that are used 95 | to communicate semantics to the document parser. 96 | 97 | This means other than the static documentation in `docs/static`, the 98 | documentation for each function and its parameters will need to be 99 | modified within the module that actually contains it. 100 | 101 | If something on there is poorly documented or is missing for some 102 | reason, please prefix your PR title with "docs/" and follow the same 103 | method as described in "Titling pull requests". If you've updated the 104 | documentation for "matching.rst", then title it like: 105 | 106 | docs/matching: fixed some really bad english 107 | 108 | Or if it's some documentation in a particular module such as 109 | `base/instruction.py` one can use: 110 | 111 | docs/instruction: fixed the spacing for the module autodocs 112 | 113 | The reason being is that any modifications to the documentation's 114 | autodocs will need to be backported into the `master` branch. This 115 | "docs/" prefix will notify the primary committer that this needs to 116 | happen. 117 | 118 | ## Requested work 119 | 120 | The following pull requests are desired: 121 | 122 | * Pull requests that introduce support for new architectures (registers 123 | or operands) 124 | * Pull requests involving the decompiler (I don't really use it, thus 125 | I haven't found a reason to write wrappers around its functionality) 126 | * Bug fixes (A lot of refactoring was done to get this plugin to a 127 | release state. Some stuff might be broken, but who knows) 128 | * Documentation issues or especially translation to other languages 129 | * Fixes for any of the issues that are listed in GitHub's issue tracker 130 | 131 | ## Conclusion 132 | 133 | Thanks for your interest in contributing to this plugin! 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2022 Ali Rizvi-Santiago. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the name of the copyright holder nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 17 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 18 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDA-minsc 2 | 3 | 4 | 5 | 8 | 18 | 19 |
6 | 7 | 9 |
    10 |
  • Website: https://github.com/arizvisa/ida-minsc
  • 11 |
  • Documentation: https://arizvisa.github.io/ida-minsc
  • 12 |
  • Mantras: https://github.com/arizvisa/ida-minsc/wiki/Mantras
  • 13 |
  • Wiki: https://github.com/arizvisa/ida-minsc/wiki
  • 14 |
  • Changelog: https://github.com/arizvisa/ida-minsc/wiki/Changelog
  • 15 |
  • IRC: #eof on EFnet
  • 16 |
17 |
20 | 21 | ## General 22 | 23 | IDA-minsc is a plugin for IDA Pro that assists a user with scripting the 24 | IDAPython plugin that is bundled with the disassembler. This plugin groups the 25 | different aspects of the IDAPython API into a simpler format which allows a 26 | reverse engineer to script different aspects of their work with very little 27 | investment. 28 | 29 | A number of concepts are introduced such as a tagging system, support for 30 | multicased functions, and filtering with the intention that most search 31 | and annotation issues can be performed with just a few lines of code. This 32 | should enable a user to write quick, hacky, temporary code that can be used 33 | to augment their reversing endeavors without distraction. 34 | 35 | ## Installation 36 | 37 | Installation should be pretty simple and requires simply cloning the repository 38 | directly into the user's IDA user directory. On the Windows platform, this is 39 | typically located at `%APPDATA%/Hex-Rays/IDA Pro`. Whereas on the Linux 40 | platform this can be found at `$HOME/.idapro`. This contents of this repository 41 | should actually replace that directory. If you have any files that presently 42 | reside there, simply move them into the repository's directory. After 43 | installation, IDA Pro should load its IDAPython plugin which should result in 44 | the `idapythonrc.py` belonging to IDA-minsc being executed which will then 45 | replace IDAPython's default namespace with the one belonging to the plugin's. 46 | 47 | To clone the repository in a directory `$TARGET`, one can simply do: 48 | 49 | $ git clone https://github.com/arizvisa/ida-minsc "$TARGET" 50 | 51 | After cloning the repository, the user will need to install its required Python 52 | dependencies into their site-packages. This can be done using `pip` which is a 53 | tool that is bundled with Python. The file that contains the user's requirements 54 | is in the root of the repository as `requirements.txt`. 55 | 56 | To install the required Python dependencies, one can run `pip` as so: 57 | 58 | $ pip install -r "requirements.txt" 59 | 60 | At this point when the user starts IDA Pro, IDA-minsc will replace IDAPython's 61 | namespace with its own at which point can be used immediately. To verify that 62 | IDA-minsc was installed properly, one can simply type in the following at the 63 | IDAPython prompt: 64 | 65 | > database.config.version() 66 | 67 | This should then return the number `0` since no database has been loaded. 68 | 69 | ## Quick Start 70 | 71 | After installing the python dependencies, you can do something like the 72 | following to list all the functions in your database: 73 | 74 | > database.functions.list() 75 | 76 | Or to iterate through all the functions in the database, you can try: 77 | 78 | > for ea in database.functions(): 79 | print(hex(ea)) 80 | 81 | Please refer to the documentation for more details on what this plugin makes 82 | available to you. 83 | 84 | ## Documentation 85 | 86 | Comprehensive documentation is available at the project page on 87 | [github.io](https://arizvisa.github.io/ida-minsc), or can be built locally via 88 | the "[docs](https://github.com/arizvisa/ida-minsc/tree/docs)" branch. 89 | 90 | If the user wishes to build documentation for local use, they will first need 91 | to install the [Sphinx](http://www.sphinx-doc.org/en/master/usage/installation.html) 92 | package. Afterwards, the entirety of the documentation resides within in the 93 | "[docs](https://github.com/arizvisa/ida-minsc/tree/docs)" branch. Simply 94 | checkout the branch, change the directory to "docs", and then run GNU make as: 95 | 96 | $ make html 97 | 98 | This will result in the build system parsing the available modules and then 99 | rendering all of the documentation into the `_build` directory relative to the 100 | `docs/Makefile`. Documentation can be generated for a number of different 101 | formats. To list all of the available formats, type in `make help` at the 102 | command prompt. 103 | 104 | ## Contributing 105 | 106 | See [CONTRIBUTING.md](https://github.com/arizvisa/ida-minsc/blob/master/CONTRIBUTING.md) 107 | for best practices on reporting issues or for adding functionality to this 108 | project. 109 | 110 | ## Thanks 111 | 112 | Thanks to a number of anonymous and non-anonymous people whom have helped with 113 | the development of this plugin over all of these years. 114 | 115 | [logo]: http://arizvisa.github.io/ida-minsc/_images/hamster.svg 116 | -------------------------------------------------------------------------------- /__root__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Root module 3 | 4 | This module contains the root namespace that IDA starts up with. Any 5 | thing defined within this module is used to replace the globals that 6 | IDA starts up with. 7 | 8 | This module also is responsible for assigning the default hooks in 9 | order to trap what the user is doing to perform any extra maintenance 10 | that needs to occur. 11 | """ 12 | 13 | ### ida-python specific modules 14 | import idaapi, ida 15 | 16 | ### Pre-populate the root namespace with a bunch of things that IDA requires 17 | 18 | ## IDA 6.9 requires the _idaapi module exists in the global namespace 19 | if idaapi.__version__ <= 6.9: 20 | import _idaapi 21 | 22 | ## IDA 6.95 requires these couple modules to exist in the global namespace 23 | if idaapi.__version__ >= 6.95: 24 | import ida_idaapi, ida_kernwin, ida_diskio 25 | 26 | ## IDA 7.4 requires that this module exists in the global namespace 27 | if idaapi.__version__ >= 7.4: 28 | import sys 29 | 30 | ### customize the root namespace 31 | import segment, database, function, instruction 32 | import structure, enumeration, ui 33 | 34 | ## some aliases for the base modules 35 | import database as db 36 | import function as func 37 | import instruction as ins 38 | import structure as struc 39 | import enumeration as enum 40 | import segment as seg 41 | 42 | ## default log setting for notifying the user 43 | # FIXME: actually use the logging module properly instead of assuming 44 | # control of the root logger. 45 | #__import__('logging').root.setLevel(__import__('logging').INFO) 46 | 47 | ## shortcuts 48 | h, top, go, goof = database.h, func.top, database.go, database.go_offset 49 | 50 | def hex(): 51 | import sys, builtins, operator 52 | version = sys.version_info.major 53 | F = operator.methodcaller('encode', 'hex') if version < 3 else operator.methodcaller('hex') 54 | integer_t = int, getattr(builtins, 'long', int) 55 | def render(item): 56 | return "{:x}".format(item) if isinstance(item, integer_t) else F(bytes(bytearray(item))) 57 | return render 58 | hex = hex() 59 | 60 | ## other useful things that we can grab from other modules 61 | 62 | # stuff for printing (of course) 63 | p = __import__('six').print_ 64 | pp, pf = pprint, pformat = [getattr(__import__('pprint'), _) for _ in ['pprint', 'pformat']] 65 | 66 | # snag the custom exceptions that we use while excluding any modules 67 | exceptions = __import__('internal').exceptions 68 | 69 | # snag the fake utilities module to share some things with the user... 70 | utils = __import__('internal').utils 71 | 72 | # construct some pattern matching types 73 | AnyRegister = utils.PatternAnyType(__import__('internal').interface.register_t) 74 | AnyInteger = utils.PatternAnyType(__import__('six').integer_types) 75 | AnyString = utils.PatternAnyType(__import__('six').string_types) 76 | AnyBytes = utils.PatternAnyType(bytes) 77 | Any = utils.PatternAny() 78 | 79 | # some types that the user might want to compare with 80 | architecture_t, register_t, symbol_t, bounds_t, location_t = (getattr(__import__('internal').interface, item) for item in ['architecture_t', 'register_t', 'symbol_t', 'bounds_t', 'location_t']) 81 | ref_t, opref_t = (getattr(__import__('internal').interface, item) for item in ['ref_t', 'opref_t']) 82 | -------------------------------------------------------------------------------- /application/delphi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Delphi module 3 | 4 | This module exposes some basic tools for working with a database built against 5 | a delphi target. These tools are simple things that can help with automating 6 | the creation of strings or other types of data structures that one may find. 7 | """ 8 | 9 | import functools, itertools, types, builtins, operator, six 10 | import database as db, function as func, instruction as ins, structure as struc 11 | 12 | import logging, string 13 | from internal import utils 14 | 15 | def string(ea): 16 | '''Convert the string defined by IDA at the address `ea` into a delphi-style string and return its length.''' 17 | if db.get.i.uint32_t(ea - 8) == 0xffffffff: 18 | db.set.undefined(ea) 19 | db.set.integer.dword(ea - 8) 20 | cb = db.set.string(ea - 4, type=idaapi.STRTYPE_LEN4) 21 | try: 22 | al = db.set.align(ea - 4 + cb, alignment=8) 23 | except TypeError: 24 | al = db.set.align(ea - 4 + cb, alignment=4) 25 | return 4 + cb + al 26 | logging.warning(u"{:s}.string({:#x}): The data at address {:#x} is not a properly prefixed delphi string.".format(__name__, ea, ea - 8)) 27 | return 0 28 | -------------------------------------------------------------------------------- /application/quicktime.py: -------------------------------------------------------------------------------- 1 | '''QuickTime stuff''' 2 | 3 | EXPORT = [ 'nameDispatch', 'nameAllDispatches' ] 4 | 5 | import idc,idautils 6 | import function,database 7 | 8 | from . import __quicktime 9 | 10 | def nextMnemonic(ea, mnem, maxaddr=0xc0*0x1000000): 11 | res = idc.print_insn_mnem(ea) 12 | if res == "": return idc.BADADDR 13 | if res == mnem: return ea 14 | return nextMnemonic( idc.next_head(ea, maxaddr), mnem, maxaddr ) 15 | 16 | def prevMnemonic(ea, mnem, minaddr=0): 17 | res = idc.print_insn_mnem(ea) 18 | #print("%x -> %s"% (ea, res)) 19 | if res == "": return idc.BADADDR 20 | if res == mnem: return ea 21 | return prevMnemonic( idc.prev_head(ea, minaddr), mnem, minaddr ) 22 | 23 | def getMinorDispatchTableAddress(ea): 24 | """find address of last lea in function""" 25 | start = idc.get_func_attr(ea, idc.FUNCATTR_START) 26 | end = idc.prev_head( idc.get_func_attr(ea, idc.FUNCATTR_END), start) 27 | res = prevMnemonic(end, 'lea', start) 28 | assert res != idc.BADADDR 29 | return idc.get_operand_value(res, 1) 30 | 31 | def getMajorDispatchTableAddress(): 32 | """find quicktime major dispatch table""" 33 | res = idc.get_name_ea_simple('theQuickTimeDispatcher') 34 | res = nextMnemonic(res, 'lea', idc.get_func_attr(res, idc.FUNCATTR_END)) 35 | assert res != idc.BADADDR 36 | return idc.get_operand_value(res, 1) 37 | 38 | def resolveDispatcher(code): 39 | major = (code & 0x00ff0000) >> 0x10 40 | minor = code & 0xff00ffff 41 | 42 | res = getMajorDispatchTableAddress() + major*8 43 | majorFlag = idc.get_wide_dword(res) 44 | majorAddress = idc.get_wide_dword(res+4) 45 | if majorFlag != 0: 46 | return majorAddress + (minor*0x10) 47 | 48 | #print("%x"% getMinorDispatchTableAddress(majorAddress)) 49 | #print("resolved by 0x%x(%x)"% (majorAddress, minor)) 50 | return majorAddress 51 | 52 | def getDispatchCode(ea): 53 | # get dispatch code out of an instruction 54 | first, second = (idc.print_operand(ea, 0), idc.get_operand_value(ea, 1)) 55 | if first == 'eax': 56 | return second 57 | raise ValueError("Search resulted in address %08x, but instruction '%s' does fulfill requested constraints"% (ea, idc.print_insn_mnem(ea))) 58 | 59 | def FindLastAssignment(ea, register): 60 | start,end = database.guessrange(ea) 61 | while ea > start: 62 | ea = database.prev(ea) 63 | m = idc.print_insn_mnem(ea) 64 | r = idc.print_operand(ea, 0) 65 | 66 | if m == 'mov' and r == register: 67 | return ea 68 | continue 69 | 70 | raise ValueError('FindLastAssignment(0x%x, %s) Found no matches'% (ea, register)) 71 | 72 | def nameDispatch(address): 73 | '''Name the dispatch function at the specified address in quicktime.qts''' 74 | try: 75 | start, end = function.range(address) 76 | 77 | except ValueError: 78 | print('%x making a function'% address) 79 | function.make(address) 80 | start, end = function.range(address) 81 | 82 | try: 83 | ea = FindLastAssignment(address, 'eax') 84 | code = getDispatchCode(ea) 85 | except ValueError: 86 | print('%08x - Unable to find dispatch code'% address) 87 | return 88 | 89 | ofs = database.getoffset(start) 90 | function.setName(start, 'dispatch_%08x_%x'% (code, ofs)) 91 | function.tag(start, 'code', hex(code)) 92 | function.tag(start, 'group', 'dispatch') 93 | try: 94 | function.tag(start, 'realname', __quicktime.qt_fv_list[code]) 95 | except KeyError: 96 | pass 97 | 98 | try: 99 | function.tag(start, 'address', hex(resolveDispatcher(code)), repeatable=True) 100 | except: 101 | pass 102 | 103 | def nameAllDispatches(ea): 104 | '''Using the address of {theQuickTimeDispatcher}, name and tag all discovered dispatch calls in quicktime.qts''' 105 | for address in idautils.DataRefsTo(ea): 106 | nameDispatch(address) 107 | return 108 | -------------------------------------------------------------------------------- /application/windbg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Windbg module 3 | 4 | This module exposes some basic tools for assistance when interacting with 5 | windbg. This can be things that help with importing/exporting data from/to 6 | windbg, or tools for generating quotes strings to paste into windbg. 7 | 8 | One utility within this module will allow one to escape all the quotes 9 | in a string of a particular depth. This can be used to help generating 10 | string that can be pasted into a conditional breakpoint, or to pass a 11 | properly quoted format-string as an argument to the `.printf` token. 12 | """ 13 | 14 | import functools, itertools, types, builtins, operator, six 15 | import database as db, function as func, instruction as ins, structure as struc 16 | 17 | import logging, string 18 | from internal import utils 19 | 20 | def reference(ea, **module): 21 | """Return a reference containing the module and offset of the address `ea`. 22 | 23 | If the string `module` is specified, then use it as the module name instead of the database filename. 24 | """ 25 | module = module.get('module', db.filename()) 26 | return '{:s}+{:x}'.format(module.replace(' ', ''), db.offset(ea)) 27 | 28 | def label(ea): 29 | '''Return a label for the given address `ea`.''' 30 | try: res = '{:s}{{+{:x}}}'.format(func.name(ea), db.offset(ea)) 31 | except: res = '+{:x}'.format(db.offset(ea)) 32 | return '{:s}!{:s}'.format(db.module(), res) 33 | 34 | def tokenize(input, escapables={"'", '"', '\\'} | {item for item in string.whitespace} - {' '}): 35 | """Yield each token belonging to the windbg format in `input` that would need to be escaped using the specified `escapables`. 36 | 37 | If the set `escapables` is defined, then use it as the list of characters to tokenize. 38 | """ 39 | result, iterable = '', (item for item in input) 40 | try: 41 | while True: 42 | char = builtins.next(iterable) 43 | if operator.contains(escapables, char): 44 | if result: 45 | yield result 46 | yield char 47 | result = '' 48 | 49 | else: 50 | result += char 51 | continue 52 | 53 | except StopIteration: 54 | if result: 55 | yield result 56 | return 57 | return 58 | 59 | def escape(input, depth=0, **extra): 60 | """ 61 | Given the windbg format string in `input`, escape each of its characters as if it was within `depth` level of quoting. 62 | 63 | If any other keyword parameters are provided, then escape those keywords with their value. 64 | """ 65 | 66 | # Define the generator form of ourself as we're just going to aggregate its 67 | # result into a string anyways. 68 | def closure(input, depth, extra): 69 | bs = '\\' 70 | ws = { six.unichr(i) : item for i, item in enumerate('0123456abtnvfr') } 71 | escaped = {bs, '"'} 72 | 73 | # Now we can tokenize our input and figure out what it's supposed to yield 74 | for token in tokenize(input): 75 | 76 | # Check if the token is a windbg quote that needs to be explicitly 77 | # escaped 78 | if operator.contains(escaped, token): 79 | yield bs * depth + token 80 | 81 | # Did the token match one of our whitespace characters? 82 | elif operator.contains(ws, token): 83 | yield bs + (bs * depth) + ws[token] 84 | 85 | # If nothing matched, then we can just yield the current character 86 | # as it was unprocessed. 87 | else: 88 | # Add any characters that were explicitly specified in the 89 | # "extra" dictionary, and replace our current token with it 90 | if any(operator.contains(token, item) for item in extra): 91 | k = builtins.next(item for item in extra if operator.contains(token, item)) 92 | token = token.replace(k, extra[k] * depth + k) 93 | 94 | # Now we can yield our token 95 | yield token 96 | continue 97 | return 98 | return str().join(closure(input, depth, extra)) 99 | 100 | def breakpoints(f=None, **kwargs): 101 | """Query the function `f` for the "break" tag, and use it to emit a list of breakpoints for windbg. 102 | 103 | If the string `module` is specified, then use it instead of the current filename when emitting the location. 104 | If the string `tagname` is provided, then use it to query instead of "break". 105 | """ 106 | tagname = kwargs.get('tagname', 'break') 107 | 108 | # if no function was provided, then recurse into ourself for all of them 109 | if f is None: 110 | for f, _ in db.selectcontents(tagname): 111 | breakpoints(f, **kwargs) 112 | return 113 | 114 | #entry, exit = func.top(f), func.bottom(f) 115 | #funcname = func.name(f) 116 | #[(entry,{tagname:'.printf "Entering {:s} %x,%x\\n",poi(@esp),@esp'.format(funcname)})], [(x,{tagname:'.printf "Exiting {:s} %x,%x\\n",poi(@esp),@esp'.format(funcname)}) for x in exit], 117 | 118 | # query the given function for the requested tagname 119 | tags, select = {}, itertools.chain(func.select(f, And=(tagname,), Or=('',))) 120 | for ea, t in select: 121 | h = tags.setdefault(ea, {}) 122 | for k in t.keys(): 123 | if k == tagname: 124 | h.setdefault(k, []).extend(t[k] if isinstance(t[k], builtins.list) else t[k].split(';')) 125 | 126 | elif operator.contains(h, k): 127 | logging.warning(u"{:s}.breakpoints({:#x}{:s}) : The specified key \"{:s}\" already exists in dictionary for address {:#x}.".format(__name__, func.addr(f), u", {:s}".format(utils.strings.kwargs(kwargs)) if kwargs else '', utils.string.escape(k, '"'), ea)) 128 | 129 | else: 130 | h[k] = t[k] 131 | continue 132 | continue 133 | 134 | # aggregate all of the discovered tags into a list of breakpoints 135 | for ea, t in tags.items(): 136 | ofs, commands = db.offset(ea), [] 137 | 138 | # create the command that emits the current label 139 | label_t = string.Template(r'.printf "$label -- $note\n"' if operator.contains(t, '') else r'.printf "$label\n"') 140 | commands.append(label_t.safe_substitute(label=label(ea), note=t.get('', ''))) 141 | 142 | # append the commands to execute when encountering the given breakpoint 143 | res = t.get(tagname, ['g']) 144 | if isinstance(res, builtins.list): 145 | commands.extend(res) 146 | else: 147 | commands.append(res) 148 | 149 | # escape all of the commands since we're going to join them together 150 | commands = (escape(cmd) for cmd in commands) 151 | 152 | parameters = next((kwargs[kw] for kw in ['parameters', 'params', 'param'] if kw in kwargs), '') 153 | six.print_('bp {:s}{:s} "{:s}"'.format("{:s} ".format(parameters) if parameters else '', reference(ea, **kwargs), escape(';'.join(commands), depth=1))) 154 | return 155 | -------------------------------------------------------------------------------- /base/_declaration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Declaration module (internal) 3 | 4 | This module contains some tools used for extracting information out of 5 | function and type declarations. 6 | 7 | TODO: Implement parsers for some of the C++ symbol manglers in order to 8 | query them for specific attributes or type information. 9 | """ 10 | 11 | import internal, idaapi 12 | import string as _string 13 | 14 | ### c declaration stuff 15 | def function(ea): 16 | '''Returns the C function declaration at the address `ea`.''' 17 | res = idaapi.idc_get_type(ea) 18 | if res is None: 19 | raise internal.exceptions.MissingTypeOrAttribute(u"The function {:x} does not have a declaration.".format(ea)) 20 | return res 21 | 22 | def arguments(ea): 23 | '''Returns an array of all of the arguments within the prototype of the function at `ea`.''' 24 | decl = function(ea) 25 | args = decl[ decl.index('(') + 1 : decl.rindex(')') ] 26 | return [ arg.strip() for arg in args.split(',')] 27 | 28 | def size(string): 29 | '''Returns the size of a type described by a C declaration in `string`.''' 30 | til = idaapi.cvar.idati if idaapi.__version__ < 7.0 else idaapi.get_idati() 31 | 32 | string = string.strip() 33 | if string.lower() == 'void': 34 | return 0 35 | elif string.startswith('class') and string.endswith('&'): 36 | res = idaapi.idc_parse_decl(til, 'void*;', 0) 37 | else: 38 | semicoloned = string if string.endswith(';') else "{:s};".format(string) 39 | res = idaapi.idc_parse_decl(til, internal.utils.string.to(semicoloned), 0) 40 | 41 | if res is None: 42 | raise internal.exceptions.DisassemblerError(u"Unable to parse the specified C declaration (\"{:s}\").".format(internal.utils.string.escape(string, '"'))) 43 | _, type, _ = res 44 | f = idaapi.get_type_size0 if idaapi.__version__ < 6.8 else idaapi.calc_type_size 45 | return f(til, type) 46 | 47 | @internal.utils.string.decorate_arguments('string') 48 | def demangle(string): 49 | '''Given a mangled C++ `string`, demangle it back into a human-readable symbol.''' 50 | if idaapi.__version__ < 7.0: 51 | res = idaapi.demangle_name(internal.utils.string.to(string), idaapi.cvar.inf.long_demnames) 52 | else: 53 | res = idaapi.demangle_name(internal.utils.string.to(string), idaapi.cvar.inf.long_demnames, idaapi.DQT_FULL) 54 | return string if res is None else internal.utils.string.of(res) 55 | 56 | def mangledQ(string): 57 | '''Return true if the provided `string` has been mangled.''' 58 | return any(string.startswith(item) for item in ['?', '__']) 59 | 60 | @internal.utils.string.decorate_arguments('info') 61 | def parse(info): 62 | '''Parse the string `info` into an ``idaapi.tinfo_t``.''' 63 | if idaapi.__version__ < 7.0: 64 | til, ti = idaapi.cvar.idati, idaapi.tinfo_t(), 65 | else: 66 | til, ti = idaapi.get_idati(), idaapi.tinfo_t(), 67 | 68 | # Convert info to a string if it's a tinfo_t 69 | info_s = "{!s}".format(info) if isinstance(info, idaapi.tinfo_t) else info 70 | 71 | # Firstly we need to ';'-terminate the type the user provided in order 72 | # for IDA's parser to understand it. 73 | terminated = info_s if info_s.endswith(';') else "{:s};".format(info_s) 74 | 75 | # Ask IDA to parse this into a tinfo_t for us. We pass the silent flag so 76 | # that we're responsible for raising an exception if there's a parsing 77 | # error of some sort. If it succeeds, then we can return our typeinfo. 78 | # Otherwise we return None because of the inability to parse it. 79 | if idaapi.__version__ < 6.9: 80 | return None if idaapi.parse_decl2(til, terminated, None, ti, idaapi.PT_SIL) is None else ti 81 | elif idaapi.__version__ < 7.0: 82 | return None if idaapi.parse_decl2(til, terminated, ti, idaapi.PT_SIL) is None else ti 83 | return None if idaapi.parse_decl(ti, til, terminated, idaapi.PT_SIL) is None else ti 84 | 85 | def string(ti): 86 | prefix = '' 87 | name, indent = '', 4 88 | cmt, cindent = '', 4 89 | flags = idaapi.PRTYPE_DEF | idaapi.PRTYPE_MULTI 90 | return idaapi.print_tinfo(prefix, indent, cindent, flags, ti, name, cmt) 91 | 92 | def unmangle_name(name): 93 | '''Return the function name from a prototype to be used for rendered an ``idaapi.tino_t``.''' 94 | 95 | # Check to see if our name is demangled. If not, then we can just return it. 96 | demangled = demangle(name) 97 | if not name or demangled == name: 98 | return demangled 99 | 100 | # If so, then we need to do some trickery to extract the name. 101 | has_parameters = any(item in demangled for item in '()') 102 | noparameters = demangled[:demangled.find('(')] if has_parameters else demangled 103 | 104 | # Strip out all templates 105 | notemplates, count = '', 0 106 | for item in noparameters: 107 | if item in '<>': 108 | count += +1 if item in '<' else -1 109 | elif count == 0: 110 | notemplates += item 111 | continue 112 | 113 | # Now we need to remove the calling convention as it should be in the typeinfo. 114 | items = notemplates.split(' ') 115 | conventions = {'__cdecl', '__stdcall', '__fastcall', '__thiscall', '__pascal', '__usercall', '__userpurge'} 116 | try: 117 | ccindex = next(idx for idx, item in enumerate(items) if any(item.endswith(cc) for cc in conventions)) 118 | items = items[1 + ccindex:] 119 | 120 | # We couldn't find a calling convention, so there's no real work to do. 121 | except StopIteration: 122 | items = items[:] 123 | 124 | # Strip out any backticked components, operators, and other weirdness. 125 | foperatorQ = lambda string: string.startswith('operator') and any(string.endswith(invalid) for invalid in _string.punctuation) 126 | joined = ' '.join(items) 127 | if '::' in joined: 128 | components = joined.split('::') 129 | components = (item for item in components if not item.startswith('`')) 130 | components = ('operator' if foperatorQ(item) else item for item in components) 131 | joined = '::'.join(components) 132 | 133 | # Check to see if this is some operator of some kind. 134 | if joined.count(' ') > 0 and joined.rsplit(' ', 2)[-2].endswith('operator'): 135 | return '_'.join(joined.rsplit(' ', 2)[-2:]) 136 | 137 | # Now we can drop everything before the last space, and then return it. 138 | return joined.rsplit(' ', 1)[-1] 139 | 140 | def unmangle_arguments(ea, info): 141 | if not info.present(): 142 | raise ValueError(info) 143 | 144 | # Grab the parameters from the idc type as it includes more information 145 | parameters = extract.arguments("{!s}".format(idaapi.idc_get_type(ea))) or extract.arguments("{!s}".format(info)) 146 | param_s = parameters.lstrip('(').rstrip(')') 147 | 148 | index, indices, iterable = 0, [], ((idx, item) for idx, item in enumerate(param_s)) 149 | for argi in range(info.get_nargs()): 150 | arg = info.get_nth_arg(argi) 151 | arg_s = "{!s}".format(arg) 152 | 153 | index, ch = next(iterable, (1 + index, ',')) 154 | while ch in ' ': 155 | index, ch = next(iterable) 156 | 157 | while ch != ',': 158 | for item in arg_s: 159 | index, ch = next(iterable) 160 | if ch != item: break 161 | 162 | count = 0 163 | while ch != ',' or count > 0: 164 | index, ch = next(iterable, (1 + index, ',')) 165 | if ch in '()': 166 | count += -1 if ch in ')' else +1 167 | continue 168 | 169 | indices.append(index) 170 | 171 | pos, res = 0, [] 172 | for argi, index in enumerate(indices): 173 | arg = info.get_nth_arg(argi) 174 | arg_s = "{!s}".format(arg) 175 | 176 | item = param_s[pos : index].strip() 177 | pos = 1 + index 178 | 179 | t, name = item[:len(arg_s)], item[len(arg_s):] 180 | res.append((t.strip(), name.strip())) 181 | return res 182 | 183 | ## examples to test below code with 184 | #"??_U@YAPAXI@Z" 185 | #"?_BADOFF_func@std@@YAABJXZ" 186 | #"??$_Div@N@?$_Complex_base@NU_C_double_complex@@@std@@IAEXABV?$complex@N@1@@Z" 187 | #"??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@PBX@Z" 188 | #"??1?$basic_ostream@DU?$char_traits@D@std@@@std@@UAE@XZ" 189 | #"??_F?$basic_stringstream@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAEXXZ" 190 | #"??1type_info@@UAE@XZ" 191 | #"sub_784B543B" 192 | #"?_Atexit@@YAXP6AXXZ@Z" 193 | #"?__ArrayUnwind@@YGXPAXIHP6EX0@Z@Z" 194 | 195 | # FIXME: this code is so hacky, that i need unit-tests for it...which should be properly fixed. 196 | # 1] If I write a parser, I can easily split out these components. (proper fix) 197 | # 2] If I use IDA's metadata to figure out each type, I can use those strings to cull them out of the declaration. (hackish) 198 | # 3] I could use completely unmaintainable nfa-based pattern matching. (regexes whee) 199 | # 4] I could continue to use string operations to cut parts out...except that they're unable to solve this problem 200 | # due to the need to keep a recursive state somewhere in order to associate types with. (current) 201 | class extract: 202 | @staticmethod 203 | def declaration(string): 204 | return demangle(string) 205 | 206 | @staticmethod 207 | def convention(string): 208 | types = {'__cdecl', '__stdcall', '__thiscall', '__fastcall'} 209 | res = string.split(' ') 210 | return res[0] 211 | 212 | @staticmethod 213 | def fullname(string): 214 | decl = extract.declaration(string) 215 | return decl[:decl.find('(')].split(' ', 3)[-1] if any(item in decl for item in ['(', ' ']) else decl 216 | 217 | @staticmethod 218 | def name(string): 219 | fn = extract.fullname(string) 220 | return fn.rsplit(':', 2)[-1] if ':' in fn else fn 221 | 222 | @staticmethod 223 | def arguments(string): 224 | res, count = '', 0 225 | for item in string[::-1]: 226 | if item in '()': 227 | count += +1 if item in ')' else -1 228 | res += item 229 | elif count > 0: 230 | res += item 231 | elif count == 0: 232 | break 233 | continue 234 | return str().join(reversed(res)) 235 | 236 | @staticmethod 237 | def result(string): 238 | decl = extract.declaration(string) 239 | decl = decl[:decl.find('(')].rsplit(' ', 1)[0] 240 | return decl.split(':', 1)[1].strip() if ':' in decl else decl.strip() 241 | 242 | @staticmethod 243 | def scope(string): 244 | decl = extract.declaration(string) 245 | decl = decl[:decl.find('(')].rsplit(' ', 1)[0] 246 | return decl.split(':', 1)[0].strip() if ':' in decl else '' 247 | -------------------------------------------------------------------------------- /base/_exceptions.py: -------------------------------------------------------------------------------- 1 | import sys, builtins as E 2 | 3 | class UnicodeException(E.BaseException): 4 | """ 5 | A base exception that handles converting a unicode message 6 | into its UTF-8 form so that it can be emitted using Python's 7 | standard console. 8 | 9 | Copied from Python 2.7.15 implementation. 10 | """ 11 | # tp_init 12 | def __init__(self, *args): 13 | self.__args__ = args 14 | self.__message__ = args[0] if len(args) == 1 else '' 15 | 16 | # Python2 can be emitted in more than one way which requires us 17 | # to implement both the Exception.__str__ and Exception.__unicode__ 18 | # methods. If returning a regular string (bytes), then we need to 19 | # utf-8 encode the result because IDA's console will automatically 20 | # decode it. 21 | if sys.version_info.major < 3: 22 | 23 | # tp_str 24 | def __str__(self): 25 | length = len(self.args) 26 | if length == 0: 27 | return "" 28 | elif length == 1: 29 | item = self.args[0] 30 | return str(item.encode('utf8') if isinstance(item, unicode) else item) 31 | return str(self.args) 32 | 33 | def __unicode__(self): 34 | # return unicode(self.__str__()) 35 | length = len(self.args) 36 | if length == 0: 37 | return u"" 38 | elif length == 1: 39 | return unicode(self.args[0]) 40 | return unicode(self.args) 41 | 42 | # Python3 really only requires us to implement this method when 43 | # emitting an exception. This is the same as a unicode type, so 44 | # we should be okay with casting the exception's arguments. 45 | else: 46 | 47 | # tp_str 48 | def __str__(self): 49 | length = len(self.args) 50 | if length == 0: 51 | return "" 52 | elif length == 1: 53 | item = self.args[0] 54 | return str(item) 55 | return str(self.args) 56 | 57 | # tp_repr 58 | def __repr__(self): 59 | repr_suffix = repr(self.args) 60 | name = type(self).__name__ 61 | dot = name.rfind('.') 62 | shortname = name[1 + dot:] if dot > -1 else name 63 | return shortname + repr_suffix 64 | 65 | # tp_as_sequence 66 | def __iter__(self): 67 | for item in self.args: 68 | yield item 69 | return 70 | 71 | # tp_as_sequence 72 | def __getitem__(self, index): 73 | return self.args[index] 74 | def __getslice__(self, *indices): 75 | res = slice(*indices) 76 | return self.args[res] 77 | 78 | # tp_getset 79 | @property 80 | def message(self): 81 | return self.__message__ 82 | @message.setter 83 | def message(self, message): 84 | # self.__message__ = "{!s}".format(message) 85 | self.__message__ = message 86 | @property 87 | def args(self): 88 | return self.__args__ 89 | @args.setter 90 | def args(self, args): 91 | self.__args__ = tuple(item for item in args) 92 | 93 | # tp_methods 94 | def __reduce__(self): 95 | return self.args 96 | def __setstate__(self, pack): 97 | self.args = pack 98 | 99 | class MissingTagError(UnicodeException, E.KeyError): 100 | """ 101 | The requested tag at the specified address does not exist. 102 | """ 103 | 104 | class MissingFunctionTagError(MissingTagError): 105 | """ 106 | The requested tag for the specified function does not exist. 107 | """ 108 | 109 | class MissingMethodError(UnicodeException, E.NotImplementedError): 110 | """ 111 | A method belonging to a superclass that is required to be overloaded was called. 112 | """ 113 | 114 | class MissingNameError(UnicodeException, E.NameError): 115 | """ 116 | A name that was required was found missing and was unable to be recovered. 117 | """ 118 | 119 | class UnsupportedVersion(UnicodeException, E.NotImplementedError): 120 | """ 121 | This functionality is not supported on the current version of IDA. 122 | """ 123 | 124 | class UnsupportedCapability(UnicodeException, E.NotImplementedError, E.EnvironmentError): 125 | """ 126 | An unexpected or unsupported capability was specified. 127 | """ 128 | 129 | class ResultMissingError(UnicodeException, E.LookupError): 130 | """ 131 | The requested item is missing from its results. 132 | """ 133 | 134 | class SearchResultsError(ResultMissingError): 135 | """ 136 | No results were found. 137 | """ 138 | 139 | class DisassemblerError(UnicodeException, E.EnvironmentError): 140 | """ 141 | An api call has thrown an error or was unsuccessful. 142 | """ 143 | 144 | class MissingTypeOrAttribute(UnicodeException, E.TypeError): 145 | """ 146 | The specified location is missing some specific attribute or type. 147 | """ 148 | 149 | class InvalidTypeOrValueError(UnicodeException, E.TypeError, E.ValueError): 150 | """ 151 | An invalid value or type was specified. 152 | """ 153 | 154 | class InvalidParameterError(InvalidTypeOrValueError, E.AssertionError): 155 | """ 156 | An invalid parameter was specified by the user. 157 | """ 158 | 159 | class OutOfBoundsError(UnicodeException, E.ValueError): 160 | """ 161 | The specified item is out of bounds. 162 | """ 163 | 164 | class AddressOutOfBoundsError(OutOfBoundsError, E.ArithmeticError): 165 | """ 166 | The specified address is out of bounds. 167 | """ 168 | 169 | class IndexOutOfBoundsError(OutOfBoundsError, E.IndexError, E.KeyError): 170 | """ 171 | The specified index is out of bounds. 172 | """ 173 | 174 | class ItemNotFoundError(ResultMissingError, E.KeyError): 175 | """ 176 | The specified item or type was not found. 177 | """ 178 | 179 | class FunctionNotFoundError(ItemNotFoundError): 180 | """ 181 | Unable to locate the specified function. 182 | """ 183 | 184 | class AddressNotFoundError(ItemNotFoundError): 185 | """ 186 | Unable to locate the specified address. 187 | """ 188 | 189 | class SegmentNotFoundError(ItemNotFoundError): 190 | """ 191 | Unable to locate the specified segment. 192 | """ 193 | 194 | class StructureNotFoundError(ItemNotFoundError): 195 | """ 196 | Unable to locate the specified structure. 197 | """ 198 | 199 | class EnumerationNotFoundError(ItemNotFoundError): 200 | """ 201 | Unable to locate the specified enumeration. 202 | """ 203 | 204 | class MemberNotFoundError(ItemNotFoundError): 205 | """ 206 | Unable to locate the specified structure or enumeration member. 207 | """ 208 | 209 | class RegisterNotFoundError(ItemNotFoundError): 210 | """ 211 | Unable to locate the specified register. 212 | """ 213 | 214 | class NetNodeNotFoundError(ItemNotFoundError): 215 | """ 216 | Unable to locate the specified netnode. 217 | """ 218 | 219 | class ReadOrWriteError(UnicodeException, E.IOError, E.ValueError): 220 | """ 221 | Unable to read or write the specified number of bytes . 222 | """ 223 | 224 | class InvalidFormatError(UnicodeException, E.KeyError, E.ValueError): 225 | """ 226 | The specified data has an invalid format. 227 | """ 228 | 229 | class SerializationError(UnicodeException, E.ValueError, E.IOError): 230 | """ 231 | There was an error while trying to serialize or deserialize the specified data. 232 | """ 233 | 234 | class SizeMismatchError(SerializationError): 235 | """ 236 | There was an error while trying to serialize or deserialize the specified data due to its size not matching. 237 | """ 238 | 239 | class UnknownPrototypeError(UnicodeException, E.LookupError): 240 | """ 241 | The requested prototype does not match any of the ones that are available. 242 | """ 243 | 244 | class DuplicateItemError(UnicodeException, E.NameError): 245 | """ 246 | The requested command has failed due to a duplicate item. 247 | """ 248 | 249 | #structure:742 and previous to it should output the module name, classname, and method 250 | #comment:334 should catch whatever tree.find raises 251 | #comment:100 (this needs some kind of error when the symbol or token component is not found) 252 | #interface:283, interface:302, interface:620, interface:640 (this should be a NameError) 253 | -------------------------------------------------------------------------------- /base/_netnode.py: -------------------------------------------------------------------------------- 1 | """ 2 | Netnode module (internal) 3 | 4 | This module wraps IDA's netnode API and dumbs it down so that a user 5 | can be mindless when reading/writing/enumerating data out of a netnode. 6 | This is an internal module and is not expected to be used by the user. 7 | """ 8 | 9 | import six, operator 10 | import idaapi 11 | 12 | import internal 13 | 14 | MAXSPECSIZE = idaapi.MAXSTR 15 | MAXNAMESIZE = idaapi.MAXNAMELEN 16 | 17 | class netnode(object): 18 | """ 19 | This namespace is an interface to IDA's netnode api. This aims to provide a 20 | portable way of accessing a netnode between all the different variations and 21 | versions of IDA. 22 | """ 23 | try: 24 | # ida 6.95 splits up their idaapi module into smaller namespaces 25 | import _ida_netnode 26 | except ImportError: 27 | # _ida_netnode has got to be in at least one of these idaapi modules... 28 | import idaapi as _ida_netnode 29 | if not hasattr(idaapi, 'new_netnode'): 30 | import _idaapi as _ida_netnode 31 | 32 | new = get = root = _ida_netnode.new_netnode 33 | delete = _ida_netnode.delete_netnode 34 | start = _ida_netnode.netnode_start 35 | end = _ida_netnode.netnode_end 36 | index = _ida_netnode.netnode_index 37 | kill = _ida_netnode.netnode_kill 38 | 39 | # We need the following api to explicitly look up a netnode by its address. 40 | if hasattr(_ida_netnode, 'exist'): 41 | exist = staticmethod(internal.utils.fcompose(_ida_netnode.new_netnode, _ida_netnode.exist)) 42 | 43 | # If it didn't exist, though, then we need to call directly into the ida library. 44 | # We're fortunate here because netnodes are really just a pointer to an integer, 45 | # and the netnode_exist api takes a pointer and returns a straight-up boolean. 46 | else: 47 | import ctypes, ida 48 | ida.netnode_exist.restype, ida.netnode_exist.argtypes = ctypes.c_bool, [ctypes.POINTER(ctypes.c_long)] 49 | exist = staticmethod(internal.utils.fcompose(ctypes.c_long, ctypes.pointer, ida.netnode_exist)) 50 | 51 | # There's a chance that this api doesn't exist in IDAPython, so if it does then 52 | # we can assign it as-is...otherwise we create a netnode with the desired name 53 | # and then see if we can grab its index to confirm its existence. 54 | if hasattr(_ida_netnode, 'netnode_exist'): 55 | exist_name = _ida_netnode.netnode_exist 56 | else: 57 | exist_name = staticmethod(internal.utils.fcompose(internal.utils.frpartial(_ida_netnode.new_netnode, False, 0), _ida_netnode.netnode_index, internal.utils.fpartial(operator.ne, idaapi.BADADDR))) 58 | 59 | # These apis should always exist and will hopefully never change. 60 | long_value = _ida_netnode.netnode_long_value 61 | next = _ida_netnode.netnode_next 62 | prev = _ida_netnode.netnode_prev 63 | rename = _ida_netnode.netnode_rename 64 | #copyto = _ida_netnode.netnode_copyto 65 | #create = _ida_netnode.netnode_create 66 | #moveto = _ida_netnode.netnode_moveto 67 | set = _ida_netnode.netnode_set 68 | set_long = _ida_netnode.netnode_set_long 69 | delvalue = _ida_netnode.netnode_delvalue 70 | 71 | blobsize = _ida_netnode.netnode_blobsize 72 | getblob = _ida_netnode.netnode_getblob 73 | setblob = _ida_netnode.netnode_setblob 74 | delblob = _ida_netnode.netnode_delblob 75 | 76 | altdel = _ida_netnode.netnode_altdel 77 | altlast = _ida_netnode.netnode_altlast 78 | altprev = _ida_netnode.netnode_altprev 79 | altset = _ida_netnode.netnode_altset 80 | altval = _ida_netnode.netnode_altval 81 | 82 | charlast = _ida_netnode.netnode_charlast 83 | charprev = _ida_netnode.netnode_charprev 84 | chardel = _ida_netnode.netnode_chardel 85 | charset = _ida_netnode.netnode_charset 86 | charval = _ida_netnode.netnode_charval 87 | 88 | hashdel = _ida_netnode.netnode_hashdel 89 | hashlast = _ida_netnode.netnode_hashlast 90 | hashprev = _ida_netnode.netnode_hashprev 91 | hashset = _ida_netnode.netnode_hashset 92 | hashset_buf = _ida_netnode.netnode_hashset_buf 93 | hashset_idx = _ida_netnode.netnode_hashset_idx 94 | hashstr = _ida_netnode.netnode_hashstr 95 | hashstr_buf = _ida_netnode.netnode_hashstr_buf 96 | hashval = _ida_netnode.netnode_hashval 97 | hashval_long = _ida_netnode.netnode_hashval_long 98 | 99 | supdel = _ida_netnode.netnode_supdel 100 | suplast = _ida_netnode.netnode_suplast 101 | supprev = _ida_netnode.netnode_supprev 102 | supset = _ida_netnode.netnode_supset 103 | supstr = _ida_netnode.netnode_supstr 104 | supval = _ida_netnode.netnode_supval 105 | 106 | valobj = _ida_netnode.netnode_valobj 107 | valstr = _ida_netnode.netnode_valstr 108 | value_exists = _ida_netnode.netnode_value_exists 109 | 110 | # now to fix up the version skew as a result of IDA 7.0 111 | if idaapi.__version__ < 7.0: 112 | supfirst = _ida_netnode.netnode_sup1st 113 | supnext = _ida_netnode.netnode_supnxt 114 | hashnext = _ida_netnode.netnode_hashnxt 115 | hashfirst = _ida_netnode.netnode_hash1st 116 | charfirst = _ida_netnode.netnode_char1st 117 | charnext = _ida_netnode.netnode_charnxt 118 | name = _ida_netnode.netnode_name 119 | altfirst = _ida_netnode.netnode_alt1st 120 | altnext = _ida_netnode.netnode_altnxt 121 | 122 | else: # >= 7.0 123 | supfirst = _ida_netnode.netnode_supfirst 124 | supnext = _ida_netnode.netnode_supnext 125 | hashnext = _ida_netnode.netnode_hashnext 126 | hashfirst = _ida_netnode.netnode_hashfirst 127 | charfirst = _ida_netnode.netnode_charfirst 128 | charnext = _ida_netnode.netnode_charnext 129 | name = _ida_netnode.netnode_get_name 130 | altfirst = _ida_netnode.netnode_altfirst 131 | altnext = _ida_netnode.netnode_altnext 132 | 133 | # default tags (older versions of IDA use a char which we'll use as well) 134 | alttag = idaapi.atag 135 | suptag = idaapi.stag 136 | hashtag = idaapi.htag 137 | chartag = b'd' if idaapi.__version__ < 7.0 else 0x64 # found while reversing ida's shared library 138 | 139 | class utils(object): 140 | """ 141 | This namespace provides utilities for interacting with a netnode and each 142 | of the types that it may be composed of. Primarily, these functions allow 143 | one to iterate through the types contained within the netnode. 144 | """ 145 | @classmethod 146 | def get(cls, index): 147 | '''Return the netnode for the provided `index`.''' 148 | return netnode.get(index) 149 | 150 | @classmethod 151 | def range(cls): 152 | '''Return the bounds of each netnode (nodeidx_t) within the database.''' 153 | this = netnode.root() 154 | ok, start = netnode.start(this), netnode.index(this) 155 | if not ok: raise internal.exceptions.NetNodeNotFoundError(u"{:s}.range() : Unable to find first node.".format('.'.join([__name__, cls.__name__]))) 156 | ok, end = netnode.end(this), netnode.index(this) 157 | if not ok: raise internal.exceptions.NetNodeNotFoundError(u"{:s}.range() : Unable to find end node.".format('.'.join([__name__, cls.__name__]))) 158 | return start, end 159 | 160 | @classmethod 161 | def renumerate(cls): 162 | '''Iterate through each netnode in the database in reverse order, and yield the (nodeidx_t, netnode*) for each item found.''' 163 | start, end = cls.range() 164 | this = netnode.root() 165 | ok = netnode.end(this) 166 | if not ok: 167 | raise internal.exceptions.NetNodeNotFoundError(u"{:s}.renumerate() : Unable to find the end node.".format('.'.join([__name__, cls.__name__]))) 168 | 169 | yield end, cls.get(end) 170 | while end != start: 171 | ok = netnode.prev(this) 172 | if not ok: break 173 | end = netnode.index(this) 174 | yield end, cls.get(end) 175 | return 176 | 177 | @classmethod 178 | def fenumerate(cls): 179 | '''Iterate through each netnode in the database in order, and yield the (nodeidx_t, netnode*) for each item found.''' 180 | start, end = cls.range() 181 | this = netnode.root() 182 | ok = netnode.start(this) 183 | if not ok: 184 | raise internal.exceptions.NetNodeNotFoundError(u"{:s}.fenumerate() : Unable to find the start node.".format('.'.join([__name__, cls.__name__]))) 185 | 186 | yield start, cls.get(start) 187 | while start != end: 188 | ok = netnode.next(this) 189 | if not ok: break 190 | start = netnode.index(this) 191 | yield start, cls.get(start) 192 | return 193 | 194 | @classmethod 195 | def valfiter(cls, node, first, last, next, val, tag): 196 | '''Iterate through all of the values for a netnode in order, and yield the (item, value) for each item that was found for the given tag.''' 197 | start, end = first(node, tag), last(node, tag) 198 | if start in {None, idaapi.BADADDR}: return 199 | yield start, val(node, start, tag) 200 | while start != end: 201 | start = next(node, start, tag) 202 | yield start, val(node, start, tag) 203 | return 204 | 205 | @classmethod 206 | def valriter(cls, node, first, last, prev, val, tag): 207 | '''Iterate through all of the values for a netnode in reverse order, and yield the (item, value) for each item that was found for the given tag.''' 208 | start, end = first(node, tag), last(node, tag) 209 | if end in {None, idaapi.BADADDR}: return 210 | yield end, val(node, end, tag) 211 | while end != start: 212 | end = prev(node, end, tag) 213 | yield end, val(node, end, tag) 214 | return 215 | 216 | @classmethod 217 | def hfiter(cls, node, first, last, next, val, tag): 218 | '''Iterate through all of the hash values for a netnode in order, and yield the (item, value) for each item that was found for the given tag.''' 219 | start, end = first(node, tag), last(node, tag) 220 | 221 | # If the start key is None, and it's the same as the end key, then we 222 | # need to verify that there's no value stored for the empty key. If 223 | # there's no value for the empty key, then we can be sure that there's 224 | # no keys to iterate through and thus we can leave. 225 | if start is None and start == end and val(node, start or '', tag) is None: 226 | return 227 | 228 | # Otherwise we need to start at the first item and continue fetching 229 | # the next key until we end up at the last one. 230 | yield start or '', val(node, start or '', tag) 231 | while start != end: 232 | start = next(node, start or '', tag) 233 | yield start or '', val(node, start or '', tag) 234 | return 235 | 236 | @classmethod 237 | def hriter(cls, node, first, last, prev, val, tag): 238 | '''Iterate through all of the hash values for a netnode in reverse order, and yield the (item, value) for each item that was found for the given tag.''' 239 | start, end = first(node, tag), last(node, tag) 240 | 241 | # If the end key is None, and it's the same as the start key, then we 242 | # need to verify that there's no value stored for the empty key. If 243 | # there's no value for the empty key, then we can be sure that there's 244 | # no keys to iterate through and thus we can leave. 245 | if end is None and start == end and val(node, end or '', tag) is None: 246 | return 247 | 248 | # Otherwise we need to start at the last item and continue fetching the 249 | # previous key until we end up at the first one. 250 | yield end or '', val(node, end or '', tag) 251 | while end != start: 252 | end = prev(node, end or '', tag) 253 | yield end or '', val(node, end or '', tag) 254 | return 255 | 256 | @classmethod 257 | def falt(cls, node, tag=netnode.alttag): 258 | '''Iterate through each "altval" for a given `node` in order, and yield each (item, value) that was found.''' 259 | for item in cls.valfiter(node, netnode.altfirst, netnode.altlast, netnode.altnext, netnode.altval, tag=tag): 260 | yield item 261 | return 262 | @classmethod 263 | def ralt(cls, node, tag=netnode.alttag): 264 | '''Iterate through each "altval" for a given `node` in reverse order, and yield each (item, value) that was found.''' 265 | for item in cls.valriter(node, netnode.altfirst, netnode.altlast, netnode.altprev, netnode.altval, tag=tag): 266 | yield item 267 | return 268 | 269 | @classmethod 270 | def fsup(cls, node, value=None, tag=netnode.suptag): 271 | '''Iterate through each "supval" for a given `node` in order, and yield each (item, value) that was found.''' 272 | for item in cls.valfiter(node, netnode.supfirst, netnode.suplast, netnode.supnext, value or netnode.supval, tag=tag): 273 | yield item 274 | return 275 | @classmethod 276 | def rsup(cls, node, value=None, tag=netnode.suptag): 277 | '''Iterate through each "supval" for a given `node` in reverse order, and yield each (item, value) that was found.''' 278 | for item in cls.valriter(node, netnode.supfirst, netnode.suplast, netnode.supprev, value or netnode.supval, tag=tag): 279 | yield item 280 | return 281 | 282 | @classmethod 283 | def fhash(cls, node, value=None, tag=netnode.hashtag): 284 | '''Iterate through each "hashval" for a given `node` in order, and yield each (item, value) that was found.''' 285 | for item in cls.hfiter(node, netnode.hashfirst, netnode.hashlast, netnode.hashnext, value or netnode.hashval, tag=tag): 286 | yield item 287 | return 288 | @classmethod 289 | def rhash(cls, node, value=None, tag=netnode.hashtag): 290 | '''Iterate through each "hashval" for a given `node` in reverse order, and yield each (item, value) that was found.''' 291 | for item in cls.hriter(node, netnode.hashfirst, netnode.hashlast, netnode.hashprev, value or netnode.hashval, tag=tag): 292 | yield item 293 | return 294 | 295 | @classmethod 296 | def fchar(cls, node, value=None, tag=netnode.chartag): 297 | '''Iterate through each "charval" for a given `node` in order, and yield each (item, value) that was found.''' 298 | for item in cls.valfiter(node, netnode.charfirst, netnode.charlast, netnode.charnext, value or netnode.charval, tag=tag): 299 | yield item 300 | return 301 | @classmethod 302 | def rchar(cls, node, value=None, tag=netnode.chartag): 303 | '''Iterate through each "charval" for a given `node` in reverse order, and yield each (item, value) that was found.''' 304 | for item in cls.valriter(node, netnode.charfirst, netnode.charlast, netnode.charprev, value or netnode.charval, tag=tag): 305 | yield item 306 | return 307 | 308 | def new(name): 309 | '''Create a netnode with the given `name`, and return its identifier.''' 310 | res = internal.utils.string.to(name) 311 | node = netnode.new(res, len(res), True) 312 | return netnode.index(node) 313 | 314 | def has(name): 315 | '''Return whether the netnode with the given `name` exists in the database or not.''' 316 | if isinstance(name, six.integer_types): 317 | return netnode.exist(name) 318 | res = internal.utils.string.to(name) 319 | return netnode.exist_name(res) 320 | 321 | def get(name): 322 | '''Get (or create) a netnode with the given `name`, and return its identifier.''' 323 | if isinstance(name, six.integer_types): 324 | node = utils.get(name) 325 | node = name 326 | elif isinstance(name, six.string_types): 327 | res = internal.utils.string.to(name) 328 | node = netnode.get(res, len(res)) 329 | else: 330 | node = name 331 | return netnode.index(node) 332 | 333 | def remove(nodeidx): 334 | '''Remove the netnode with the identifier `nodeidx`.''' 335 | node = utils.get(nodeidx) 336 | return netnode.kill(node) 337 | 338 | ### node name 339 | class name(object): 340 | """ 341 | This namespace is used to interact with the naming information for a given netnode. 342 | """ 343 | 344 | @classmethod 345 | def has(cls, nodeidx): 346 | '''Return whether the node identified by `nodeidx` has a name associated with it.''' 347 | node = utils.get(nodeidx) 348 | res = netnode.name(node) 349 | return res is not None 350 | @classmethod 351 | def get(cls, nodeidx): 352 | '''Return the name of the netnode identified by `nodeidx`.''' 353 | node = utils.get(nodeidx) 354 | res = netnode.name(node) 355 | return internal.utils.string.of(res) 356 | @classmethod 357 | def set(cls, nodeidx, string): 358 | '''Set the name of the netnode identified by `nodeidx` to `string`.''' 359 | node = utils.get(nodeidx) 360 | res = internal.utils.string.to(string) 361 | return netnode.rename(node, res) 362 | 363 | ### node value (?) 364 | class value(object): 365 | """ 366 | This namespace is used to interact with the value for a given netnode. 367 | """ 368 | 369 | @classmethod 370 | def has(cls, nodeidx): 371 | '''Return whether the node identified by `nodeidx` has a value associated with it.''' 372 | node = utils.get(nodeidx) 373 | return netnode.value_exists(node) 374 | exists = internal.utils.alias(has, 'value') 375 | 376 | @classmethod 377 | def get(cls, nodeidx, type=None): 378 | '''Return the value for the netnode identified by `nodeidx` casted to the provided `type`.''' 379 | node = utils.get(nodeidx) 380 | if not netnode.value_exists(node): 381 | return None 382 | 383 | if type in {None}: 384 | return netnode.valobj(node) 385 | elif issubclass(type, memoryview): 386 | res = netnode.valobj(node) 387 | return res and memoryview(res) 388 | elif issubclass(type, bytes): 389 | res = netnode.valstr(node) 390 | return res and bytes(res) 391 | elif issubclass(type, six.string_types): 392 | return netnode.valstr(node) 393 | elif issubclass(type, six.integer_types): 394 | return netnode.long_value(node) 395 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 396 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.get({:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's value.".format('.'.join([__name__, cls.__name__]), description, type, type)) 397 | 398 | @classmethod 399 | def set(cls, nodeidx, value): 400 | '''Set the value for the netnode identified by `nodeidx` to the provided `value`.''' 401 | node = utils.get(nodeidx) 402 | if isinstance(value, memoryview): 403 | return netnode.set(nodeidx, value.tobytes()) 404 | elif isinstance(value, bytes): 405 | return netnode.set(node, value) 406 | elif isinstance(value, six.string_types): 407 | return netnode.set(node, value) 408 | elif isinstance(value, six.integer_types): 409 | return netnode.set_long(node, value) 410 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 411 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.set({:#x}, {!r}) : An unsupported type ({!r}) was specified for the netnode's value.".format('.'.join([__name__, cls.__name__]), description, value, value.__class__)) 412 | 413 | @classmethod 414 | def remove(cls, nodeidx): 415 | '''Remove the value for the netnode identified by `nodeidx`.''' 416 | node = utils.get(nodeidx) 417 | return netnode.delvalue(node) 418 | 419 | @classmethod 420 | def repr(cls, nodeidx): 421 | '''Display the value for the netnode identified by `nodeidx`.''' 422 | if not cls.has(nodeidx): 423 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 424 | raise internal.exceptions.MissingTypeOrAttribute(u"{:s}.repr({:s}) : The specified node ({:s}) does not have any value.".format('.'.join([__name__, cls.__name__]), description, description)) 425 | res, string, value = cls.get(nodeidx), cls.get(nodeidx, type=bytes), cls.get(nodeidx, type=int) 426 | return "{!r} {!r} {:#x}".format(res, string, value) 427 | 428 | ### node blob 429 | class blob(object): 430 | """ 431 | This namespace is used to interact with the blob assigned to a given netnode. 432 | """ 433 | @classmethod 434 | def has(cls, nodeidx, tag): 435 | '''Return whether the node identified by `nodeidx` has a blob associated with it.''' 436 | node = utils.get(nodeidx) 437 | res = netnode.blobsize(node, 0, tag) 438 | return res > 0 439 | 440 | @classmethod 441 | def get(cls, nodeidx, tag, start=0): 442 | """Return the blob stored in `tag` for the netnode identified by `nodeidx`. 443 | 444 | If an offset is provided as `start`, then return the bytes from the specified offset. 445 | """ 446 | node = utils.get(nodeidx) 447 | sz = netnode.blobsize(node, start, tag) 448 | res = netnode.getblob(node, start, tag) 449 | return None if res is None else res[:sz] 450 | 451 | @classmethod 452 | def set(cls, nodeidx, tag, value, start=0): 453 | """Assign the data provided by `value` to the blob stored in `tag` for the netnode identified by `nodeidx`. 454 | 455 | If an offset is provided as `start`, then store the provided `value` at the given offset. 456 | """ 457 | node = utils.get(nodeidx) 458 | return netnode.setblob(node, value.tobytes() if isinstance(value, memoryview) else value, start, tag) 459 | 460 | @classmethod 461 | def remove(cls, nodeidx, tag, start=0): 462 | """Remove the data from the blob stored in `tag` for the netnode identified by `nodeidx`. 463 | 464 | If an offset is provided as `start`, then remove the data at the given offset. 465 | """ 466 | node = utils.get(nodeidx) 467 | return netnode.delblob(node, start, tag) 468 | 469 | @classmethod 470 | def size(cls, nodeidx, tag, start=0): 471 | """Return the size of the blob stored in `tag` for the netnode identified by `nodeidx`. 472 | 473 | If an offset is provided as `start`, then return the size from the given offset. 474 | """ 475 | node = utils.get(nodeidx) 476 | return netnode.blobsize(node, start, tag) 477 | 478 | @classmethod 479 | def repr(cls, nodeidx, tag): 480 | '''Display the blob stored in `tag` for the netnode identified by `nodeidx`.''' 481 | if cls.size(nodeidx, tag) == 0: 482 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 483 | raise internal.exceptions.MissingTypeOrAttribute(u"{:s}.repr({:s}, {!r}) : The tag {!r} for the specified node ({:s}) does not have a blob.".format('.'.join([__name__, cls.__name__]), description, tag, tag, description)) 484 | res = cls.get(nodeidx, tag) 485 | return "{!r}".format(res) 486 | 487 | ### node iteration 488 | def fiter(): 489 | '''Iterate through each netnode index from the database in order.''' 490 | for nodeidx, _ in utils.fenumerate(): 491 | yield nodeidx 492 | return 493 | def riter(): 494 | '''Iterate through each netnode index from the database in reverse order.''' 495 | for nodeidx, _ in utils.renumerate(): 496 | yield nodeidx 497 | return 498 | 499 | def fitems(): 500 | '''Iterate through each netnode index and node from the database in order.''' 501 | for nodeidx, item in utils.fenumerate(): 502 | yield nodeidx, item 503 | return 504 | def ritems(): 505 | '''Iterate through each netnode index and node from the database in reverse order.''' 506 | for nodeidx, item in utils.renumerate(): 507 | yield nodeidx, item 508 | return 509 | 510 | ### node altval : sparse array[integer] = integer 511 | class alt(object): 512 | """ 513 | This namespace is used for interacting with the sparse array stored 514 | within a given netnode. This sparse array is used to store integers, 515 | and is referred to by IDA as an "altval". 516 | """ 517 | 518 | @classmethod 519 | def has(cls, nodeidx, index, tag=None): 520 | '''Return whether the netnode identified by `nodeidx` has an "altval" for the specified `index`.''' 521 | return any(index == idx for idx in cls.fiter(nodeidx, tag=tag)) 522 | 523 | @classmethod 524 | def get(cls, nodeidx, index, tag=None): 525 | '''Return the integer at the `index` of the "altval" array belonging to the netnode identified by `nodeidx`.''' 526 | node = utils.get(nodeidx) 527 | return netnode.altval(node, index, netnode.alttag if tag is None else tag) 528 | 529 | @classmethod 530 | def set(cls, nodeidx, index, value, tag=None): 531 | '''Assign the integer `value` at the `index` of the "altval" array belonging to the netnode identified by `nodeidx`.''' 532 | node = utils.get(nodeidx) 533 | return netnode.altset(node, index, value, netnode.alttag if tag is None else tag) 534 | 535 | @classmethod 536 | def remove(cls, nodeidx, index, tag=None): 537 | '''Remove the integer from the specified `index` of the "altval" array belonging to the netnode identified by `nodeidx`.''' 538 | node = utils.get(nodeidx) 539 | return netnode.altdel(node, index, netnode.alttag if tag is None else tag) 540 | 541 | @classmethod 542 | def fiter(cls, nodeidx, tag=None): 543 | '''Iterate through all of the indexes of the "altval" array belonging to the netnode identified by `nodeidx` in order.''' 544 | node = utils.get(nodeidx) 545 | for nalt, _ in utils.falt(node, tag=netnode.alttag if tag is None else tag): 546 | yield nalt 547 | return 548 | 549 | @classmethod 550 | def fitems(cls, nodeidx, tag=None): 551 | '''Iterate through all of the elements of the "altval" array belonging to the netnode identified by `nodeidx` in order.''' 552 | node = utils.get(nodeidx) 553 | for nalt, altval in utils.falt(node, tag=netnode.alttag if tag is None else tag): 554 | yield nalt, altval 555 | return 556 | 557 | @classmethod 558 | def riter(cls, nodeidx, tag=None): 559 | '''Iterate through all of the indexes of the "altval" array belonging to the netnode identified by `nodeidx` in reverse order.''' 560 | node = utils.get(nodeidx) 561 | for nalt, _ in utils.ralt(node, tag=netnode.alttag if tag is None else tag): 562 | yield nalt 563 | return 564 | 565 | @classmethod 566 | def ritems(cls, nodeidx, tag=None): 567 | '''Iterate through all of the elements of the "altval" array belonging to the netnode identified by `nodeidx` in reverse order.''' 568 | node = utils.get(nodeidx) 569 | for nalt, altval in utils.ralt(node, tag=netnode.alttag if tag is None else tag): 570 | yield nalt, altval 571 | return 572 | 573 | @classmethod 574 | def repr(cls, nodeidx, tag=None): 575 | '''Display the "altval" array belonging to the netnode identified by `nodeidx`.''' 576 | res = [] 577 | for index, value in cls.fitems(nodeidx, tag=tag): 578 | res.append("{0:x} : {1:#x} ({1:d})".format(index, value)) 579 | if not res: 580 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 581 | raise internal.exceptions.MissingTypeOrAttribute(u"{:s}.repr({:s}) : The specified node ({:s}) does not have any altvals.".format('.'.join([__name__, cls.__name__]), description, description)) 582 | return '\n'.join(res) 583 | 584 | ### node supval : sparse array[integer] = str * 1024 585 | class sup(object): 586 | """ 587 | This namespace is used for interacting with the sparse array stored 588 | within a given netnode. This sparse array is used to store bytes, 589 | and is referred to by IDA as a "supval". 590 | """ 591 | 592 | MAX_SIZE = 0x400 593 | 594 | @classmethod 595 | def has(cls, nodeidx, index, tag=None): 596 | '''Return whether the netnode identified by `nodeidx` has a "supval" for the specified `index`.''' 597 | return any(index == item for item in cls.fiter(nodeidx, tag=tag)) 598 | 599 | @classmethod 600 | def get(cls, nodeidx, index, type=None, tag=None): 601 | '''Return the value at the `index` of the "supval" array belonging to the netnode identified by `nodeidx` casted as the specified `type`.''' 602 | node = utils.get(nodeidx) 603 | if type in {None}: 604 | return netnode.supval(node, index, netnode.suptag if tag is None else tag) 605 | elif issubclass(type, memoryview): 606 | res = netnode.supval(node, index, netnode.suptag if tag is None else tag) 607 | return res and memoryview(res) 608 | elif issubclass(type, bytes): 609 | return netnode.supstr(node, index, netnode.suptag if tag is None else tag) 610 | elif issubclass(type, six.string_types): 611 | return netnode.supstr(node, index, netnode.suptag if tag is None else tag) 612 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 613 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.get({:#x}, {:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's supval.".format('.'.join([__name__, cls.__name__]), description, index, type, type)) 614 | 615 | @classmethod 616 | def set(cls, nodeidx, index, value, tag=None): 617 | '''Assign the provided `value` to the specified `index` of the "supval" array belonging to the netnode identified by `nodeidx`.''' 618 | node = utils.get(nodeidx) 619 | return netnode.supset(node, index, value.tobytes() if isinstance(value, memoryview) else value, netnode.suptag if tag is None else tag) 620 | 621 | @classmethod 622 | def remove(cls, nodeidx, index, tag=None): 623 | '''Remove the value at the specified `index` of the "supval" array belonging to the netnode identified by `nodeidx`.''' 624 | node = utils.get(nodeidx) 625 | return netnode.supdel(node, index, netnode.suptag if tag is None else tag) 626 | 627 | @classmethod 628 | def fiter(cls, nodeidx, tag=None): 629 | '''Iterate through all of the indexes of the "supval" array belonging to the netnode identified by `nodeidx` in order.''' 630 | node = utils.get(nodeidx) 631 | for nsup, _ in utils.fsup(node, tag=netnode.suptag if tag is None else tag): 632 | yield nsup 633 | return 634 | 635 | @classmethod 636 | def fitems(cls, nodeidx, type=None, tag=None): 637 | '''Iterate through all of the elements of the "supval" array belonging to the netnode identified by `nodeidx` in order.''' 638 | if type in {None}: 639 | value, transform = netnode.supval, internal.utils.fidentity 640 | elif issubclass(type, memoryview): 641 | value, transform = netnode.supval, memoryview 642 | elif issubclass(type, bytes): 643 | value, transform = netnode.supstr, internal.utils.fidentity 644 | elif issubclass(type, six.string_types): 645 | value, transform = netnode.supstr, internal.utils.fidentity 646 | else: 647 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 648 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.fitems({:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's supval.".format('.'.join([__name__, cls.__name__]), description, type, type)) 649 | node = utils.get(nodeidx) 650 | for nsup, supval in utils.fsup(node, value, tag=netnode.suptag if tag is None else tag): 651 | yield nsup, transform(supval) 652 | return 653 | 654 | @classmethod 655 | def riter(cls, nodeidx, tag=None): 656 | '''Iterate through all of the indexes of the "supval" array belonging to the netnode identified by `nodeidx` in reverse order.''' 657 | node = utils.get(nodeidx) 658 | for nsup, _ in utils.rsup(node, tag=netnode.suptag if tag is None else tag): 659 | yield nsup 660 | return 661 | 662 | @classmethod 663 | def ritems(cls, nodeidx, type=None, tag=None): 664 | '''Iterate through all of the elements of the "supval" array belonging to the netnode identified by `nodeidx` in reverse order.''' 665 | if type in {None}: 666 | value, transform = netnode.supval, internal.utils.fidentity 667 | elif issubclass(type, memoryview): 668 | value, transform = netnode.supval, memoryview 669 | elif issubclass(type, bytes): 670 | value, transform = netnode.supstr, internal.utils.fidentity 671 | elif issubclass(type, six.string_types): 672 | value, transform = netnode.supstr, internal.utils.fidentity 673 | else: 674 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 675 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.ritems({:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's supval.".format('.'.join([__name__, cls.__name__]), description, type, type)) 676 | node = utils.get(nodeidx) 677 | for nsup, supval in utils.rsup(node, value, tag=netnode.suptag if tag is None else tag): 678 | yield nsup, transform(supval) 679 | return 680 | 681 | @classmethod 682 | def repr(cls, nodeidx, tag=None): 683 | '''Display the "supval" array belonging to the netnode identified by `nodeidx`.''' 684 | res = [] 685 | for index, item in enumerate(cls.fiter(nodeidx, tag=tag)): 686 | value = cls.get(nodeidx, item, tag=tag) 687 | res.append("[{:d}] {:x} : {!r}".format(index, item, value)) 688 | if not res: 689 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 690 | raise internal.exceptions.MissingTypeOrAttribute(u"{:s}.repr({:s}) : The specified node ({:s}) does not have any supvals.".format('.'.join([__name__, cls.__name__]), description, description)) 691 | return '\n'.join(res) 692 | 693 | ### node hashval : sparse dictionary[str * 510] = str * 1024 694 | class hash(object): 695 | """ 696 | This namespace is used for interacting with the dictionary stored 697 | within a given netnode. This dictionary is keyed by bytes of a 698 | maximum length of 510, and is used to store bytes of a maximum 699 | length of 1024. IDA refers to this dictionary as a "hashval". 700 | """ 701 | 702 | @classmethod 703 | def has(cls, nodeidx, key, tag=None): 704 | '''Return whether the netnode identified by `nodeidx` has a "hashval" for the specified `key`.''' 705 | return any(key == item for item in cls.fiter(nodeidx, tag=tag)) 706 | 707 | @classmethod 708 | def get(cls, nodeidx, key, type=None, tag=None): 709 | '''Return the value for the provided `key` of the "hashval" dictionary belonging to the netnode identified by `nodeidx` casted as the specified `type`.''' 710 | node = utils.get(nodeidx) 711 | if type in {None}: 712 | return netnode.hashval(node, key, netnode.hashtag if tag is None else tag) 713 | elif issubclass(type, memoryview): 714 | res = netnode.hashval(node, key, netnode.hashtag if tag is None else tag) 715 | return res and memoryview(res) 716 | elif issubclass(type, bytes): 717 | res = netnode.hashval(node, key, netnode.hashtag if tag is None else tag) 718 | return res and bytes(res) 719 | elif issubclass(type, six.string_types): 720 | return netnode.hashstr(node, key, netnode.hashtag if tag is None else tag) 721 | elif issubclass(type, six.integer_types): 722 | return netnode.hashval_long(node, key, netnode.hashtag if tag is None else tag) 723 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 724 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.get({:#x}, {!r}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's hash.".format('.'.join([__name__, cls.__name__]), description, key, type, type)) 725 | 726 | @classmethod 727 | def set(cls, nodeidx, key, value, tag=None): 728 | '''Assign the provided `value` to the specified `key` for the "hashval" dictionary belonging to the netnode identified by `nodeidx`.''' 729 | node = utils.get(nodeidx) 730 | # in my testing the type really doesn't matter 731 | if isinstance(value, memoryview): 732 | return netnode.hashset(node, key, value.tobytes(), netnode.hashtag if tag is None else tag) 733 | elif isinstance(value, bytes): 734 | return netnode.hashset(node, key, value, netnode.hashtag if tag is None else tag) 735 | elif isinstance(value, six.string_types): 736 | return netnode.hashset_buf(node, key, value, netnode.hashtag if tag is None else tag) 737 | elif isinstance(value, six.integer_types): 738 | return netnode.hashset_idx(node, key, value, netnode.hashtag if tag is None else tag) 739 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 740 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.set({:#x}, {!r}, {!r}) : An unsupported type ({!r}) was specified for the netnode's hash.".format('.'.join([__name__, cls.__name__]), description, key, value, type(value))) 741 | 742 | @classmethod 743 | def remove(cls, nodeidx, key, tag=None): 744 | '''Remove the value assigned to the specified `key` of the "hashval" dictionary belonging to the netnode identified by `nodeidx`.''' 745 | node = utils.get(nodeidx) 746 | return netnode.hashdel(node, key, netnode.hashtag if tag is None else tag) 747 | 748 | @classmethod 749 | def fiter(cls, nodeidx, tag=None): 750 | '''Iterate through all of the keys of the "hashval" dictionary belonging to the netnode identified by `nodeidx` in order.''' 751 | node = utils.get(nodeidx) 752 | for idx, _ in utils.fhash(node, tag=netnode.hashtag if tag is None else tag): 753 | yield idx 754 | return 755 | 756 | @classmethod 757 | def fitems(cls, nodeidx, type=None, tag=None): 758 | '''Iterate through all of the elements of the "hashval" dictionary belonging to the netnode identified by `nodeidx` in order.''' 759 | if type in {None}: 760 | value, transform = netnode.hashval, internal.utils.fidentity 761 | elif issubclass(type, memoryview): 762 | value, transform = netnode.hashval, memoryview 763 | elif issubclass(type, bytes): 764 | value, transform = netnode.hashval, bytes 765 | elif issubclass(type, six.string_types): 766 | value, transform = netnode.hashstr, internal.utils.fidentity 767 | elif issubclass(type, six.integer_types): 768 | value, transform = netnode.hashval_long, internal.utils.fidentity 769 | else: 770 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 771 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.fitems({:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's hash.".format('.'.join([__name__, cls.__name__]), description, type, type)) 772 | node = utils.get(nodeidx) 773 | for idx, hashval in utils.fhash(node, value, tag=netnode.hashtag if tag is None else tag): 774 | yield idx, transform(hashval) 775 | return 776 | 777 | @classmethod 778 | def riter(cls, nodeidx, tag=None): 779 | '''Iterate through all of the keys of the "hashval" dictionary belonging to the netnode identified by `nodeidx` in reverse order.''' 780 | node = utils.get(nodeidx) 781 | for idx, _ in utils.rhash(node, tag=netnode.hashtag if tag is None else tag): 782 | yield idx 783 | return 784 | 785 | @classmethod 786 | def ritems(cls, nodeidx, type=None, tag=None): 787 | '''Iterate through all of the elements of the "hashval" dictionary belonging to the netnode identified by `nodeidx` in reverse order.''' 788 | if type in {None}: 789 | value, transform = netnode.hashval, internal.utils.fidentity 790 | elif issubclass(type, memoryview): 791 | value, transform = netnode.hashval, memoryview 792 | elif issubclass(type, bytes): 793 | value, transform = netnode.hashval, bytes 794 | elif issubclass(type, six.string_types): 795 | value, transform = netnode.hashstr, internal.utils.fidentity 796 | elif issubclass(type, six.integer_types): 797 | value, transform = netnode.hashval_long, internal.utils.fidentity 798 | else: 799 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 800 | raise internal.exceptions.InvalidTypeOrValueError(u"{:s}.ritems({:#x}, type={!r}) : An unsupported type ({!r}) was requested for the netnode's hash.".format('.'.join([__name__, cls.__name__]), description, type, type)) 801 | node = utils.get(nodeidx) 802 | for idx, hashval in utils.rhash(node, value, tag=netnode.hashtag if tag is None else tag): 803 | yield idx, transform(hashval) 804 | return 805 | 806 | @classmethod 807 | def repr(cls, nodeidx, tag=None): 808 | '''Display the "hashval" dictionary belonging to the netnode identified by `nodeidx`.''' 809 | res = [] 810 | try: 811 | l1 = max(len(key or '') for key in cls.fiter(nodeidx, tag=tag)) 812 | l2 = max(len("{!r}".format(cls.get(nodeidx, key, tag=tag))) for key in cls.fiter(nodeidx, tag=tag)) 813 | except ValueError: 814 | l1, l2 = 0, 2 815 | 816 | for index, key in enumerate(cls.fiter(nodeidx, tag=tag)): 817 | value = "{:<{:d}s} : default={!r}, bytes={!r}, int={:#x}({:d})".format("{!r}".format(cls.get(nodeidx, key, tag=tag)), l2, cls.get(nodeidx, key, None, tag=tag), cls.get(nodeidx, key, bytes, tag=tag), cls.get(nodeidx, key, int, tag=tag), cls.get(nodeidx, key, int, tag=tag)) 818 | res.append("[{:d}] {:<{:d}s} -> {:s}".format(index, key, l1, value)) 819 | if not res: 820 | description = "{:#x}".format(nodeidx) if isinstance(nodeidx, six.integer_types) else "{!r}".format(nodeidx) 821 | raise internal.exceptions.MissingTypeOrAttribute(u"{:s}.repr({:s}) : The specified node ({:s}) does not have any hashvals.".format('.'.join([__name__, cls.__name__]), description, description)) 822 | return '\n'.join(res) 823 | 824 | # FIXME: implement a file-allocation-table based filesystem using the netnode wrappers defined above 825 | class filesystem(object): 826 | ALLOCATION_TABLE = '$ file-allocation-table' 827 | SECTOR_TABLE = '$ sector-table' 828 | SECTOR = 1024 829 | def __init__(self, name): 830 | node = idaapi.netnode(name, 0, True) 831 | -------------------------------------------------------------------------------- /base/segment.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Segment module 3 | 4 | This module provides a number of tools that can be used to enumerate 5 | or work with segments within a database. 6 | 7 | The base argument type for some of the utilities within this module 8 | is the ``idaapi.segment_t``. This type is interchangeable with the 9 | address or the segment name and so either can be used to identify a 10 | segment. 11 | 12 | When listing or enumerating segments there are different types that 13 | one can use in order to filter or match them. These types are as 14 | follows: 15 | 16 | `name` - Match according to the exact segment name 17 | `like` - Filter the segment names according to a glob 18 | `regex` - Filter the function names according to a regular-expression 19 | `index` - Match the segment by its index 20 | `identifier` - Match the segment by its identifier 21 | `selector` - Match the segment by its selector 22 | `greater` or `ge` - Filter the segments for any after the specified address (inclusive) 23 | `gt` - Filter the segments for any after the specified address (exclusive) 24 | `less` or `le` - Filter the segments for any before the specified address (inclusive) 25 | `lt` - Filter the segments for any before the specified address (exclusive) 26 | `predicate` - Filter the segments by passing their ``idaapi.segment_t`` to a callable 27 | 28 | Some examples of using these keywords are as follows:: 29 | 30 | > for l, r in database.segments(): ... 31 | > database.segments.list(regex=r'\.r?data') 32 | > iterable = database.segments.iterate(like='*text*') 33 | > result = database.segments.search(greater=0x401000) 34 | 35 | """ 36 | 37 | import six, builtins 38 | 39 | import functools, operator, itertools, types 40 | import os, logging 41 | import math, re, fnmatch 42 | 43 | import database 44 | import ui, internal 45 | from internal import utils, interface, exceptions as E 46 | 47 | import idaapi 48 | 49 | ## enumerating 50 | __matcher__ = utils.matcher() 51 | __matcher__.combinator('regex', utils.fcompose(utils.fpartial(re.compile, flags=re.IGNORECASE), operator.attrgetter('match')), idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name, utils.string.of) 52 | __matcher__.attribute('index', 'index') 53 | __matcher__.attribute('identifier', 'name'), __matcher__.attribute('id', 'name') 54 | __matcher__.attribute('selector', 'sel') 55 | __matcher__.combinator('like', utils.fcompose(fnmatch.translate, utils.fpartial(re.compile, flags=re.IGNORECASE), operator.attrgetter('match')), idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name, utils.string.of) 56 | __matcher__.boolean('name', lambda name, item: name.lower() == item.lower(), idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name, utils.string.of) 57 | if idaapi.__version__ < 7.0: 58 | __matcher__.boolean('greater', operator.le, 'endEA') 59 | __matcher__.boolean('gt', operator.lt, 'endEA') 60 | __matcher__.boolean('less', operator.ge, 'startEA') 61 | __matcher__.boolean('lt', operator.gt, 'startEA') 62 | else: 63 | __matcher__.boolean('greater', operator.le, 'end_ea') 64 | __matcher__.boolean('gt', operator.lt, 'end_ea') 65 | __matcher__.boolean('less', operator.ge, 'start_ea') 66 | __matcher__.boolean('lt', operator.gt, 'start_ea') 67 | __matcher__.predicate('predicate'), __matcher__.predicate('pred') 68 | 69 | @utils.string.decorate_arguments('regex', 'like', 'name') 70 | def __iterate__(**type): 71 | '''Iterate through each segment defined in the database that match the keywords specified by `type`.''' 72 | def newsegment(index): 73 | seg = idaapi.getnseg(index) 74 | seg.index, _ = index, ui.navigation.set(interface.range.start(seg)) 75 | return seg 76 | iterable = (newsegment(index) for index in builtins.range(idaapi.get_segm_qty())) 77 | for key, value in (type or {'predicate': utils.fconstant(True)}).items(): 78 | iterable = (item for item in __matcher__.match(key, value, iterable)) 79 | for item in iterable: yield item 80 | 81 | @utils.multicase(string=six.string_types) 82 | @utils.string.decorate_arguments('string') 83 | def list(string): 84 | '''List all of the segments whose name matches the glob specified by `string`.''' 85 | return list(like=string) 86 | @utils.multicase() 87 | @utils.string.decorate_arguments('regex', 'like', 'name') 88 | def list(**type): 89 | '''List all of the segments in the database that match the keyword specified by `type`.''' 90 | get_segment_name = idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name 91 | 92 | listable = [] 93 | 94 | # Set some reasonable defaults 95 | maxindex = maxaddr = maxsize = maxname = 0 96 | 97 | # First pass through our segments to grab lengths of displayed fields 98 | for seg in __iterate__(**type): 99 | maxindex = max(seg.index, maxindex) 100 | maxaddr = max(interface.range.end(seg), maxaddr) 101 | maxsize = max(seg.size(), maxsize) 102 | maxname = max(len(get_segment_name(seg)), maxname) 103 | 104 | listable.append(seg) 105 | 106 | # Collect the maximum sizes for everything from the first pass. We have 107 | # to use different algorithms as due to Python's issues with imprecision, 108 | # the resulting number of digits will vary depending on what base is 109 | # actually being used when calculating the logarithm. 110 | cindex = utils.string.digits(maxindex, 10) 111 | caddr, csize = (utils.string.digits(item, 10) for item in [maxaddr, maxsize]) 112 | 113 | # List all the fields for each segment that we've aggregated 114 | for seg in listable: 115 | comment, _ = idaapi.get_segment_cmt(seg, 0) or idaapi.get_segment_cmt(seg, 1), ui.navigation.set(interface.range.start(seg)) 116 | six.print_(u"[{:{:d}d}] {:#0{:d}x}<>{:#0{:d}x} : {:<+#{:d}x} : {:>{:d}s} : sel:{:04x} flags:{:02x}{:s}".format(seg.index, math.trunc(cindex), interface.range.start(seg), 2 + math.trunc(caddr), interface.range.end(seg), 2 + math.trunc(caddr), seg.size(), 3 + math.trunc(csize), utils.string.of(get_segment_name(seg)), maxname, seg.sel, seg.flags, u"// {:s}".format(utils.string.of(comment)) if comment else '')) 117 | return 118 | 119 | ## searching 120 | @utils.string.decorate_arguments('name') 121 | def by_name(name): 122 | '''Return the segment with the given `name`.''' 123 | res = utils.string.to(name) 124 | seg = idaapi.get_segm_by_name(res) 125 | if seg is None: 126 | raise E.SegmentNotFoundError(u"{:s}.by_name({!r}) : Unable to locate the segment with the specified name.".format(__name__, name)) 127 | return seg 128 | byname = utils.alias(by_name) 129 | def by_selector(selector): 130 | '''Return the segment associated with `selector`.''' 131 | seg = idaapi.get_segm_by_sel(selector) 132 | if seg is None: 133 | raise E.SegmentNotFoundError(u"{:s}.by_selector({:#x}) : Unable to locate the segment with the specified selector.".format(__name__, selector)) 134 | return seg 135 | byselector = utils.alias(by_selector) 136 | def by_address(ea): 137 | '''Return the segment that contains the specified `ea`.''' 138 | seg = idaapi.getseg(interface.address.within(ea)) 139 | if seg is None: 140 | raise E.SegmentNotFoundError(u"{:s}.by_address({:#x}) : Unable to locate segment containing the specified address.".format(__name__, ea)) 141 | return seg 142 | byaddress = utils.alias(by_address) 143 | @utils.multicase(segment=idaapi.segment_t) 144 | def by(segment): 145 | '''Return a segment by its ``idaapi.segment_t``.''' 146 | return segment 147 | @utils.multicase(name=six.string_types) 148 | @utils.string.decorate_arguments('name') 149 | def by(name): 150 | '''Return the segment by its `name`.''' 151 | return by_name(name) 152 | @utils.multicase(ea=six.integer_types) 153 | def by(ea): 154 | '''Return the segment containing the address `ea`.''' 155 | return by_address(ea) 156 | @utils.multicase() 157 | def by(): 158 | '''Return the current segment.''' 159 | return ui.current.segment() 160 | @utils.multicase() 161 | @utils.string.decorate_arguments('regex', 'like', 'name') 162 | def by(**type): 163 | '''Return the segment matching the specified keywords in `type`.''' 164 | searchstring = utils.string.kwargs(type) 165 | get_segment_name = idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name 166 | 167 | listable = [item for item in __iterate__(**type)] 168 | if len(listable) > 1: 169 | maxaddr = max(builtins.map(interface.range.end, listable) if listable else [1]) 170 | caddr = utils.string.digits(maxaddr, 16) 171 | messages = ((u"[{:d}] {:0{:d}x}:{:0{:d}x} {:s} {:+#x} sel:{:04x} flags:{:02x}".format(seg.index, interface.range.start(seg), math.trunc(caddr), interface.range.end(seg), math.trunc(caddr), utils.string.of(get_segment_name(seg)), seg.size(), seg.sel, seg.flags)) for seg in listable) 172 | [ logging.info(msg) for msg in messages ] 173 | logging.warning(u"{:s}.by({:s}) : Found {:d} matching results. Returning the first segment at index {:d} from {:0{:d}x}<>{:0{:d}x} with the name {:s} and size {:+#x}.".format(__name__, searchstring, len(listable), listable[0].index, interface.range.start(listable[0]), math.trunc(caddr), interface.range.end(listable[0]), math.trunc(caddr), utils.string.of(get_segment_name(listable[0])), listable[0].size())) 174 | 175 | iterable = (item for item in listable) 176 | res = builtins.next(iterable, None) 177 | if res is None: 178 | raise E.SearchResultsError(u"{:s}.by({:s}) : Found 0 matching results.".format(__name__, searchstring)) 179 | return res 180 | 181 | @utils.multicase(name=six.string_types) 182 | @utils.string.decorate_arguments('name') 183 | def search(name): 184 | '''Search through all the segments and return the first one matching the glob `name`.''' 185 | return by(like=name) 186 | @utils.multicase() 187 | @utils.string.decorate_arguments('regex', 'like', 'name') 188 | def search(**type): 189 | '''Search through all the segments and return the first one that matches the keyword specified by `type`.''' 190 | return by(**type) 191 | 192 | ## properties 193 | @utils.multicase() 194 | def bounds(): 195 | '''Return the bounds of the current segment.''' 196 | seg = ui.current.segment() 197 | if seg is None: 198 | raise E.SegmentNotFoundError(u"{:s}.bounds() : Unable to locate the current segment.".format(__name__)) 199 | return interface.range.bounds(seg) 200 | @utils.multicase() 201 | def bounds(segment): 202 | '''Return the bounds of the segment specified by `segment`.''' 203 | seg = by(segment) 204 | return interface.range.bounds(seg) 205 | range = utils.alias(bounds) 206 | 207 | @utils.multicase() 208 | def iterate(): 209 | '''Iterate through all of the addresses within the current segment.''' 210 | seg = ui.current.segment() 211 | if seg is None: 212 | raise E.SegmentNotFoundError(u"{:s}.iterate() : Unable to locate the current segment.".format(__name__)) 213 | return iterate(seg) 214 | @utils.multicase() 215 | def iterate(segment): 216 | '''Iterate through all of the addresses within the specified `segment`.''' 217 | seg = by(segment) 218 | return iterate(seg) 219 | @utils.multicase(segment=idaapi.segment_t) 220 | def iterate(segment): 221 | '''Iterate through all of the addresses within the ``idaapi.segment_t`` represented by `segment`.''' 222 | left, right = interface.range.unpack(segment) 223 | for ea in database.address.iterate(left, right): 224 | yield ea 225 | return 226 | 227 | @utils.multicase() 228 | def size(): 229 | '''Return the size of the current segment.''' 230 | seg = ui.current.segment() 231 | if seg is None: 232 | raise E.SegmentNotFoundError(u"{:s}.size() : Unable to locate the current segment.".format(__name__)) 233 | return interface.range.size(seg) 234 | @utils.multicase() 235 | def size(segment): 236 | '''Return the size of the segment specified by `segment`.''' 237 | seg = by(segment) 238 | return interface.range.size(seg) 239 | 240 | @utils.multicase() 241 | def offset(): 242 | '''Return the offset of the current address from the beginning of the current segment.''' 243 | return offset(ui.current.segment(), ui.current.address()) 244 | @utils.multicase(ea=six.integer_types) 245 | def offset(ea): 246 | '''Return the offset of the address `ea` from the beginning of the current segment.''' 247 | return offset(ui.current.segment(), ea) 248 | @utils.multicase(ea=six.integer_types) 249 | def offset(segment, ea): 250 | '''Return the offset of the address `ea` from the beginning of `segment`.''' 251 | seg = by(segment) 252 | return ea - interface.range.start(seg) 253 | 254 | @utils.multicase(offset=six.integer_types) 255 | def by_offset(offset): 256 | '''Return the specified `offset` translated to the beginning of the current segment.''' 257 | return by_offset(ui.current.segment(), offset) 258 | @utils.multicase(offset=six.integer_types) 259 | def by_offset(segment, offset): 260 | '''Return the specified `offset` translated to the beginning of `segment`.''' 261 | seg = by(segment) 262 | return interface.range.start(seg) + offset 263 | byoffset = utils.alias(by_offset) 264 | 265 | @utils.multicase(offset=six.integer_types) 266 | def go_offset(offset): 267 | '''Go to the `offset` of the current segment.''' 268 | return go_offset(ui.current.segment(), offset) 269 | @utils.multicase(offset=six.integer_types) 270 | def go_offset(segment, offset): 271 | '''Go to the `offset` of the specified `segment`.''' 272 | seg = by(segment) 273 | return database.go(offset + interface.range.start(seg)) 274 | goof = gooffset = gotooffset = goto_offset = utils.alias(go_offset) 275 | 276 | @utils.multicase() 277 | def read(): 278 | '''Return the contents of the current segment.''' 279 | get_bytes = idaapi.get_many_bytes if idaapi.__version__ < 7.0 else idaapi.get_bytes 280 | 281 | seg = ui.current.segment() 282 | if seg is None: 283 | raise E.SegmentNotFoundError(u"{:s}.read() : Unable to locate the current segment.".format(__name__)) 284 | return get_bytes(interface.range.start(seg), interface.range.size(seg)) 285 | @utils.multicase() 286 | def read(segment): 287 | '''Return the contents of the segment identified by `segment`.''' 288 | get_bytes = idaapi.get_many_bytes if idaapi.__version__ < 7.0 else idaapi.get_bytes 289 | 290 | seg = by(segment) 291 | return get_bytes(interface.range.start(seg), interface.range.size(seg)) 292 | string = utils.alias(read) 293 | 294 | @utils.multicase() 295 | def repr(): 296 | '''Return the current segment in a printable form.''' 297 | segment = ui.current.segment() 298 | if segment is None: 299 | raise E.SegmentNotFoundError(u"{:s}.repr() : Unable to locate the current segment.".format(__name__)) 300 | return repr(segment) 301 | @utils.multicase() 302 | def repr(segment): 303 | '''Return the specified `segment` in a printable form.''' 304 | get_segment_name = idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name 305 | 306 | seg = by(segment) 307 | return "{:s} {:s} {:#x}-{:#x} ({:+#x})".format(object.__repr__(seg), get_segment_name(seg), interface.range.start(seg), interface.range.end(seg), interface.range.size(seg)) 308 | 309 | @utils.multicase() 310 | def top(): 311 | '''Return the top address of the current segment.''' 312 | seg = ui.current.segment() 313 | if seg is None: 314 | raise E.SegmentNotFoundError(u"{:s}.top() : Unable to locate the current segment.".format(__name__)) 315 | return interface.range.start(seg) 316 | @utils.multicase() 317 | def top(segment): 318 | '''Return the top address of the segment identified by `segment`.''' 319 | seg = by(segment) 320 | return interface.range.start(seg) 321 | 322 | @utils.multicase() 323 | def bottom(): 324 | '''Return the bottom address of the current segment.''' 325 | seg = ui.current.segment() 326 | if seg is None: 327 | raise E.SegmentNotFoundError(u"{:s}.bottom() : Unable to locate the current segment.".format(__name__)) 328 | return interface.range.end(seg) 329 | @utils.multicase() 330 | def bottom(segment): 331 | '''Return the bottom address of the segment identified by `segment`.''' 332 | seg = by(segment) 333 | return interface.range.end(seg) 334 | 335 | @utils.multicase() 336 | def name(): 337 | '''Return the name of the current segment.''' 338 | get_segment_name = idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name 339 | 340 | seg = ui.current.segment() 341 | if seg is None: 342 | raise E.SegmentNotFoundError(u"{:s}.name() : Unable to locate the current segment.".format(__name__)) 343 | res = get_segment_name(seg) 344 | return utils.string.of(res) 345 | @utils.multicase() 346 | def name(segment): 347 | '''Return the name of the segment identified by `segment`.''' 348 | get_segment_name = idaapi.get_segm_name if hasattr(idaapi, 'get_segm_name') else idaapi.get_true_segm_name 349 | 350 | seg = by(segment) 351 | res = get_segment_name(seg) 352 | return utils.string.of(res) 353 | 354 | @utils.multicase() 355 | def color(): 356 | '''Return the color of the current segment.''' 357 | seg = ui.current.segment() 358 | if seg is None: 359 | raise E.SegmentNotFoundError(u"{:s}.color() : Unable to locate the current segment.".format(__name__)) 360 | b,r = (seg.color&0xff0000)>>16, seg.color&0x0000ff 361 | return None if seg.color == 0xffffffff else (r<<16)|(seg.color&0x00ff00)|b 362 | @utils.multicase() 363 | def color(segment): 364 | '''Return the color of the segment identified by `segment`.''' 365 | seg = by(segment) 366 | b,r = (seg.color&0xff0000)>>16, seg.color&0x0000ff 367 | return None if seg.color == 0xffffffff else (r<<16)|(seg.color&0x00ff00)|b 368 | @utils.multicase(none=None.__class__) 369 | def color(none): 370 | '''Clear the color of the current segment.''' 371 | return color(ui.current.segment(), None) 372 | @utils.multicase(none=None.__class__) 373 | def color(segment, none): 374 | '''Clear the color of the segment identified by `segment`.''' 375 | seg = by(segment) 376 | seg.color = 0xffffffff 377 | return bool(seg.update()) 378 | @utils.multicase(rgb=six.integer_types) 379 | def color(segment, rgb): 380 | '''Sets the color of the segment identified by `segment` to `rgb`.''' 381 | r,b = (rgb&0xff0000) >> 16, rgb&0x0000ff 382 | seg = by(segment) 383 | seg.color = (b<<16)|(rgb&0x00ff00)|r 384 | return bool(seg.update()) 385 | 386 | @utils.multicase() 387 | def within(): 388 | '''Returns true if the current address is within any segment.''' 389 | return within(ui.current.address()) 390 | @utils.multicase(ea=six.integer_types) 391 | def within(ea): 392 | '''Returns true if the address `ea` is within any segment.''' 393 | return any(interface.range.within(ea, seg) for seg in __iterate__()) 394 | 395 | @utils.multicase(ea=six.integer_types) 396 | def contains(ea): 397 | '''Returns true if the address `ea` is contained within the current segment.''' 398 | return contains(ui.current.segment(), ea) 399 | @utils.multicase(address=six.integer_types, ea=six.integer_types) 400 | def contains(address, ea): 401 | '''Returns true if the address `ea` is contained within the segment belonging to the specified `address`.''' 402 | seg = by_address(address) 403 | return contains(seg, ea) 404 | @utils.multicase(name=six.string_types, ea=six.integer_types) 405 | @utils.string.decorate_arguments('name') 406 | def contains(name, ea): 407 | '''Returns true if the address `ea` is contained within the segment with the specified `name`.''' 408 | seg = by_name(name) 409 | return contains(seg, ea) 410 | @utils.multicase(segment=idaapi.segment_t, ea=six.integer_types) 411 | def contains(segment, ea): 412 | '''Returns true if the address `ea` is contained within the ``idaapi.segment_t`` specified by `segment`.''' 413 | return interface.range.within(ea, segment) 414 | 415 | @utils.multicase() 416 | def type(): 417 | '''Return the type of the current segment.''' 418 | return type(ui.current.segment()) 419 | @utils.multicase(ea=six.integer_types) 420 | def type(ea): 421 | '''Return the type of the segment containing the address `ea`.''' 422 | result = idaapi.segtype(ea) 423 | if result == idaapi.SEG_UNDF and not database.within(ea): 424 | bounds, results = "{:#x}<>{:#x}".format(*database.config.bounds()), {getattr(idaapi, name) : name for name in dir(idaapi) if name.startswith('SEG_')} 425 | logging.warning(u"{:s}.type({:#x}) : Returning {:s}({:d}) for the segment type due to the given address ({:#x}) not being within the boundaries of the database ({:s}).".format(__name__, ea, results[result], result, ea, bounds)) 426 | return result 427 | @utils.multicase(name=six.string_types) 428 | @utils.string.decorate_arguments('name') 429 | def type(name): 430 | '''Return the type of the segment with the specified `name`.''' 431 | seg = by_name(name) 432 | return type(seg) 433 | @utils.multicase(segment=idaapi.segment_t) 434 | def type(segment): 435 | '''Return the type of the ``idaapi.segment_t`` specified by `segment`.''' 436 | return segment.type 437 | @utils.multicase(segtype=six.integer_types) 438 | def type(segment, segtype): 439 | '''Return whether the given `segment` is of the provided `segtype`.''' 440 | return type(segment) == segtype 441 | 442 | ## functions 443 | # shamefully ripped from idc.py 444 | def __load_file(filename, ea, size, offset=0): 445 | path = os.path.abspath(filename) 446 | 447 | # use IDA to open up the file contents 448 | # XXX: does IDA support unicode file paths? 449 | res = idaapi.open_linput(path, False) 450 | if not res: 451 | raise E.DisassemblerError(u"{:s}.load_file({!r}, {:#x}, {:+#x}) : Unable to create an `idaapi.loader_input_t` from path \"{:s}\".".format(__name__, filename, ea, size, path)) 452 | 453 | # now we can write the file into the specified address as a segment 454 | ok = idaapi.file2base(res, offset, ea, ea+size, False) 455 | idaapi.close_linput(res) 456 | return ok 457 | 458 | def __save_file(filename, ea, size, offset=0): 459 | path = os.path.abspath(filename) 460 | 461 | # use IDA to open up a file to write to 462 | # XXX: does IDA support unicode file paths? 463 | of = idaapi.fopenWB(path) 464 | if not of: 465 | raise E.DisassemblerError(u"{:s}.save_file({!r}, {:#x}, {:+#x}) : Unable to open target file \"{:s}\".".format(__name__, filename, ea, size, utils.string.escape(path, '"'))) 466 | 467 | # now we can write the segment into the file we opened 468 | res = idaapi.base2file(of, offset, ea, ea+size) 469 | idaapi.eclose(of) 470 | return res 471 | 472 | @utils.string.decorate_arguments('filename') 473 | def load(filename, ea, size=None, offset=0, **kwds): 474 | """Load the specified `filename` to the address `ea` as a segment. 475 | 476 | If `size` is not specified, use the length of the file. 477 | The keyword `offset` represents the offset into the file to use. 478 | The keyword `name` can be used to name the segment. 479 | """ 480 | filesize = os.stat(filename).st_size 481 | 482 | cb = filesize - offset if size is None else size 483 | res = __load_file(utils.string.to(filename), ea, cb, offset) 484 | if not res: 485 | raise E.ReadOrWriteError(u"{:s}.load({!r}, {:#x}, {:+#x}, {:#x}{:s}) : Unable to load file into {:#x}{:+#x} from \"{:s}\".".format(__name__, filename, ea, cb, offset, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', ea, cb, utils.string.escape(os.path.relpath(filename), '"'))) 486 | return new(ea, cb, kwds.get('name', os.path.split(filename)[1])) 487 | 488 | def map(ea, size, newea, **kwds): 489 | """Map `size` bytes of data from `ea` into a new segment at `newea`. 490 | 491 | The keyword `name` can be used to name the segment. 492 | """ 493 | 494 | # grab the file offset and the data we want 495 | fpos, data = idaapi.get_fileregion_offset(ea), database.read(ea, size) 496 | if len(data) != size: 497 | raise E.ReadOrWriteError(u"{:s}.map({:#x}, {:+#x}, {:#x}{:s}) : Unable to read {:#x} bytes from {:#x}.".format(__name__, ea, size, newea, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', size, ea)) 498 | 499 | # rebase the data to the new address 500 | res = idaapi.mem2base(data, newea, fpos) 501 | if not res: 502 | raise E.DisassemblerError(u"{:s}.map({:#x}, {:+#x}, {:#x}{:s}) : Unable to remap {:#x}:{:+#x} to {:#x}.".format(__name__, ea, size, newea, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', ea, size, newea)) 503 | 504 | # now we can create the new segment 505 | return new(newea, size, kwds.get("name", "map_{:x}".format(ea))) 506 | #return create(newea, size, kwds.get("name", "map_{:s}".format(newea>>4))) 507 | 508 | # creation/destruction 509 | @utils.string.decorate_arguments('name') 510 | def new(offset, size, name, **kwds): 511 | """Create a segment at `offset` with `size` and name it according to `name`. 512 | 513 | The keyword `bits` can be used to specify the bit size of the segment 514 | The keyword `comb` can be used to specify any flags (idaapi.sc*) 515 | The keyword `align` can be used to specify paragraph alignment (idaapi.sa*) 516 | The keyword `org` specifies the origin of the segment (must be paragraph aligned due to ida) 517 | """ 518 | res = utils.string.to(name) 519 | 520 | # find the segment according to the name specified by the user 521 | seg = idaapi.get_segm_by_name(res) 522 | if seg is not None: 523 | raise E.DuplicateItemError(u"{:s}.new({:#x}, {:+#x}, \"{:s}\"{:s}) : A segment with the specified name (\"{:s}\") already exists.".format(__name__, offset, size, utils.string.escape(name, '"'), u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', utils.string.escape(name, '"'))) 524 | 525 | # FIXME: use disassembler default bit length instead of 32 526 | bits = kwds.get( 'bits', 32 if idaapi.getseg(offset) is None else idaapi.getseg(offset).abits()) 527 | 528 | ## create a selector with the requested origin 529 | if bits == 16: 530 | org = kwds.get('org',0) 531 | if org & 0xf > 0: 532 | raise E.InvalidTypeOrValueError(u"{:s}.new({:#x}, {:+#x}, {!r}{:s}) : The specified origin ({:#x}) is not aligned to the size of a paragraph (0x10).".format(__name__, offset, size, name, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', org)) 533 | 534 | para = offset // 16 535 | sel = idaapi.allocate_selector(para) 536 | idaapi.set_selector(sel, (para - kwds.get('org', 0) // 16) & 0xffffffff) 537 | 538 | ## if the user specified a selector, then use it 539 | elif 'sel' in kwds or 'selector' in kwds: 540 | sel = kwds.get('sel', kwds.get('selector', idaapi.find_free_selector())) 541 | 542 | ## choose the paragraph size defined by the user 543 | elif 'para' in kwds or 'paragraphs' in kwds: 544 | para = kwds.get('paragraph', kwds.get('para', 1)) 545 | sel = idaapi.setup_selector(para) 546 | 547 | ## find a selector that is 1 paragraph size, 548 | elif idaapi.get_selector_qty(): 549 | sel = idaapi.find_selector(1) 550 | 551 | # otherwise find a free one and set it. 552 | else: 553 | sel = idaapi.find_free_selector() 554 | idaapi.set_selector(sel, 1) 555 | 556 | # populate the segment_t for versions of IDA prior to 7.0 557 | if idaapi.__version__ < 7.0: 558 | seg = idaapi.segment_t() 559 | seg.startEA, seg.endEA = offset, offset + size 560 | 561 | # now for versions of IDA 7.0 and newer 562 | else: 563 | seg = idaapi.segment_t() 564 | seg.start_ea, seg.end_ea = offset, offset + size 565 | 566 | # assign the rest of the necessary attributes 567 | seg.sel = sel 568 | seg.bitness = {16:0,32:1,64:2}[bits] 569 | seg.comb = kwds.get('comb', idaapi.scPub) # public 570 | seg.align = kwds.get('align', idaapi.saRelByte) # paragraphs 571 | 572 | # now we can add our segment_t to the database 573 | res = utils.string.to(name) 574 | ok = idaapi.add_segm_ex(seg, res, "", idaapi.ADDSEG_NOSREG|idaapi.ADDSEG_SPARSE) 575 | if not ok: 576 | ok = idaapi.del_selector(sel) 577 | if not ok: 578 | logging.warning(u"{:s}.new({:#x}, {:+#x}, {!r}{:s}) : Unable to delete the created selector ({:#x}) for the new segment.".format(__name__, offset, size, name, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '', sel)) 579 | raise E.DisassemblerError(u"{:s}.new({:#x}, {:+#x}, {!r}{:s}) : Unable to add a new segment.".format(__name__, offset, size, name, u", {:s}".format(utils.string.kwargs(kwds)) if kwds else '')) 580 | return seg 581 | create = utils.alias(new) 582 | 583 | def remove(segment, contents=False): 584 | """Remove the specified `segment`. 585 | 586 | If the bool `contents` is specified, then remove the contents of the segment from the database. 587 | """ 588 | if not isinstance(segment, idaapi.segment_t): 589 | cls = segment.__class__ 590 | raise E.InvalidParameterError(u"{:s}.remove({!r}) : Expected a `{:s}`, but received a {!s}.".format(__name__, segment, idaapi.segment_t.__name__, cls)) 591 | 592 | # delete the selector defined by the segment_t 593 | res = idaapi.del_selector(segment.sel) 594 | if res == 0: 595 | logging.warning(u"{:s}.remove({!r}) : Unable to delete the selector {:#x}.".format(__name__, segment, segment.sel)) 596 | 597 | # remove the actual segment using the address in the segment_t 598 | res = idaapi.del_segm(interface.range.start(segment), idaapi.SEGMOD_KILL if contents else idaapi.SEGMOD_KEEP) 599 | if res == 0: 600 | logging.warning(u"{:s}.remove({!r}) : Unable to delete the segment {:s} with the selector {:s}.".format(__name__, segment, segment.name, segment.sel)) 601 | return res 602 | delete = utils.alias(remove) 603 | 604 | @utils.string.decorate_arguments('filename') 605 | def save(filename, segment, offset=0): 606 | """Export the segment identified by `segment` to the file named `filename`. 607 | 608 | If the int `offset` is specified, then begin writing into the file at the specified offset. 609 | """ 610 | if isinstance(segment, idaapi.segment_t): 611 | return __save_file(utils.string.to(filename), interface.range.start(segment), size(segment), offset) 612 | return save(filename, by(segment)) 613 | export = utils.alias(save) 614 | 615 | #res = idaapi.add_segment_translation(ea, selector) 616 | #res = idaapi.del_segment_translation(ea) 617 | 618 | -------------------------------------------------------------------------------- /ida-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDAMetadataDescriptorVersion": 1, 3 | "plugin": { 4 | "name": "ida-minsc", 5 | "entryPoint": "idapythonrc.py", 6 | "categories": ["python", "library", "dwim", "hamsters", "portable", "indexed-comments", "API-scripting-and-automation", "vulnerability-research-and-exploit-development", "collaboration-and-productivity", "other"], 7 | "logoPath": "http://arizvisa.github.io/ida-minsc/_images/hamster.svg", 8 | "idaVersions": "<8.5", 9 | "description": "IDA-minsc wraps IDAPython to provide a higher-level context-sensitive pattern-matching api that simplifies many annotation tasks. This augments things like decoding structured data from the database, adds support for indexing, tags, and python types to comments, and allows for arithmetic with things such as structures, bitmasks/enumerations, and much more.", 10 | "version": "0.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /idapythonrc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Internal initialization script 3 | 4 | This is an internal script that is executed when IDA starts. Things 5 | such as meta_path hooks, replacing the namespace with the contents 6 | of the __root__ module, and implementing a work-around for the hack 7 | that IDAPython does with saving the contents of sys.modules. After 8 | initializing everything, this script will then hand off execution 9 | to the user's idapythonrc.py in their home directory. 10 | """ 11 | 12 | # output the IDAPython banner when IDA starts 13 | print_banner() 14 | 15 | # some general python modules that we use for meta_path 16 | import sys, os, imp 17 | import idaapi 18 | 19 | # grab ida's user directory and remove from it from the path since we use 20 | # python's meta_path to locate all of our modules. we also use this path 21 | # to find out where our loader logic is actually located. 22 | root = idaapi.get_user_idadir() 23 | sys.path[:] = [item for item in sys.path if os.path.realpath(item) not in {os.path.realpath(root)}] 24 | 25 | # grab the loader, and then use it to seed python's meta_path. 26 | loader = imp.load_source('__loader__', os.path.join(root, 'plugins', 'minsc.py')) 27 | sys.meta_path.extend(loader.finders()) 28 | 29 | # then we need to patch the version into "idaapi" so that we can 30 | # access it when figuring out which logic we need to use. 31 | loader.patch_version(idaapi) 32 | 33 | # IDA 6.95 obnoxiously replaces the displayhook with their own 34 | # version which makes it so that we can't hook it with ours. 35 | if idaapi.__version__ >= 6.95 and hasattr(ida_idaapi, '_IDAPython_displayhook') and hasattr(ida_idaapi._IDAPython_displayhook, 'orig_displayhook'): 36 | sys.displayhook = ida_idaapi._IDAPython_displayhook.orig_displayhook 37 | del(ida_idaapi._IDAPython_displayhook) 38 | 39 | # replace sys.displayhook with our own so that IDAPython can't 40 | # tamper with our __repr__ implementations. 41 | sys.displayhook = loader.DisplayHook(sys.stdout.write, sys.displayhook).displayhook 42 | 43 | # now we can just load it into the globals() namespace, but we still 44 | # need to preserve it as we'll need one more function after transition. 45 | loader.load(globals(), preserve={'loader', '_orig_stdout', '_orig_stderr'}) 46 | 47 | # now we can start everything up within our namespace and then we can 48 | # just delete the loader afterwards. 49 | loader.startup(globals()) 50 | del(loader) 51 | 52 | ## stupid fucking idapython hax 53 | 54 | # prevent idapython from trying to write its banner to the message window since we called it up above. 55 | print_banner = lambda: None 56 | 57 | # find the frame that fucks with our sys.modules, and save it for later 58 | frame = __import__('sys')._getframe() 59 | while frame.f_code.co_name != 'IDAPython_ExecScript': 60 | frame = frame.f_back 61 | 62 | # inject our current sys.modules state into IDAPython_ExecScript's state if it's the broken version 63 | if 'basemodules' in frame.f_locals: 64 | frame.f_locals['basemodules'].update(__import__('sys').modules) 65 | del(frame) 66 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six>=1.10.0 2 | networkx>=1.10 3 | future>=0.16.0 4 | -------------------------------------------------------------------------------- /tools/general.py: -------------------------------------------------------------------------------- 1 | """ 2 | General module 3 | 4 | This module provides generalized tools that a user may find 5 | useful in their reversing adventures. This includes classes 6 | for performing address translations, coloring marks or tags, 7 | recursively walking through basic blocks until a sentinel 8 | block has been reached, or even recursively walking a 9 | function's childrens until a particular sentinel function 10 | is encountered. 11 | 12 | The tools defined within here are unorganized and pretty 13 | much unmaintained. Thus they may shift around during their 14 | existence as they eventually find their place. 15 | """ 16 | 17 | import six, sys, logging, builtins 18 | import functools, operator, itertools, types 19 | import logging 20 | 21 | import database, function as func, instruction, segment 22 | import ui, internal 23 | 24 | def map(F, **kwargs): 25 | """Execute the callback `F` on all functions in the database. Synonymous to `map(F, database.functions())` but with some extra logging to display the current progress. 26 | 27 | The `F` parameter is defined as a function taking either an 28 | `(address, **kwargs)` or a `(index, address, **kwargs)`. Any 29 | keyword arguments are passed to `F` unmodified. 30 | """ 31 | f1 = lambda idx, ea, **kwargs: F(ea, **kwargs) 32 | f2 = lambda idx, ea, **kwargs: F(idx, ea, **kwargs) 33 | Ff = internal.utils.pycompat.method.function(F) if isinstance(F, types.MethodType) else F 34 | Fc = internal.utils.pycompat.function.code(Ff) 35 | f = f1 if internal.utils.pycompat.code.argcount(Fc) == 1 else f2 36 | 37 | result, all = [], database.functions() 38 | total = len(all) 39 | if len(all): 40 | ea = next(item for item in all) 41 | try: 42 | for i, ea in enumerate(all): 43 | ui.navigation.set(ea) 44 | six.print_("{:#x}: processing # {:d} of {:d} : {:s}".format(ea, 1 + i, total, func.name(ea))) 45 | result.append( f(i, ea, **kwargs) ) 46 | except KeyboardInterrupt: 47 | six.print_("{:#x}: terminated at # {:d} of {:d} : {:s}".format(ea, 1 + i, total, func.name(ea))) 48 | return result 49 | 50 | # For poor folk without a dbgeng 51 | class remote(object): 52 | """ 53 | An object that can be used to translate addresses to and from 54 | a debugging target so that one does not need to rebase their 55 | entire database, or come up with some other tricks to translate 56 | a binary address to its runtime address. 57 | """ 58 | def __init__(self, remote, local=None): 59 | """Create a new instance with the specified `remote` base address. 60 | 61 | If `local` is not specified, then use the current database's base 62 | address for performing calculations. 63 | """ 64 | if local is None: 65 | local = database.config.baseaddress() 66 | self.lbase = local 67 | self.rbase = remote 68 | 69 | def get(self, ea): 70 | '''Translate a remote address to the local database address.''' 71 | offset = ea - self.rbase 72 | return offset + self.lbase 73 | 74 | def put(self, ea): 75 | '''Translate a local database address to the remote address.''' 76 | offset = ea - self.lbase 77 | return offset + self.rbase 78 | 79 | def go(self, ea): 80 | '''Seek the database to the specified remote address.''' 81 | res = self.get(ea) 82 | database.go(res) 83 | 84 | ## XXX: would be useful to have a quick wrapper class for interacting with Ida's mark list 85 | ## in the future, this would be abstracted into a arbitrarily sized tree. 86 | 87 | def colormarks(color=0x7f007f): 88 | """Walk through the current list of marks whilst coloring them with the specified `color`. 89 | 90 | Each mark's address is tagged with its description, and if the 91 | address belongs to a function, the function is also tagged with the 92 | address of the marks that it contains. 93 | """ 94 | # tag and color 95 | f = {item for item in []} 96 | for ea, m in database.marks(): 97 | database.tag(ea, 'mark', m) 98 | if database.color(ea) is None: 99 | database.color(ea, color) 100 | try: 101 | f.add(func.top(ea)) 102 | except internal.exceptions.FunctionNotFoundError: 103 | pass 104 | continue 105 | 106 | # tag the functions too 107 | for ea in f: 108 | m = func.marks(ea) 109 | func.tag(ea, 'marks', [ea for ea, _ in m]) 110 | return 111 | 112 | def recovermarks(): 113 | """Walk through the tags made by ``colormarks`` and re-create the marks that were found. 114 | 115 | This is useful if any marks were accidentally deleted and can be used for 116 | recovering them as long as they were initally tagged properly. 117 | """ 118 | # collect 119 | result = [] 120 | for fn, l in database.select('marks'): 121 | m = {item for item in l['marks']} if hasattr(l['marks'], '__iter__') else {int(item, 16) for item in l['marks'].split(',')} if isinstance(l['marks'], six.string_types) else {l['marks']} 122 | res = [(ea, d['mark']) for ea, d in func.select(fn, 'mark')] 123 | if m != { ea for ea, _ in res }: 124 | logging.warning("{:s} : Ignoring the function tag \"{:s}\" for function {:#x} due to its value being out-of-sync with the contents values ({!s} <> {!s}).".format('.'.join([__name__, 'recovermarks']), fn, builtins.map("{:#x}".format, m), builtins.map("{:#x}".format, {ea for ea, _ in res}))) 125 | result.extend(res) 126 | result.sort(key=lambda item: item[1]) 127 | 128 | # discovered marks versus database marks 129 | result = {ea : item for ea, item in result.items()} 130 | current = {ea : descr for ea, descr in database.marks()} 131 | 132 | # create tags 133 | for x, y in result.items(): 134 | if x in current: 135 | logging.warning("{:#x}: skipping already existing mark : {!r}".format(x, current[x])) 136 | continue 137 | 138 | # x not in current 139 | if x not in current: 140 | logging.info("{:#x}: adding missing mark due to tag : {!r}".format(x, result[x])) 141 | elif current[x] != result[x]: 142 | logging.info("{:#x}: database tag is different than mark description : {!r}".format(x, result[x])) 143 | else: 144 | assert current[x] == result[x] 145 | database.mark(x, y) 146 | 147 | # marks that aren't reachable in the database 148 | for ea in { item for item in current.keys() }.difference({item for item in result.keys()}): 149 | logging.warning("{:#x}: unreachable mark (global) : {!r}".format(ea, current[ea])) 150 | 151 | # color them 152 | colormarks() 153 | 154 | def checkmarks(): 155 | """Emit all functions that contain more than 1 mark within them. 156 | 157 | As an example, if marks are used to keep track of backtraces then 158 | this tool will emit where those backtraces intersect. 159 | """ 160 | listable = [] 161 | for a, m in database.marks(): 162 | try: 163 | listable.append((func.top(a), a, m)) 164 | except internal.exceptions.FunctionNotFoundError: 165 | pass 166 | continue 167 | 168 | d = listable[:] 169 | d.sort(key=lambda item: item[0]) 170 | 171 | flookup = {} 172 | for fn, a, m in d: 173 | try: 174 | flookup[fn].append((a, m)) 175 | except: 176 | flookup[fn] = [(a, m)] 177 | continue 178 | 179 | functions = [ (k, v) for k, v in flookup.items() if len(v) > 1 ] 180 | if not functions: 181 | logging.warning('There are no functions available containing multiple marks.') 182 | return 183 | 184 | for k, v in functions: 185 | six.print_("{:#x} : in function {:s}".format(k, func.name(func.by_address(k))), file=sys.stdout) 186 | six.print_('\n'.join(("- {:#x} : {:s}".format(a, m) for a, m in sorted(v))), file=sys.stdout) 187 | return 188 | 189 | def collect(ea, sentinel): 190 | """Collect all the basic blocks starting at address `ea` and recurse until a terminating block is encountered. 191 | 192 | If the set `sentinel` is specified, then its addresses are used as 193 | sentinel blocks and collection will terminate when those blocks are 194 | reached. 195 | """ 196 | if isinstance(sentinel, (list, tuple)): 197 | sentinel = {item for item in sentinel} 198 | if not all((sentinel, isinstance(sentinel, set))): 199 | raise AssertionError("{:s}.collect({:#x}, {!r}) : Sentinel is empty or not a set.".format(__name__, ea, sentinel)) 200 | def _collect(addr, result): 201 | process = {item for item in []} 202 | for blk in builtins.map(func.block, func.block.after(addr)): 203 | if any(blk in coll for coll in [result, sentinel]): 204 | continue 205 | process.add(blk) 206 | for addr, _ in process: 207 | result |= _collect(addr, result | process) 208 | return result 209 | addr, _ = blk = func.block(ea) 210 | return _collect(addr, {blk}) 211 | 212 | def collectcall(ea, sentinel=set()): 213 | """Collect all of the function calls starting at function `ea` and recurse until a terminating function is encountered. 214 | 215 | If the set `sentinel` is specified, then its addresses are used as 216 | sentinel functions and collection will terminate when one of those 217 | functions are reached. 218 | """ 219 | if isinstance(sentinel, (list, tuple)): 220 | sentinel = {item for item in sentinel} 221 | if not isinstance(sentinel, set): 222 | raise AssertionError("{:s}.collectcall({:#x}, {!r}) : Sentinel is not a set.".format(__name__, ea, sentinel)) 223 | def _collectcall(addr, result): 224 | process = {item for item in []} 225 | for f in func.down(addr): 226 | if any(f in coll for coll in [result, sentinel]): 227 | continue 228 | if not func.within(f): 229 | logging.warning("{:s}.collectcall({:#x}, {!r}) : Adding non-function address {:#x} ({:s}).".format(__name__, ea, sentinel, f, database.name(f))) 230 | result.add(f) 231 | continue 232 | process.add(f) 233 | for addr in process: 234 | result |= _collectcall(addr, result | process) 235 | return result 236 | addr = func.top(ea) 237 | return _collectcall(addr, {addr}) 238 | 239 | # FIXME: Don't emit the +0 if offset is 0 240 | def above(ea, includeSegment=False): 241 | '''Return all of the function names and their offset that calls the function at `ea`.''' 242 | tryhard = lambda ea: "{:s}{:+x}".format(func.name(func.top(ea)), ea - func.top(ea)) if func.within(ea) else "{:+x}".format(ea) if func.name(ea) is None else func.name(ea) 243 | return '\n'.join(':'.join([segment.name(ea), tryhard(ea)] if includeSegment else [tryhard(ea)]) for ea in func.up(ea)) 244 | 245 | # FIXME: Don't emit the +0 if offset is 0 246 | def below(ea, includeSegment=False): 247 | '''Return all of the function names and their offset that are called by the function at `ea`.''' 248 | tryhard = lambda ea: "{:s}{:+x}".format(func.name(func.top(ea)), ea - func.top(ea)) if func.within(ea) else "{:+x}".format(ea) if func.name(ea) is None else func.name(ea) 249 | return '\n'.join(':'.join([segment.name(ea), tryhard(ea)] if includeSegment else [tryhard(ea)]) for ea in func.down(ea)) 250 | 251 | # FIXME: this only works on x86 where args are pushed via stack 252 | def makecall(ea=None, target=None): 253 | """Output the function call at `ea` and its arguments with the address they originated from. 254 | 255 | If `target` is specified, then assume that the instruction is 256 | calling `target` instead of the target address that the call 257 | is referencing. 258 | """ 259 | ea = current.address() if ea is None else ea 260 | if not func.contains(ea, ea): 261 | return None 262 | 263 | if database.config.bits() != 32: 264 | raise RuntimeError("{:s}.makecall({!r}, {!r}) : Unable to determine arguments for {:s} due to {:d}-bit calling convention.".format(__name__, ea, target, database.disasm(ea), database.config.bits())) 265 | 266 | if target is None: 267 | # scan down until we find a call that references something 268 | chunk, = ((l, r) for l, r in func.chunks(ea) if l <= ea <= r) 269 | result = [] 270 | while (len(result) < 1) and ea < chunk[1]: 271 | # FIXME: it's probably not good to just scan for a call 272 | if not database.instruction(ea).startswith('call '): 273 | ea = database.next(ea) 274 | continue 275 | result = database.cxdown(ea) 276 | if len(result) == 0: raise TypeError("{:s}.makecall({!r}, {!r}) : Unable to determine number of arguments.".format(__name__, ea, target)) 277 | 278 | if len(result) != 1: 279 | raise ValueError("{:s}.makecall({!r}, {!r}) : An invalid number of targets was returned for the call at {:#x}. The call targets that were returned are {!r}.".format(__name__, ea, result)) 280 | fn, = result 281 | else: 282 | fn = target 283 | 284 | try: 285 | result = [] 286 | for offset, name, size in func.arguments(fn): 287 | left = database.address.prevstack(ea, offset + database.config.bits() // 8) 288 | # FIXME: if left is not an assignment or a push, find last assignment 289 | result.append((name, left)) 290 | except internal.exceptions.OutOfBoundsError: 291 | raise internal.exceptions.OutOfBoundserror("{:s}.makecall({!r}, {!r}) : Unable to get arguments for target function.".format(__name__, ea, target)) 292 | 293 | # FIXME: replace these crazy list comprehensions with something more comprehensible. 294 | # result = ["{:s}={:s}".format(name, instruction.op_repr(ea, 0)) for name, ea in result] 295 | result = ["({:#x}){:s}={:s}".format(ea, name, ':'.join(instruction.op_repr(database.address.prevreg(ea, instruction.op_value(ea, 0), write=True), n) for n in instruction.opsi_read(database.address.prevreg(ea, instruction.op_value(ea, 0), write=True))) if instruction.op_type(ea, 0) == 'reg' else instruction.op_repr(ea, 0)) for name, ea in result] 296 | 297 | try: 298 | return "{:s}({:s})".format(internal.declaration.demangle(func.name(func.by_address(fn))), ','.join(result)) 299 | except: 300 | pass 301 | return "{:s}({:s})".format(internal.declaration.demangle(database.name(fn)), ','.join(result)) 302 | -------------------------------------------------------------------------------- /tools/tagfix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tagfix module 3 | 4 | This module is provided to a user to allow one to rebuild the 5 | cache that is built when a database is finished processing. If 6 | the cache is corrupted through some means, this module can be 7 | used to rebuild the tag-cache by manually scanning the currently 8 | defined tags and resetting its references in order to allow one 9 | to query again. 10 | 11 | To manually rebuild the cache for the database, use the following:: 12 | 13 | > tools.tagfix.everything() 14 | 15 | Likewise to rebuild the cache for just the globals or the contents:: 16 | 17 | > tools.tagfix.globals() 18 | > tools.tagfix.contents() 19 | 20 | """ 21 | 22 | import six, sys, logging, builtins 23 | import functools, operator, itertools, types 24 | 25 | import database as db, function as func, ui 26 | import internal 27 | 28 | import idaapi 29 | output = sys.stderr 30 | 31 | def fetch_contents(fn): 32 | """Fetch the number of references for the contents of function `fn` from the database. 33 | 34 | Returns the tuple `(function, address, tags)` where the `address` and 35 | `tags` items are both dictionaries containing the number of references 36 | for the addresses and tag names. The `function` item contains the address 37 | of the function whose references were counted. 38 | """ 39 | address, tags = {}, {} 40 | 41 | for ea in map(ui.navigation.analyze, func.iterate(fn)): 42 | items = db.tag(ea) 43 | 44 | # tally up all of the reference counts from the dictionary that we 45 | # fetched for the current address we're iterating through. 46 | for name in items: 47 | address[ea] = address.get(ea, 0) + 1 48 | tags[name] = tags.get(name, 0) + 1 49 | continue 50 | return func.address(fn), address, tags 51 | 52 | def fetch_globals_functions(): 53 | """Fetch the number of references for all of the global tags (functions) from the database. 54 | 55 | Returns the tuple `(address, tags)` where the `address` and `tags` items 56 | are both dictionaries containing the number of references for both 57 | addresses and tag names for each function from the database. 58 | """ 59 | address, tags = {}, {} 60 | functions = [item for item in db.functions()] 61 | for i, ea in enumerate(map(ui.navigation.analyze, functions)): 62 | items = func.tag(ea) 63 | six.print_(u"globals: counting the tags assigned to function {:#x} : {:d} of {:d}".format(ea, 1 + i, len(functions)), file=output) 64 | 65 | # iterate through all of the items in the tags that we decoded, and 66 | # tally up their keys in order to return their reference count. 67 | # them. once decoded then we can just iterate through their keys and 68 | for name in items: 69 | address[ea] = address.get(ea, 0) + 1 70 | tags[name] = tags.get(name, 0) + 1 71 | continue 72 | return address, tags 73 | 74 | def fetch_globals_data(): 75 | """Fetch the number of references for all of the global tags (non-functions) from the database. 76 | 77 | Returns a tuple `(address, tags)` where the `address` and `tags` items 78 | are both dictionaries containing the number of references for both 79 | addresses and tag names for every non-function in the database. 80 | """ 81 | address, tags = {}, {} 82 | left, right = db.config.bounds() 83 | six.print_(u'globals: counting any tags that are assigned to global data', file=output) 84 | for ea in map(ui.navigation.analyze, db.address.iterate(left, right)): 85 | if func.within(ea): 86 | continue 87 | items = db.tag(ea) 88 | 89 | # after grabbing the tags for the current address we're iterating 90 | # through, tally up the number of keys and their values. 91 | for name in items: 92 | address[ea] = address.get(ea, 0) + 1 93 | tags[name] = tags.get(name, 0) + 1 94 | continue 95 | return address, tags 96 | 97 | def fetch_globals(): 98 | """Fetch the number of references for all of the global tags associated with both functions and non-functions from the database. 99 | 100 | Returns the tuple `(address, tags)` where the `address` and `tags` 101 | items are both dictionaries containing the number of references 102 | for both addresses and tag names. 103 | """ 104 | # Read both the address and tags from all functions and globals. 105 | faddr, ftags = fetch_globals_functions() 106 | daddr, dtags = fetch_globals_data() 107 | 108 | # Consolidate tags into individual dictionaries. 109 | six.print_(u'globals: tallying up the database tags for building the index', file=output) 110 | 111 | address, tags = {}, {} 112 | for results, item in itertools.chain(zip(2 * [address], [faddr, daddr]), zip(2 * [tags], [ftags, dtags])): 113 | matching = {ea for ea in results} & {ea for ea in item} 114 | missing = {ea for ea in item} - {ea for ea in results} 115 | 116 | # Update all of the keys that aren't in both our results 117 | # and our items, and add up the ones that are. 118 | results.update({ea : item[ea] for ea in missing}) 119 | results.update({ea : results[ea] + item[ea] for ea in matching}) 120 | 121 | # Output our results to the specified output. 122 | six.print_(u"globals: found {:d} addresses to include in index".format(len(address)), file=output) 123 | six.print_(u"globals: found {:d} tags to include in index".format(len(tags)), file=output) 124 | return address, tags 125 | 126 | def contents(ea): 127 | '''Generate the cache for the contents of the function `ea`.''' 128 | try: 129 | func.address(ea) 130 | except internal.exceptions.FunctionNotFoundError: 131 | logging.warning(u"{:s}.contents({:#x}): Unable to fetch cache the for the address {:#x} as it is not a function.".format('.'.join([__name__]), ea, ea)) 132 | return {}, {} 133 | 134 | # Read the addresses and tags from the contents of the function. 135 | logging.debug(u"{:s}.contents({:#x}): Fetching the cache for the function {:#x}.".format('.'.join([__name__]), ea, ea)) 136 | f, address, tags = fetch_contents(ui.navigation.procedure(ea)) 137 | 138 | # Update the addresses and tags for the contents of the function. 139 | ui.navigation.set(ea) 140 | logging.debug(u"{:s}.contents({:#x}): Updating the name references in the cache belonging to function {:#x}.".format('.'.join([__name__]), ea, ea)) 141 | for k, v in tags.items(): 142 | internal.comment.contents.set_name(f, k, v, target=f) 143 | 144 | logging.debug(u"{:s}.contents({:#x}): Updating the address references in the cache belonging to function {:#x}.".format('.'.join([__name__]), ea, ea)) 145 | for k, v in address.items(): 146 | if not func.within(k): 147 | continue 148 | internal.comment.contents.set_address(k, v, target=f) 149 | 150 | return address, tags 151 | 152 | def globals(): 153 | '''Build the index of references for all of the globals in the database.''' 154 | 155 | # Read all of the data tags for each function and address. 156 | address, tags = fetch_globals() 157 | 158 | # Update the the index containing the address and tags we counted. 159 | six.print_(u'globals: updating the name references in the index for the database', file=output) 160 | for k, v in tags.items(): 161 | internal.comment.globals.set_name(k, v) 162 | 163 | six.print_(u'globals: updating the address references in the index for the database', file=output) 164 | for k, v in address.items(): 165 | internal.comment.globals.set_address(k, v) 166 | 167 | return address, tags 168 | 169 | def all(): 170 | '''Build the index of references for all the globals and generate the caches for every function in the database.''' 171 | functions = [item for item in db.functions()] 172 | 173 | # process all function contents tags 174 | for i, ea in enumerate(functions): 175 | six.print_(u"updating the cache for the tags belonging to function ({:#x}) : {:d} of {:d}".format(ea, 1 + i, len(functions)), file=output) 176 | _, _ = contents(ea) 177 | 178 | # process all global tags 179 | six.print_(u'updating the index for the database with references for all globals', file=output) 180 | _, _ = globals() 181 | 182 | def customnames(): 183 | '''Iterate through all of the "custom" names within the database and update their references in either the index or their associated function cache.''' 184 | # FIXME: first delete all the custom names '__name__' tag 185 | left, right = db.config.bounds() 186 | for ea in db.address.iterate(left, right): 187 | ctx = internal.comment.contents if func.within(ea) and func.address(ea) != ea else internal.comment.globals 188 | if db.type.has_customname(ea): 189 | ctx.inc(ea, '__name__') 190 | continue 191 | return 192 | 193 | def extracomments(): 194 | '''Iterate through all of the "extra" comments within the database and update their references in either the index or their associated function cache.''' 195 | left, right = db.config.bounds() 196 | for ea in db.address.iterate(left, right): 197 | ctx = internal.comment.contents if func.within(ea) else internal.comment.globals 198 | 199 | count = db.extra.__count__(ea, idaapi.E_PREV) 200 | if count: [ ctx.inc(ea, '__extra_prefix__') for i in range(count) ] 201 | 202 | count = db.extra.__count__(ea, idaapi.E_NEXT) 203 | if count: [ ctx.inc(ea, '__extra_suffix__') for i in range(count) ] 204 | return 205 | 206 | def everything(): 207 | '''Rebuild the index for all of the globals and the cache for each function from the database.''' 208 | erase() 209 | all() 210 | 211 | def erase_globals(): 212 | '''Remove the contents of the index from the database which is used for storing information about the global tags.''' 213 | node = internal.comment.tagging.node() 214 | hashes, alts, sups = map(list, (iterator(node) for iterator in [internal.netnode.hash.fiter, internal.netnode.alt.fiter, internal.netnode.sup.fiter])) 215 | total = sum(map(len, [hashes, alts, sups])) 216 | 217 | yield total 218 | 219 | current = 0 220 | for idx, k in enumerate(hashes): 221 | internal.netnode.hash.remove(node, k) 222 | yield current + idx, k 223 | 224 | current += len(hashes) 225 | for idx, ea in enumerate(sups): 226 | internal.netnode.sup.remove(node, ea) 227 | yield current + idx, ea 228 | 229 | current += len(sups) 230 | for idx, ea in enumerate(alts): 231 | internal.netnode.alt.remove(node, ea) 232 | yield current + idx, ea 233 | return 234 | 235 | def erase_contents(): 236 | '''Remove the cache associated with each function from the database.''' 237 | functions = [item for item in db.functions()] 238 | total, tag = len(functions), internal.comment.contents.btag 239 | yield total 240 | 241 | for idx, ea in enumerate(map(ui.navigation.set, functions)): 242 | internal.netnode.blob.remove(ea, tag) 243 | yield idx, ea 244 | return 245 | 246 | def erase(): 247 | '''Erase the index of all the globals and the cache associated with each function from the database.''' 248 | iter1, iter2 = erase_contents(), erase_globals() 249 | total = sum(map(next, [iter1, iter2])) 250 | 251 | current = 0 252 | for idx, ea in iter1: 253 | six.print_(u"removing the cache for function {:#x} : {:d} of {:d}".format(ea, 1 + idx, total), file=output) 254 | 255 | res = idx + 1 256 | for idx, addressOrName in iter2: 257 | format = "address {:#x}".format if isinstance(addressOrName, six.integer_types) else "tagname {!r}".format 258 | six.print_(u"removing the global {:s} from the index : {:d} of {:d}".format(format(addressOrName), 1 + res + idx, total), file=output) 259 | return 260 | 261 | def verify_index(): 262 | '''Iterate through the index and verify that each contents entry is pointing at the right functions.''' 263 | cls, ok = internal.comment.contents, True 264 | 265 | # Iterate through the entire index of contents. 266 | for ea, available in cls.iterate(): 267 | if not func.within(ea): 268 | ok, _ = False, six.print_(u"[{:#x}] the item in the index ({:#x}) has been orphaned and is not associated with a function".format(ea, ea), file=output) 269 | continue 270 | 271 | # Verify the owner of the address the cache is stored in 272 | # actually belongs to the correct function. 273 | f = ui.navigation.analyze(func.address(ea)) 274 | if f != ea: 275 | ok, _ = False, six.print_(u"[{:#x}] the item has the wrong parent ({:#x}) and should be owned by {:#x}".format(ea, ea, f), file=output) 276 | continue 277 | 278 | # Verify the keys inside the cache are only ones that we know about. 279 | expected = {key for key in [cls.__tags__, cls.__address__]} 280 | keys = {key for key in available} 281 | if keys - expected: 282 | ok, _ = False, six.print_(u"[{:#x}] the index item for this function contains unsupported keys ({:s})".format(ea, ', '.join(sorted(keys - expected))), file=output) 283 | continue 284 | 285 | # Make sure that both keys are contained within the cache. 286 | if keys != expected: 287 | ok, _ = False, six.print_(u"[{:#x}] the index item for this function contains keys ({:s}) that do not match the requirements ({:s})".format(ea, ', '.join(keys), ', '.join(expected)), file=output) 288 | continue 289 | return ok 290 | 291 | def verify_content(ea): 292 | '''Iterate through the contents cache for an individual function and verify that the addresses in its cache are correct.''' 293 | cls = internal.comment.contents 294 | try: 295 | cache = cls._read(ea, ea) 296 | 297 | # We should be within a function, otherwise this can't be verified. 298 | except internal.exceptions.FunctionNotFoundError: 299 | six.print_(u"[{:#x}] unable to read the cache for the requested address {:#x}".format(ea, ea), file=output) 300 | return False 301 | 302 | # If there was no cache, then we can just immediately return. 303 | if cache is None: 304 | six.print_(u"[{:#x}] the requested address ({:#x}) does not contain a cache".format(ea, ea), file=output) 305 | return False 306 | 307 | # Grab the keys from the cache in order to cross-check them. 308 | expected, available = {key for key in [cls.__tags__, cls.__address__]}, {key for key in cache} 309 | 310 | # Verify that the keys in our cache match what we expect. 311 | if available - expected: 312 | six.print_(u"[{:#x}] the cache at {:#x} contains unsupported keys ({:s})".format(ea, ea, ', '.join(sorted(available - expected))), file=output) 313 | return False 314 | 315 | # Ensure that the cache definitely contains the keys we expect. 316 | if available != expected: 317 | six.print_(u"[{:#x}] the cache at {:#x} contains keys ({:s}) that do not meet the requirements ({:s})".format(ea, ea, ', '.join(available), ', '.join(expected)), file=output) 318 | return False 319 | 320 | # If we're not within a function, then we need to bail because 321 | # the next tests can't possibly succeed. 322 | if not func.within(ea): 323 | six.print_(u"[{:#x}] the cache at {:#x} is not part of a function".format(ea, ea), file=output) 324 | return False 325 | f = func.address(ea) 326 | 327 | # If we verify that the addresses in the cache are all within the 328 | # function that the cache is associated with, then we're done. 329 | if not builtins.all(func.contains(f, item) for item in cache[cls.__address__]): 330 | missed = {item for item in cache[cls.__address__] if not func.contains(f, item)} 331 | six.print_(u"[{:#x}] the cache references {:d} address{:s} that are not owned by function {:#x}".format(ea, len(missed), '' if len(missed) == 1 else 'es', f), file=output) 332 | 333 | # Otherwise, some of the addresses are pointing to the wrong place. 334 | for index, item in enumerate(sorted(missed)): 335 | six.print_(u"[{:#x}] item {:d} of {:d} at {:#x} should be owned by {:#x} but {:s}".format(ea, 1 + index, len(missed), item, f, "is in {:#x}".format(func.address(item)) if func.within(item) else 'is not in a function'), file=output) 336 | return False 337 | 338 | # Iterate through the cache for a function and store all of the tags 339 | # that are available for each address. We also keep track of the implicit 340 | # tags because we're going to do some quirky things to adjust for them. 341 | results, implicit = {}, {key : [] for key in ['__typeinfo__', '__name__']} 342 | for ea in cache[cls.__address__]: 343 | items, empty = {key for key in db.tag(ea)}, {item for item in []} 344 | for name in items: 345 | results.setdefault(ea, empty).add(name) 346 | 347 | # Find the intersection of our tags with the keys for the implicit 348 | # tags so that we can remember their addresses and query them later. 349 | for name in {key for key in implicit} & items: 350 | implicit[name].append(ea) 351 | continue 352 | 353 | # Sanity check the addresses in our implicit collection as we convert 354 | # them into a set for a quick membership test. This shouldn't happen, 355 | # but when verifying things without having to worry about performance 356 | # cost I don't think it causes too much pain. 357 | for key in implicit: 358 | items = {item for item in implicit[key]} 359 | if len(items) != len(implicit[key]): 360 | counts = {ea : len([ea for ea in group]) for ea, group in itertools.groupby(implicit[key])} 361 | six.print_(u"[{:#x}] duplicate addresses were discovered for implicit tag {!r} at: {:s}".format(f, key, ', '.join(ea for ea, count in counts if count > 1)), file=output) 362 | implicit[key] = items 363 | 364 | # Now we need to do some quirky things to handle some of the implicit 365 | # tags that are associated with the first address. 366 | for key, locations in implicit.items(): 367 | count = cache[cls.__tags__].get(key, 0) 368 | 369 | # If the number of locations does not match up to the reference 370 | # count in the cache, then we also discard as it doesn't match up. 371 | if operator.contains(locations, f) and len(locations) > count: 372 | results[f].discard(key) 373 | continue 374 | continue 375 | 376 | # Last thing to do is to convert the results that we fixed up into 377 | # actual counts so that we can check them individually. 378 | tags, address = {}, {} 379 | for ea, keys in results.items(): 380 | count = 0 381 | for item in keys: 382 | tags[item] = tags.get(item, 0) + 1 383 | count += 1 384 | address[ea] = count 385 | 386 | # First we'll verify the address counts. 387 | expected, available = {ea for ea in cache[cls.__address__]}, {ea for ea in address} 388 | if expected != available: 389 | additional, missing = sorted(available - expected), sorted(expected - available) 390 | six.print_(u"[{:#x}] the address cache for {:#x} is desynchronized and {:s} addresses...".format(f, f, "contains {:d} additional and {:d} missing".format(len(additional), len(missing)) if additional and missing else "is missing {:d}".format(len(missing)) if missing else "has {:d} additional".format(len(additional))), file=output) 391 | if additional: 392 | six.print_(u"[{:#x}] ...the additional addresses are: {:s}".format(f, ', '.join(map("{:#x}".format, additional))), file=output) 393 | if missing: 394 | six.print_(u"[{:#x}] ...the addresses that are missing are: {:s}".format(f, ', '.join(map("{:#x}".format, missing))), file=output) 395 | return False 396 | 397 | # Then we'll verify the tag names. 398 | expected, available = {key for key in cache[cls.__tags__]}, {key for key in tags} 399 | if expected != available: 400 | additional, missing = sorted(available - expected), sorted(expected - available) 401 | six.print_(u"[{:#x}] the name cache for {:#x} is desynchronized and {:s} keys...".format(f, f, "contains {:d} additional and {:d} missing".format(len(additional), len(missing)) if additional and missing else "is missing {:d}".format(len(missing)) if missing else "has {:d} additional".format(len(additional))), file=output) 402 | if additional: 403 | six.print_(u"[{:#x}] ...the additional keys are: {:s}".format(f, ', '.join(map("{!r}".format, additional))), file=output) 404 | if missing: 405 | six.print_(u"[{:#x}] ...the keys that are missing are: {:s}".format(f, ', '.join(map("{!r}".format, missing))), file=output) 406 | return False 407 | 408 | # If those were all right, then all critical checks are complete and we 409 | # can check on the reference counts. Starting with the tag names... 410 | for key in expected & available: 411 | expected = cache[cls.__tags__] 412 | if expected[key] != tags[key]: 413 | six.print_(u"[{:#x}] expected to find {:d} reference{:s} to tag {!r}, whereas {:s} found within the function".format(f, expected[key], '' if expected[key] == 1 else 's', key, "{:d} was".format(tags[key]) if tags[key] == 1 else "{:d} were".format(tags[key])), file=output) 414 | continue 415 | 416 | # Now we can compare the address reference counts. 417 | expected, available = {ea for ea in cache[cls.__address__]}, {ea for ea in address} 418 | for ea in map(ui.navigation.analyze, expected & available): 419 | count, expected = address[ea], cache[cls.__address__] 420 | 421 | # This should compare exactly. So if the count doesn't match, let someone know. 422 | if count != expected[ea]: 423 | six.print_(u"[{:#x}] expected to find {:d} reference{:s} to address {:#x}, whereas {:s} found within the function".format(f, expected[ea], '' if expected[ea] == 1 else 's', ea, "{:d} was".format(count) if count == 1 else "{:d} were".format(count)), file=output) 424 | continue 425 | return True 426 | 427 | def verify_globals(): 428 | '''Verify the globals for every address from the database.''' 429 | cls = internal.comment.globals 430 | 431 | # Calculate all the possible combinations for the implicit tags so that 432 | # we can use them to figure out which variation will match. 433 | implicit = {item for item in ['__typeinfo__', '__name__', '__extra_prefix__', '__extra_suffix__']} 434 | combinations = [{item for item in combination} for combination in itertools.chain(*(itertools.combinations(implicit, length) for length in range(1 + len(implicit))))] 435 | unique = {item for item in map(tuple, combinations)} 436 | available = sorted({item for item in items} for items in unique) 437 | ok, counts, results = True, {}, {} 438 | 439 | # Iterate through the index for the globals and tally up the counts 440 | # of each tag at the given address. We default with db.tag to fetch 441 | # them and switch it up only if a function is detected. 442 | for ea, count in cls.iterate(): 443 | Ftags = db.tag 444 | 445 | # First figure out how to validate the address. If it's a function, 446 | # then we can use func.address. 447 | if func.within(ea): 448 | f = func.address(ea) 449 | if f != ea: 450 | six.print_(u"[{:#x}] the item in the global index ({:#x}) is not at the beginning of a function ({:#x})".format(ea, ea, f), file=output) 451 | 452 | # We can now force the address to point to the actual function 453 | # address because func.tag will correct this anyways. 454 | ea, Ftags = f, func.tag 455 | 456 | # In this case we must be a global and we need to use a combination 457 | # of database.contains, and then interface.address.head. 458 | elif not db.within(ea): 459 | ok, _ = False, six.print_(u"[{:#x}] the item in the global index ({:#x}) is not within the boundaries of the database".format(ea, ea), file=output) 460 | continue 461 | 462 | # If we're in the bounds of the database, then we can always succeed 463 | # as db.tag will correct the address regardless of what we do. 464 | elif internal.interface.address.head(ea, silent=True) != ea: 465 | six.print_(u"[{:#x}] the item in the global index ({:#x}) is not pointing at the head of its address ({:#x})".format(ea, ea, internal.interface.address.head(ea, silent=True)), file=output) 466 | 467 | # Now we can align its address and count the number of tags. 468 | ea = internal.interface.address.head(ui.navigation.set(ea), silent=True) 469 | expected = {tag for tag in Ftags(ea)} 470 | 471 | # When we do this, we have to figure out whether the implicit tags 472 | # were actually indexed which we accomplish by generating all possible 473 | # combinations and figuring out which one is the right one. 474 | matches = [combination for combination in available if combination & expected == combination] 475 | if count in {len(expected - match) for match in matches}: 476 | candidates = [match for match in matches if len(expected - match) == count] 477 | logging.debug(u"{:s}.verify_globals(): Found {:d} candidate{:s} for the tags ({:s}) belonging to the {:s} at {:#x} that would result in a proper count of {:d} reference{:s}.".format('.'.join([__name__]), len(candidates), '' if len(candidates) == 1 else 's', ', '.join(map("{!r}".format, expected)), 'function' if func.within(ea) else 'address', ea, count, '' if count == 1 else 's')) 478 | format = functools.partial(u"{:s}.verify_globals(): ...Candidate #{:d} would remove {:s}{:s} resulting in: {:s}.".format, '.'.join([__name__])) 479 | [logging.debug(format(1 + index, "{:d} tag".format(len(listable)) if len(listable) == 1 else "{:d} tags".format(len(listable)), ", {:s}{:s}".format(', '.join(map("{!r}".format, listable[:-1])), ", and {!r},".format(*listable[-1:]) if len(listable) > 1 else ", {!r},".format(*listable)) if listable else '', ', '.join(map("{!r}".format, expected - candidate)))) for index, (candidate, listable) in enumerate(zip(candidates, map(sorted, candidates)))] 480 | 481 | # If the count wasn't in our list of possible matches, then this address 482 | # has a bunk reference count and we need to explain the to the user. 483 | else: 484 | # FIXME: Make sure this it outputting the results properly. 485 | smallest, largest = min(available, key=len) if available else {item for item in []}, max(available, key=len) if available else {item for item in []} 486 | if len(largest) == len(smallest): 487 | format = "{:d} reference".format if len(expected) == 1 else "{:d} references".format 488 | elif len(largest) > len(smallest): 489 | format = "{:d} to {:d} references".format if len(largest) - len(smallest) > 0 and len(expected) > 0 else "{:d} references".format 490 | else: 491 | format = "{:d} references".format 492 | ok, _ = False, six.print_(u"[{:#x}] expected to find {:d} reference{:s} at {:s} {:#x}, but found {:s} instead".format(ea, count, '' if count == 1 else 's', 'function' if func.within(ea) else 'address', ea, format(len(expected - largest), len(expected - smallest)))) 493 | 494 | # First tally up all of the counts that aren't affected by implicit tags. 495 | for key in expected - implicit: 496 | counts[key] = counts.get(key, 0) + 1 497 | 498 | # Now we need to tally the implicit tags for the given address. We key 499 | # this by the index of the available combinations so that we have multiple 500 | # counts for each set of implicit tags that we can later compare. 501 | for index, choice in enumerate(available): 502 | for key in expected & choice: 503 | candidates = results.setdefault(key, {}) 504 | candidates[index] = candidates.get(index, 0) + 1 505 | continue 506 | continue 507 | 508 | # That was everything, now we just got to verify our global number of 509 | # references for each specific tag that isn't implicit. 510 | references = {key : count for key, count in cls.counts()} 511 | tags = {tag for tag in references} 512 | for key in tags - implicit: 513 | count = references[key] 514 | if key not in counts: 515 | ok, _ = False, six.print_(u"[{:s}] unable to locate the referenced tag ({!r}) in the database index".format(key, key)) 516 | elif count != counts[key]: 517 | ok, _ = False, six.print_(u"[{:s}] expected to find {:d} reference{:s} for the explicit tag {!r}, whereas {:s} found within the database.".format(key, count, '' if count == 1 else 's', key, "{:d} was".format(counts[key]) if counts[key] == 1 else "{:d} were".format(counts[key])), file=output) 518 | continue 519 | 520 | # The very last thing to do is to verify the tag counts for the implicit 521 | # tags. This requires us to go through the results and find an index that 522 | # matches what was written into the global index. 523 | for key in tags & implicit: 524 | count, candidates = references[key], {candidate for _, candidate in results.get(key, {}).items()} 525 | logging.debug(u"{:s}.verify_globals(): Found {:d} candidate{:s} ({:s}) for the implicit tag ({!r}) while searching for a count of {:d}.".format('.'.join([__name__]), len(candidates), '' if len(candidates) == 1 else 's', ', '.join(map("{:d}".format, sorted(candidates))), key, count)) 526 | if not candidates: 527 | ok, _ = False, six.print_(u"[{:s}] unable to locate the referenced implicit tag ({!r}) in the database index".format(key, key)) 528 | elif not operator.contains(candidates, count): 529 | ok, _ = False, six.print_(u"[{:s}] expected to find {:d} reference{:s} for the implicit tag ({!r}) in the list of candidates ({:s})".format(key, count, '' if count == 1 else 's', key, ', '.join(map("{:d}".format, candidates))), file=output) 530 | continue 531 | return ok 532 | 533 | def verify_contents(): 534 | '''Verify the contents of every single function in the index.''' 535 | index = sorted({ea for ea, _ in internal.comment.contents.iterate()}) 536 | 537 | # Verify the index as the very first thing. 538 | ok = verify_index() 539 | if not ok: 540 | six.print_(u'some issues were found within the index... ignoring them and proceeding to verify each cache referenced by the index', file=output) 541 | 542 | # Now we can iterate through the index and process each function's contents. 543 | i = count = 0 544 | for i, ea in enumerate(index): 545 | ok = verify_content(ui.navigation.set(ea)) 546 | count += 1 if ok else 0 547 | return count, len(index) 548 | 549 | def verify(): 550 | '''Use the index to verify the reference counts for the globals, functions, and the caches containing their contents.''' 551 | verified, available = verify_contents() 552 | ok = verify_globals() 553 | six.print_(u"Verification of globals has {:s}. Successfully verified{:s} {:d} of {:d} indexed functions.".format('succeeded' if ok else 'failed', ' only' if verified < available else '', verified, available)) 554 | return ok and verified == available 555 | 556 | __all__ = ['everything', 'globals', 'contents'] 557 | -------------------------------------------------------------------------------- /tools/tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tags module 3 | 4 | This module exposes tools for exporting the currently defined tags 5 | within the database. Once exported, these tags can then be pickled 6 | or then re-applied to the same or another database. Some options 7 | are allowed which will let a user apply translations to the tags 8 | before applying them to a target database. 9 | 10 | To fetch all of the tags from the database:: 11 | 12 | > res = tools.tags.read() 13 | 14 | To export only specific tags from the database:: 15 | 16 | > res = tools.tags.export('tag1', 'tag2', ...) 17 | 18 | To apply previously read tags to the database:: 19 | 20 | > tools.tags.apply(res) 21 | 22 | To apply previously read tags with different names to the database:: 23 | 24 | > tools.tags.apply(res, tag1='my_tag1', tag2='my_tag2', ...) 25 | 26 | """ 27 | 28 | import six, sys, logging, builtins 29 | import functools, operator, itertools, types, string 30 | 31 | import database as db, function as func, structure as struc, ui 32 | import internal 33 | 34 | output = sys.stderr 35 | 36 | ### miscellaneous tag utilities 37 | def list(): 38 | '''Return the tags for the all of the function contents within the database as a set.''' 39 | return {res for res in itertools.chain(*(res for _, res in db.selectcontents()))} 40 | 41 | ### internal utility functions and classes 42 | def lvarNameQ(name): 43 | '''Determine whether a `name` is something that IDA named automatically.''' 44 | if any(name.startswith(item) for item in ['arg_', 'var_']): 45 | res = name.split('_', 1)[-1] 46 | return all(item in string.hexdigits for item in res) 47 | elif name.startswith(' '): 48 | return name[1:] in {'s', 'r'} 49 | return False 50 | 51 | def locationToAddress(loc): 52 | '''Convert the function location `loc` back into an address.''' 53 | 54 | ## if location is a tuple, then convert it to an address 55 | if isinstance(loc, tuple): 56 | f, cid, ofs = loc 57 | base, _ = next(b for i, b in enumerate(func.chunks(f)) if i == cid) 58 | return base + ofs 59 | 60 | ## otherwise, it's already an address 61 | return loc 62 | 63 | def addressToLocation(ea, chunks=None): 64 | """Convert the address `ea` to a `(function, id, offset)`. 65 | 66 | The fields `id` and `offset` represent the chunk index and the 67 | offset into the chunk for the function at `ea`. If the list 68 | `chunks` is specified as a parameter, then use it as a tuple 69 | of ranges in order to calculate the correct address. 70 | """ 71 | F, chunks = func.by(ea), chunks or [ch for ch in func.chunks(ea)] 72 | cid, base = next((i, l) for i, (l, r) in enumerate(chunks) if l <= ea < r) 73 | return func.top(F), cid, ea - base 74 | 75 | class dummy(object): 76 | """ 77 | A dummy object that is guaranteed to return False whenever it is compared 78 | against anything. 79 | """ 80 | def __eq__(self, other): return False 81 | def __cmp__(self, other): return -1 82 | dummy = dummy() 83 | 84 | ### read without using the tag cache 85 | class read(object): 86 | """ 87 | This namespace contains tools that can be used to manually read 88 | tags out of the database without using the cache. 89 | 90 | If `location` is specified as true, then read each contents tag 91 | according to its location rather than an address. This allows one 92 | to perform a translation of the tags in case the function chunks 93 | are at different addresses than when the tags were read. 94 | """ 95 | 96 | def __new__(cls, location=False): 97 | '''Read all of the tags defined within the database.''' 98 | return cls.everything(location=location) 99 | 100 | ## reading the content from a function 101 | @classmethod 102 | def content(cls, ea): 103 | '''Iterate through every tag belonging to the contents of the function at `ea`.''' 104 | F = func.by(ea) 105 | 106 | # iterate through every address in the function 107 | for ea in func.iterate(F): 108 | ui.navigation.set(ea) 109 | 110 | # yield the tags 111 | res = db.tag(ea) 112 | if res: yield ea, res 113 | return 114 | 115 | ## reading the tags from a frame 116 | @classmethod 117 | def frame(cls, ea): 118 | '''Iterate through each field within the frame belonging to the function `ea`.''' 119 | F = func.by(ea) 120 | 121 | # iterate through all of the frame's members 122 | try: 123 | res = func.frame(F) 124 | except internal.exceptions.MissingTypeOrAttribute: 125 | logging.info(u"{:s}.frame({:#x}) : Skipping function at {:#x} due to a missing frame.".format('.'.join([__name__, cls.__name__]), ea, ea)) 126 | return 127 | 128 | for member in res.members: 129 | # if ida has named it and there's no comment, then skip 130 | if lvarNameQ(member.name) and not member.tag(): 131 | continue 132 | 133 | # if it's a structure, then the type is the structure name 134 | if isinstance(member.type, struc.structure_t): 135 | logging.debug(u"{:s}.frame({:#x}) : Storing structure-based type as name for field {:+#x} with tne type {!s}.".format('.'.join([__name__, cls.__name__]), ea, member.offset, internal.utils.string.repr(member.type))) 136 | type = member.type.name 137 | 138 | # otherwise, the type is a tuple that we can serialize 139 | else: 140 | type = member.type 141 | 142 | # otherwise, it's just a regular field. so we can just save what's important. 143 | yield member.offset, (member.name, type, member.tag()) 144 | return 145 | 146 | ## reading everything from the entire database 147 | @classmethod 148 | def everything(cls, location=False): 149 | """Read all of the tags defined within the database. 150 | 151 | Returns a tuple of the format `(Globals, Contents, Frames)`. Each field 152 | is a dictionary keyed by location or offset that retains the tags that 153 | were read. If the boolean `location` was specified then key each 154 | contents tag by location instead of address. 155 | """ 156 | global read 157 | 158 | # read the globals and the contents 159 | six.print_(u'--> Grabbing globals...', file=output) 160 | Globals = { ea : res for ea, res in read.globals() } 161 | 162 | # read all content 163 | six.print_(u'--> Grabbing contents from all functions...', file=output) 164 | Contents = { loc : res for loc, res in read.contents(location=location) } 165 | 166 | # read the frames 167 | six.print_(u'--> Grabbing frames from all functions...', file=output) 168 | Frames = {ea : res for ea, res in read.frames()} 169 | 170 | # return everything back to the user 171 | return Globals, Contents, Frames 172 | 173 | ## reading the globals from the database 174 | @staticmethod 175 | def globals(): 176 | '''Iterate through all of the tags defined globally witin the database.''' 177 | ea, sentinel = db.config.bounds() 178 | 179 | # loop till we hit the end of the database 180 | while ea < sentinel: 181 | ui.navigation.auto(ea) 182 | funcQ = func.within(ea) 183 | 184 | # figure out which tag function to use 185 | f = func.tag if funcQ else db.tag 186 | 187 | # grab the tag and yield it 188 | res = f(ea) 189 | if res: yield ea, res 190 | 191 | # if we're in a function, then seek to the next chunk 192 | if funcQ: 193 | _, ea = func.chunk(ea) 194 | continue 195 | 196 | # otherwise, try the next address till we hit a sentinel value 197 | try: ea = db.a.next(ea) 198 | except internal.exceptions.OutOfBoundsError: ea = sentinel 199 | return 200 | 201 | ## reading the contents from the entire database 202 | @staticmethod 203 | def contents(location=False): 204 | """Iterate through the contents tags for all the functions within the database. 205 | 206 | Each iteration yields a tuple of the format `(location, tags)` where 207 | `location` can be either an address or a chunk identifier and offset 208 | depending on whether `location` was specified as true or not. 209 | """ 210 | global read 211 | 212 | # Iterate through each function in the database 213 | for ea in db.functions(): 214 | 215 | # it's faster to precalculate the chunks here 216 | F, chunks = func.by(ea), [ch for ch in func.chunks(ea)] 217 | 218 | # Iterate through the function's contents yielding each tag 219 | for ea, res in read.content(ea): 220 | loc = addressToLocation(ea, chunks=chunks) if location else ea 221 | yield loc, res 222 | continue 223 | return 224 | 225 | ## reading the frames from the entire database 226 | @staticmethod 227 | def frames(): 228 | '''Iterate through the fields of each frame for all the functions defined within the database.''' 229 | global read 230 | 231 | for ea in db.functions(): 232 | ui.navigation.procedure(ea) 233 | res = dict(read.frame(ea)) 234 | if res: yield ea, res 235 | return 236 | 237 | ### Applying tags to the database 238 | class apply(object): 239 | """ 240 | This namespace contains tools that can be used to apply tags that 241 | have been previously read back into the database. 242 | 243 | Various functions defined within this namespace take a variable number of 244 | keyword arguments which represent a mapping for the tag names. When a 245 | tag name was specified, this mapping will be used to rename the tags 246 | before actually writing them back into the database. 247 | """ 248 | 249 | def __new__(cls, Globals_Contents_Frames, **tagmap): 250 | '''Apply the tags in the argument `(Globals, Contents, Frames)` from the `Globals_Contents_Frames` tuple back into the database.''' 251 | res = Globals, Contents, Frames = Globals_Contents_Frames 252 | return cls.everything(res, **tagmap) 253 | 254 | ## applying the content to a function 255 | @classmethod 256 | def content(cls, Contents, **tagmap): 257 | '''Apply `Contents` back into a function's contents within the database.''' 258 | global apply 259 | return apply.contents(Contents, **tagmap) 260 | 261 | ## applying a frame to a function 262 | @classmethod 263 | def frame(cls, ea, frame, **tagmap): 264 | '''Apply the fields from `frame` back into the function at `ea`.''' 265 | tagmap_output = u", {:s}".format(u', '.join(u"{:s}={:s}".format(internal.utils.string.escape(k), internal.utils.string.escape(v)) for k, v in tagmap.items())) if tagmap else '' 266 | 267 | # nothing to do here, so we gtfo 268 | if not frame: 269 | return 270 | 271 | # grab the function's frame 272 | try: 273 | F = func.frame(ea) 274 | 275 | # if no frame exists for the function, we'll need to create it 276 | except internal.exceptions.MissingTypeOrAttribute: 277 | 278 | # first we figure out the bounds of our members in order to figure out the the lvars and args sizes 279 | framekeys = {item for item in frame.keys()} 280 | minimum, maximum = min(framekeys), max(framekeys) 281 | 282 | # calculate the size of regs by first finding everything that begins at offset 0 283 | res = sorted(offset for offset in framekeys if offset >= 0) 284 | 285 | # now we look for anything near offset 0 that begins with a space (which should be a register) 286 | regs = 0 287 | for offset in res: 288 | name, type, _ = frame[offset] 289 | if not name.startswith(' '): break 290 | 291 | # if type is a string, then treat it as a structure so we can calculate a size 292 | if isinstance(type, six.string_types): 293 | try: 294 | st = struc.by(type) 295 | except internal.exceptions.StructureNotFoundError: 296 | logging.fatal(u"{:s}.frame({:#x}, ...{:s}): Unable to find structure \"{:s}\" for member {:+#x} in order to calculate register size for function at {:+#x}. Using register size of {:+#x}.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, internal.utils.string.escape(type, '"'), offset, ea, regs)) 297 | break 298 | type = [(int, 1), st.size] 299 | 300 | # extract the size components and calculate the total number of bytes 301 | realtype, reallength = type if isinstance(type, builtins.list) else [type, 1] 302 | _, realsize = realtype 303 | cb = realsize * reallength 304 | 305 | # add it to the current aggregate of the register size 306 | regs += cb 307 | 308 | # finally we can create the frame 309 | logging.warning(u"{:s}.frame({:#x}, ...{:s}) : Creating a new frame for function {:#x} with the parameters lvars={:+#x} regs={:+#x} args={:+#x}.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, ea, abs(minimum), regs, abs(maximum))) 310 | F = func.frame.new(ea, abs(minimum), regs, abs(maximum) - regs) 311 | 312 | # iterate through our dictionary of members 313 | for offset, (name, type, saved) in frame.items(): 314 | 315 | # first try and locate the member 316 | try: 317 | member = F.members.by_offset(offset) 318 | except: 319 | member = None 320 | 321 | # if we didn't find a member, then try and add it with what we currently know 322 | if member is None: 323 | logging.warning(u"{:s}.frame({:#x}, ...{:s}) : Unable to find frame member at {:+#x}. Attempting to create the member with the name (\"{:s}\"), type ({!s}), and tags (\"{:s}\").".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, offset, internal.utils.string.escape(name, '"'), internal.utils.string.repr(type), internal.utils.string.repr(saved))) 324 | try: 325 | member = F.members.add(name, type, offset) 326 | except: 327 | logging.fatal(u"{:s}.frame({:#x}, ...{:s}) : Unable to add frame member at {:+#x}. Skipping application of the name (\"{:s}\"), type ({!s}), and tags (\"{:s}\") to it.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, offset, internal.utils.string.escape(name, '"'), internal.utils.string.repr(type), internal.utils.string.repr(saved))) 328 | continue 329 | 330 | # check if the name has changed or is different in some way 331 | if member.name != name: 332 | log = logging.info if lvarNameQ(member.name) else logging.warning 333 | log(u"{:s}.frame({:#x}, ...{:s}) : Renaming frame member {:+#x} from the name \"{:s}\" to \"{:s}\".".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, offset, internal.utils.string.escape(member.name, '"'), internal.utils.string.escape(name, '"'))) 334 | member.name = name 335 | 336 | # check what's going to be overwritten with different values prior to doing it 337 | state, res = member.tag(), saved 338 | 339 | # transform the new tag state using the tagmap 340 | new = { tagmap.get(name, name) : value for name, value in res.items() } 341 | 342 | # check if the tag mapping resulted in the deletion of a tag 343 | if len(new) != len(res): 344 | reskeys, newkeys = ({item for item in items.keys()} for items in [res, new]) 345 | for name in reskeys - newkeys: 346 | logging.warning(u"{:s}.frame({:#x}, ...{:s}) : Refusing requested tag mapping as it results in the tag \"{:s}\" overwriting tag \"{:s}\" for the frame member {:+#x}. The value {!s} would be overwritten by {!s}.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, internal.utils.string.escape(name, '"'), internal.utils.string.escape(tagmap[name], '"'), offset, internal.utils.string.repr(res[name]), internal.utils.string.repr(res[tagmap[name]]))) 347 | pass 348 | 349 | # warn the user about what's going to be overwritten prior to doing it 350 | statekeys, newkeys = ({item for item in items.keys()} for items in [state, new]) 351 | for name in statekeys & newkeys: 352 | if state[name] == new[name]: continue 353 | logging.warning(u"{:s}.frame({:#x}, ...{:s}) : Overwriting tag \"{:s}\" for frame member {:+#x} with new value {!s}. The old value was {!s}.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, internal.utils.string.escape(name, '"'), offset, internal.utils.string.repr(new[name]), internal.utils.string.repr(state[name]))) 354 | 355 | # now we can update the current dictionary 356 | mapstate = { name : value for name, value in new.items() if state.get(name, dummy) != value } 357 | state.update(mapstate) 358 | 359 | # apply each tag directly to the field that it belongs to. 360 | [ member.tag(name, value) for name, value in state.items() ] 361 | 362 | # if the type is a string, then figure out which structure to use 363 | if isinstance(type, six.string_types): 364 | try: 365 | member.type = struc.by(type) 366 | except internal.exceptions.StructureNotFoundError: 367 | logging.warning(u"{:s}.frame({:#x}, ...{:s}): Unable to find structure \"{:s}\" for member at {:+#x}. Skipping it.".format('.'.join([__name__, cls.__name__]), ea, tagmap_output, internal.utils.string.escape(type, '"'), offset)) 368 | 369 | # otherwise, it's a pythonic tuple that we can just assign 370 | else: 371 | member.type = type 372 | continue 373 | return 374 | 375 | ## apply everything to the entire database 376 | @classmethod 377 | def everything(cls, Globals_Contents_Frames, **tagmap): 378 | '''Apply the tags in the argument `(Globals, Contents, Frames)` from the `Globals_Contents_Frames` tuple back into the database.''' 379 | global apply 380 | Globals, Contents, Frames = Globals_Contents_Frames 381 | 382 | ## convert a sorted list keyed by an address into something that updates ida's navigation pointer 383 | def update_navigation(xs, setter): 384 | '''Call `setter` on ea for each iteration of list `xs`.''' 385 | for x in xs: 386 | ea, _ = x 387 | setter(ea) 388 | yield x 389 | return 390 | 391 | ## convert a sorted list keyed by a location into something that updates ida's navigation pointer 392 | def update_navigation_contents(xs, setter): 393 | '''Call `setter` on location for each iteration of list `xs`.''' 394 | for x in xs: 395 | loc, _ = x 396 | ea = locationToAddress(loc) 397 | setter(ea) 398 | yield x 399 | return 400 | 401 | ## handle globals 402 | six.print_(u"--> Writing globals... ({:d} entr{:s})".format(len(Globals), 'y' if len(Globals) == 1 else 'ies'), file=output) 403 | iterable = sorted(Globals.items(), key=operator.itemgetter(0)) 404 | res = apply.globals(update_navigation(iterable, ui.navigation.auto), **tagmap) 405 | # FIXME: verify that res matches number of Globals 406 | 407 | ## handle contents 408 | six.print_(u"--> Writing function contents... ({:d} entr{:s})".format(len(Contents), 'y' if len(Contents) == 1 else 'ies'), file=output) 409 | iterable = sorted(Contents.items(), key=operator.itemgetter(0)) 410 | res = apply.contents(update_navigation_contents(iterable, ui.navigation.set), **tagmap) 411 | # FIXME: verify that res matches number of Contents 412 | 413 | ## update any frames 414 | six.print_(u"--> Applying frames to each function... ({:d} entr{:s})".format(len(Frames), 'y' if len(Frames) == 1 else 'ies'), file=output) 415 | iterable = sorted(Frames.items(), key=operator.itemgetter(0)) 416 | res = apply.frames(update_navigation(iterable, ui.navigation.procedure), **tagmap) 417 | # FIXME: verify that res matches number of Frames 418 | 419 | return 420 | 421 | ## applying tags to the globals 422 | @staticmethod 423 | def globals(Globals, **tagmap): 424 | '''Apply the tags in `Globals` back into the database.''' 425 | global apply 426 | cls, tagmap_output = apply.__class__, u", {:s}".format(u', '.join(u"{:s}={:s}".format(internal.utils.string.escape(oldtag), internal.utils.string.escape(newtag)) for oldtag, newtag in tagmap.items())) if tagmap else '' 427 | 428 | count = 0 429 | for ea, res in Globals: 430 | ns = func if func.within(ea) else db 431 | 432 | # grab the current (old) tag state 433 | state = ns.tag(ea) 434 | 435 | # transform the new tag state using the tagmap 436 | new = { tagmap.get(name, name) : value for name, value in res.items() } 437 | 438 | # check if the tag mapping resulted in the deletion of a tag 439 | if len(new) != len(res): 440 | reskeys, newkeys = ({item for item in items.keys()} for items in [res, new]) 441 | for name in reskeys - newkeys: 442 | logging.warning(u"{:s}.globals(...{:s}) : Refusing requested tag mapping as it results in the tag \"{:s}\" overwriting the tag \"{:s}\" in the global {:#x}. The value {!s} would be replaced with {!s}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.escape(name, '"'), internal.utils.string.escape(tagmap[name], '"'), ea, internal.utils.string.repr(res[name]), internal.utils.string.repr(res[tagmap[name]]))) 443 | pass 444 | 445 | # check what's going to be overwritten with different values prior to doing it 446 | statekeys, newkeys = ({item for item in items.keys()} for items in [state, new]) 447 | for name in statekeys & newkeys: 448 | if state[name] == new[name]: continue 449 | logging.warning(u"{:s}.globals(...{:s}) : Overwriting tag \"{:s}\" for global at {:#x} with new value {!s}. Old value was {!s}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.escape(name, '"'), ea, internal.utils.string.repr(new[name]), internal.utils.string.repr(state[name]))) 450 | 451 | # now we can apply the tags to the global address 452 | try: 453 | [ ns.tag(ea, name, value) for name, value in new.items() if state.get(name, dummy) != value ] 454 | except: 455 | logging.warning(u"{:s}.globals(...{:s}) : Unable to apply tags ({!s}) to global {:#x}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.repr(new), ea), exc_info=True) 456 | 457 | # increase our counter 458 | count += 1 459 | return count 460 | 461 | ## applying contents tags to all the functions 462 | @staticmethod 463 | def contents(Contents, **tagmap): 464 | '''Apply the tags in `Contents` back into each function within the database.''' 465 | global apply 466 | cls, tagmap_output = apply.__class__, u", {:s}".format(u', '.join(u"{:s}={:s}".format(internal.utils.string.escape(oldtag), internal.utils.string.escape(newtag)) for oldtag, newtag in tagmap.items())) if tagmap else '' 467 | 468 | count = 0 469 | for loc, res in Contents: 470 | ea = locationToAddress(loc) 471 | 472 | # warn the user if this address is not within a function 473 | if not func.within(ea): 474 | logging.warning(u"{:s}.contents(...{:s}) : Address {:#x} is not within a function. Using a global tag.".format('.'.join([__name__, cls.__name__]), tagmap_output, ea)) 475 | 476 | # grab the current (old) tag state 477 | state = db.tag(ea) 478 | 479 | # transform the new tag state using the tagmap 480 | new = { tagmap.get(name, name) : value for name, value in res.items() } 481 | 482 | # check if the tag mapping resulted in the deletion of a tag 483 | if len(new) != len(res): 484 | reskeys, newkeys = ({item for item in items.keys()} for items in [res, new]) 485 | for name in reskeys - newkeys: 486 | logging.warning(u"{:s}.contents(...{:s}) : Refusing requested tag mapping as it results in the tag \"{:s}\" overwriting tag \"{:s}\" for the contents at {:#x}. The value {!s} would be overwritten by {!s}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.escape(name, '"'), internal.utils.string.escape(tagmap[name], '"'), ea, internal.utils.string.repr(res[name]), internal.utils.string.repr(res[tagmap[name]]))) 487 | pass 488 | 489 | # inform the user if any tags are being overwritten with different values 490 | statekeys, newkeys = ({item for item in items.keys()} for items in [state, new]) 491 | for name in statekeys & newkeys: 492 | if state[name] == new[name]: continue 493 | logging.warning(u"{:s}.contents(...{:s}) : Overwriting contents tag \"{:s}\" for address {:#x} with new value {!s}. Old value was {!s}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.escape(name, '"'), ea, internal.utils.string.repr(new[name]), internal.utils.string.repr(state[name]))) 494 | 495 | # write the tags to the contents address 496 | try: 497 | [ db.tag(ea, name, value) for name, value in new.items() if state.get(name, dummy) != value ] 498 | except: 499 | logging.warning(u"{:s}.contents(...{:s}) : Unable to apply tags {!s} to location {:#x}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.repr(new), ea), exc_info=True) 500 | 501 | # increase our counter 502 | count += 1 503 | return count 504 | 505 | ## applying frames to all the functions 506 | @staticmethod 507 | def frames(Frames, **tagmap): 508 | '''Apply the fields from `Frames` back into each function's frame.''' 509 | global apply 510 | cls, tagmap_output = apply.__class__, u", {:s}".format(u', '.join(u"{:s}={:s}".format(internal.utils.string.escape(oldtag), internal.utils.string.escape(newtag)) for oldtag, newtag in tagmap.items())) if tagmap else '' 511 | 512 | count = 0 513 | for ea, res in Frames: 514 | try: 515 | apply.frame(ea, res, **tagmap) 516 | except: 517 | logging.warning(u"{:s}.frames(...{:s}) : Unable to apply tags ({!s}) to frame at {:#x}.".format('.'.join([__name__, cls.__name__]), tagmap_output, internal.utils.string.repr(res), ea), exc_info=True) 518 | 519 | # increase our counter 520 | count += 1 521 | return count 522 | 523 | ### Exporting tags from the database using the tag cache 524 | class export(object): 525 | """ 526 | This namespace contains tools that can be used to quickly 527 | export specific tags out of the database using the cache. 528 | 529 | If `location` is specified as true, then read each contents tag 530 | according to its location rather than an address. This allows one 531 | to perform a translation of the tags in case the function chunks 532 | are at different addresses than when the tags were read. 533 | """ 534 | 535 | def __new__(cls, *tags, **location): 536 | '''Read the specified tags within the database using the cache.''' 537 | return cls.everything(*tags, **location) 538 | 539 | ## query the content from a function 540 | @classmethod 541 | def content(cls, F, *tags, **location): 542 | '''Iterate through the specified `tags` belonging to the contents of the function at `ea` using the cache.''' 543 | identity = lambda res: res 544 | translate = addressToLocation if location.get('location', False) else identity 545 | 546 | iterable = func.select(F, Or=tags) if tags else func.select(F) 547 | for ea, res in iterable: 548 | ui.navigation.set(ea) 549 | if res: yield translate(ea), res 550 | return 551 | 552 | ## query the frame from a function 553 | @classmethod 554 | def frame(cls, F, *tags): 555 | '''Iterate through each field containing the specified `tags` within the frame belonging to the function `ea`.''' 556 | global read, internal 557 | tags_ = { tag for tag in tags } 558 | 559 | for ofs, item in read.frame(F): 560 | field, type, state = item 561 | 562 | # if no tags were specified or the entire state is in the requested tags, then save the entire member 563 | if not tags_ or all(name in tags_ for name in state): 564 | yield ofs, (field, type, state) 565 | continue 566 | 567 | # otherwise, store the state in a dictionary using only the tags the user asked for. 568 | state_keys = {item for item in state} 569 | save = { name : state[name] for name in state_keys & tags_ } 570 | 571 | # if anything was found, then re-encode it and yield to the user 572 | if res: yield ofs, (field, type, save) 573 | return 574 | 575 | ## query the entire database for the specified tags 576 | @classmethod 577 | def everything(cls, *tags, **location): 578 | """Read all of the specified `tags` within the database using the cache. 579 | 580 | Returns a tuple of the format `(Globals, Contents, Frames)`. Each field 581 | is a dictionary keyed by location or offset that retains the tags that 582 | were read. If the boolean `location` was specified then key each 583 | contents tag by location instead of address. 584 | """ 585 | global export 586 | 587 | # collect all the globals into a dictionary 588 | six.print_(u'--> Grabbing globals (cached)...', file=output) 589 | iterable = export.globals(*tags) 590 | Globals = {ea : res for ea, res in filter(None, iterable)} 591 | 592 | # grab all the contents into a dictionary 593 | six.print_(u'--> Grabbing contents from functions (cached)...', file=output) 594 | location = location.get('location', False) 595 | iterable = export.contents(*tags, location=location) 596 | Contents = {loc : res for loc, res in filter(None, iterable)} 597 | 598 | # grab any frames into a dictionary 599 | six.print_(u'--> Grabbing frames from functions (cached)...', file=output) 600 | iterable = export.frames(*tags) 601 | Frames = {ea : res for ea, res in filter(None, iterable)} 602 | 603 | # return it back to the user 604 | return Globals, Contents, Frames 605 | 606 | ## query all the globals matching the specified tags 607 | @staticmethod 608 | def globals(*tags): 609 | '''Iterate through all of the specified global `tags` within the database using the cache.''' 610 | iterable = db.select(Or=tags) if tags else db.select() 611 | for ea, res in iterable: 612 | ui.navigation.auto(ea) 613 | if res: yield ea, res 614 | return 615 | 616 | ## query all the contents in each function that match the specified tags 617 | @staticmethod 618 | def contents(*tags, **location): 619 | """Iterate through the specified contents `tags` within the database using the cache. 620 | 621 | Each iteration yields a tuple of the format `(location, tags)` where 622 | `location` can be either an address or a chunk identifier and offset 623 | depending on whether `location` was specified as true or not. 624 | """ 625 | global export 626 | location = location.get('location', False) 627 | 628 | iterable = db.selectcontents(Or=tags) if tags else db.selectcontents() 629 | for F, res in iterable: 630 | for loc, res in export.content(F, *res, location=location): 631 | if res: yield loc, res 632 | continue 633 | return 634 | 635 | ## query all the frames that match the specified tags 636 | @staticmethod 637 | def frames(*tags): 638 | '''Iterate through the fields in each function's frame containing the specified `tags`.''' 639 | global export 640 | tags_ = {x for x in tags} 641 | 642 | for ea in db.functions(): 643 | ui.navigation.procedure(ea) 644 | res = dict(export.frame(ea, *tags)) 645 | if res: yield ea, res 646 | return 647 | 648 | __all__ = ['list', 'read', 'export', 'apply'] 649 | --------------------------------------------------------------------------------