├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── __ida_setup__.py ├── idarop ├── __init__.py ├── engine.py └── ui.py ├── netnode ├── __init__.py ├── netnode.py └── test_netnode.py ├── plugins └── idarop_plugin_t.py ├── screenshots ├── FilteringGadgets.PNG ├── ListingGadgets.PNG ├── SearchForGadgets.PNG └── SearchingAndListingGadgets.PNG ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lucasg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Idarop : a ROP database plugin for IDA 2 | ========= 3 | 4 | `Idarop` is an IDA plugin which list and store all the ROP gadgets presents within the opened binary. The codebase is vastly copied from the unmaintained IDA plugin [`idaploiter`](https://github.com/iphelix/ida-sploiter). However `idasploiter` is built to work at runtime (lifting IDA debugger API), whereas `idarop` is aimed for a more static approach. 5 | 6 | While there is an incredible variety of ROP assisting tools ([just grep ROP in that list](http://www.capstone-engine.org/showcase.html)), most of them just output the found gadgets in the console which makes storing and searching through them a hassle. `idarop` aims to capitalize on the `idb` file format (and IDA) to store ROP gagdets along RE informations (assembly annotations, type infos, etc.) : 7 | 8 |

9 | Listing ROP Gadgets in a specific list view in IDA 10 |

11 | 12 | Using IDA to view gadgets allows the user to take advantage of the "advanced" list filtering IDA provides : in the following picture, only gadgets having a `0xff` opcode and less than 4 bytes are shown (and the ones touching `esp` are highlighted). 13 | 14 |

15 | Filtering ROP Gadgets using IDA Filters Tool 16 |

17 | 18 | NB : This plugin only works on `x86` binaries. 19 | 20 | ## Dependencies 21 | 22 | `idarop` rely on [`ida-netnode`](https://github.com/williballenthin/ida-netnode) to store found gadgets address in the `.idb` database. If `netnode` not installed, the ROP search results will just be discarded at IDA's exit. 23 | 24 | ## Usage 25 | 26 | `idarop` provides two shortucts : 27 | 28 | * `Maj+R` to list found ROP gadgets 29 | * `Ctrl+Maj+R` to do a new gadgets search (wipes previous results) 30 | 31 |

32 | Searching ROP gadgets within IDA 33 |

34 | 35 | ( The search configuration and UI is copied and adapted from `idasploiter`) 36 | 37 | ## Install 38 | 39 | `idarop` is on Pypi, so you can pip from it. 40 | 41 | On Windows: 42 | 43 | * `C:\Python27\Scripts\pip2.7.exe install idarop --install-option="--ida"` 44 | * `C:\Python27\Scripts\pip2.7.exe install idarop --install-option="--ida="6.9""` 45 | 46 | Ida is installed in the Program Files folder, so you need to run this command with Administrator rights. 47 | 48 | 49 | Alternatively, you can clone the repo and type "`C:\Python27\python.exe setup.py install --ida`". `idarop` use a "clever" [`__ida_setup__.py`](https://github.com/lucasg/idasetup) script to override the traditionnal `distools` `install` command to install the plugin in the correct IDA plugins directory. 50 | 51 | ## Credits 52 | 53 | Since this project is largely a ersatz of it, it would be criminal of me not to thanks [Peter Kacherginsky](https://thesprawl.org/) for its work on `idasploiter`. -------------------------------------------------------------------------------- /__ida_setup__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import getpass 4 | import distutils 5 | from distutils.core import setup 6 | from setuptools.command.install import install 7 | 8 | 9 | if (sys.version_info > (3, 0)): 10 | raise ImportError("Idapython runs in a Python 2.7 interpreter, please execute this install setup with it.") 11 | 12 | def ida_install_dir_windows(version, *args): 13 | IDA_INSTALL_DIR_WINDOWS = { 14 | '6.8' : os.path.join(os.environ.get("ProgramFiles(x86)", "KeyError"), "IDA 6.8", "plugins"), 15 | '6.9' : os.path.join(os.environ.get("ProgramFiles(x86)", "KeyError"), "IDA 6.9", "plugins"), 16 | '7.0' : os.path.join(os.environ.get("ProgramW6432", "KeyError"), "IDA 7.0", "plugins"), 17 | '7.1' : os.path.join(os.environ.get("ProgramW6432", "KeyError"), "IDA 7.1", "plugins"), 18 | } 19 | 20 | return IDA_INSTALL_DIR_WINDOWS[version] 21 | 22 | def ida_install_dir_macos(version, *args): 23 | IDA_INSTALL_DIR_MACOS = { 24 | '6.8' : os.path.join("/Applications", "IDA Pro 6.8", "idaq.app/Contents/MacOS/plugins"), 25 | '6.9' : os.path.join("/Applications", "IDA Pro 6.9", "idaq.app/Contents/MacOS/plugins"), 26 | '7.0' : os.path.join("/Applications", "IDA Pro 7.0", "idaq.app/Contents/MacOS/plugins"), 27 | '7.1' : os.path.join("/Applications", "IDA Pro 7.1", "idaq.app/Contents/MacOS/plugins"), 28 | } 29 | 30 | return IDA_INSTALL_DIR_MACOS[version] 31 | 32 | def ida_install_dir_linux(version, is_user, *args): 33 | IDA_INSTALL_DIR_LINUX_USER = { 34 | '6.8' : os.path.join("/home", getpass.getuser() ,"IDA 6.8", "plugins"), 35 | '6.9' : os.path.join("/home", getpass.getuser() ,"IDA 6.9", "plugins"), 36 | '7.0' : os.path.join("/home", getpass.getuser() ,"IDA 7.0", "plugins"), 37 | '7.1' : os.path.join("/home", getpass.getuser() ,"IDA 7.1", "plugins"), 38 | } 39 | 40 | IDA_INSTALL_DIR_LINUX_SYSTEM = { 41 | '6.8' : os.path.join("/opt", "IDA 6.8", "plugins"), 42 | '6.9' : os.path.join("/opt", "IDA 6.9", "plugins"), 43 | '7.0' : os.path.join("/opt", "IDA 7.0", "plugins"), 44 | '7.1' : os.path.join("/opt", "IDA 7.1", "plugins"), 45 | } 46 | 47 | if is_user: 48 | return IDA_INSTALL_DIR_LINUX_USER[version] 49 | else: 50 | return IDA_INSTALL_DIR_LINUX_SYSTEM[version] 51 | 52 | IDA_SUPPORTED_VERSIONS = ('6.8','6.9','7.0','7.1') 53 | 54 | IDA_INSTALL_DIRS = { 55 | 56 | # On Windows, the folder is at C:\Program Files (x86)\IDA %d\plugins 57 | 'win32' : ida_install_dir_windows, 58 | 59 | 'cygwin': ida_install_dir_windows, 60 | 61 | # On MacOS, the folder is at /Applications/IDA\ Pro\ %d/idaq.app/Contents/MacOS/plugins 62 | 'darwin' : ida_install_dir_macos, 63 | 64 | # On Linux, the folder may be at /opt/IDA/plugins/ 65 | 'linux2' : ida_install_dir_linux, 66 | 67 | # Python 3 68 | 'linux' : ida_install_dir_linux, 69 | } 70 | 71 | class IdaPluginInstallCommand(install): 72 | description = "install the current plugin in IDA plugin folder." 73 | user_options = install.user_options + [ 74 | ('ida', None, 'force custom ida install script.'), 75 | ('ida-version=', None, 'specify ida version.'), 76 | ('ida-install-deps', None, 'install ida plugin dependencies.'), 77 | ] 78 | 79 | def initialize_options(self): 80 | install.initialize_options(self) 81 | self.ida = False # explicitely tell setuptools to use the ida setup script 82 | self.ida_version = None # locate default ida version 83 | self.ida_install_deps = False # Install plugin deps 84 | 85 | def finalize_options(self): 86 | 87 | # Search for a supported version installed 88 | if self.ida_version == None: 89 | 90 | for ida_version in IDA_SUPPORTED_VERSIONS: 91 | ida_install_dir = IDA_INSTALL_DIRS[sys.platform](ida_version, self.user) 92 | 93 | if os.path.exists(ida_install_dir): 94 | self.ida_version = ida_version 95 | self.announce("[IDA PLUGIN INSTALL] No ida version provided, using default version : %s" % self.ida_version, level=distutils.log.ERROR) 96 | break 97 | 98 | 99 | assert self.ida_version in IDA_SUPPORTED_VERSIONS, 'Supported IDA on this platform : %s' % IDA_SUPPORTED_VERSIONS 100 | install.finalize_options(self) 101 | 102 | def install_dependencies(self, dist, install_dir): 103 | # type: (distutils.core.install, setuptools.dist.Distribution, str) -> void 104 | """ Recursively install dependency using pip (for those on pipy) """ 105 | 106 | if not len(dist.install_requires): 107 | return 108 | 109 | 110 | # inner import in order to prevent build breakage 111 | # on old Python2 installs with no pip package unless 112 | # there is actually a need for it. 113 | import pip 114 | 115 | for dependency in dist.install_requires: 116 | self.announce("[IDA PLUGIN INSTALL] installing dependency %s -> %s" % (dependency, install_dir), level=distutils.log.INFO) 117 | 118 | if not self.dry_run: 119 | pip.main(['install', '-t', install_dir, "--ignore-installed" , dependency]) 120 | 121 | def install_packages(self, dist, install_dir): 122 | # type: (distutils.core.install, setuptools.dist.Distribution, str) -> void 123 | """ Install python packages """ 124 | 125 | for package in dist.packages: 126 | self.announce("[IDA PLUGIN INSTALL] copy package %s -> %s" % (package, install_dir), level=distutils.log.INFO) 127 | 128 | if not self.dry_run: 129 | self.copy_tree(package, os.path.join(install_dir, package)) 130 | 131 | def install_plugins(self, dist, install_dir): 132 | # type: (distutils.core.install, setuptools.dist.Distribution, str) -> void 133 | """ Install ida plugins entry points """ 134 | 135 | ida_plugins = dist.package_data.get('ida_plugins', []) 136 | for plugin in ida_plugins: 137 | self.announce("[IDA PLUGIN INSTALL] copy plugin %s -> %s" % (plugin, install_dir), level=distutils.log.INFO) 138 | 139 | if not self.dry_run: 140 | self.copy_file(plugin,install_dir) 141 | 142 | def run(self, *args, **kwargs): 143 | """ Install ida plugins routine """ 144 | 145 | dist = self.distribution # type: setuptools.dist.Distribution 146 | 147 | # Custom install script 148 | if self.ida: 149 | install_dir = self.root # respect user-override install dir 150 | if not install_dir: # otherwise return the ida install dir 151 | install_dir = IDA_INSTALL_DIRS[sys.platform](self.ida_version) 152 | 153 | if self.ida_install_deps: 154 | self.install_dependencies(dist, install_dir) 155 | 156 | self.install_packages(dist, install_dir) 157 | self.install_plugins(dist, install_dir) 158 | 159 | install.run(self) -------------------------------------------------------------------------------- /idarop/__init__.py: -------------------------------------------------------------------------------- 1 | IDAROP_VERSION = "0.4.2" 2 | IDAROP_DESCRIPTION = "ROP search and visualization plugin for IDA" -------------------------------------------------------------------------------- /idarop/engine.py: -------------------------------------------------------------------------------- 1 | """ IDA ROP view plugin rop search engine and processing """ 2 | 3 | # Python libraries 4 | import binascii 5 | import sys 6 | import logging 7 | from struct import pack, unpack 8 | from collections import namedtuple 9 | 10 | # IDA libraries 11 | import idaapi 12 | import idc 13 | 14 | if idaapi.IDA_SDK_VERSION <= 695: 15 | from idaapi import get_segm_qty, getnseg 16 | if idaapi.IDA_SDK_VERSION >= 700: 17 | from ida_segment import get_segm_qty, getnseg 18 | else: 19 | pass 20 | 21 | ############################################################################### 22 | # Data Structure class 23 | 24 | class SegmentEntry(namedtuple('Segment','name start end size r w x segclass')): 25 | """ Segment entry container for listing segments and characteristics """ 26 | 27 | __slots__ = () 28 | 29 | def get_display_list(self): 30 | """ Return the display format list for the segment listing """ 31 | return [ self.name , 32 | "%08X" % self.start, 33 | "%08X" % self.end, 34 | "%08X" % self.size, 35 | (".", "R")[self.r], 36 | (".", "W")[self.w], 37 | (".", "X")[self.x], 38 | self.segclass] 39 | 40 | class Gadget(namedtuple('Gadget', 'address ret_address instructions opcodes size')): 41 | """ Gadget element container for rop listing and export to csv """ 42 | 43 | __slots__ = () 44 | 45 | def get_display_list(self, address_format): 46 | """ Return the display format list for the rop gadget listing """ 47 | txt_instructions = " ; ".join(self.instructions) 48 | txt_opcodes = " ".join("%02x" % ord(op) for op in self.opcodes) 49 | return [ idc.SegName(self.address), 50 | address_format % self.address, 51 | address_format % self.ret_address, 52 | txt_instructions, 53 | txt_opcodes, 54 | "%d" % len(self.opcodes), 55 | ("N", "Y")["sp" in txt_instructions] 56 | ] 57 | 58 | 59 | 60 | # ROP Search Engine 61 | class IdaRopSearch(): 62 | 63 | def __init__(self, sploiter): 64 | 65 | self.maxRopOffset = 40 # Maximum offset from the return instruction to look for gadgets. default: 40 66 | self.maxRopSize = 6 # Maximum number of instructions to look for gadgets. default: 6 67 | self.maxRetnImm = 64 # Maximum imm16 value in retn. default: 64 68 | self.maxJopImm = 255 # Maximum jop [reg + IMM] value. default: 64 69 | self.maxRops = 0 # Maximum number of ROP chains to find. default: 0 (unlimited) 70 | 71 | self.debug = False 72 | 73 | self.regnames = idaapi.ph_get_regnames() 74 | 75 | self.sploiter = sploiter 76 | self.retns = list() 77 | self.gadgets = list() 78 | 79 | # Decoded instruction cache 80 | self.insn_cache = dict() 81 | 82 | # Extra bytes to read to ensure correct decoding of 83 | # RETN, RETN imm16, CALL /2, and JMP /4 instructions. 84 | self.dbg_read_extra = 6 # FF + ModR/M + SIB + disp32 85 | 86 | self.insn_arithmetic_ops = ["inc","dec","neg", "add","sub","mul","imul","div","idiv","adc","sbb","lea"] 87 | self.insn_bit_ops = ["not","and","or","xor","shr","shl","sar","sal","shld","shrd","ror","rcr","rcl"] 88 | 89 | def get_o_reg_name(self, insn, n): 90 | 91 | reg_num = insn.Operands[n].reg 92 | reg_name = self.regnames[reg_num] 93 | 94 | # NOTE: IDA's x86/x86-64 regname array contains only register root names 95 | # (e.g ax,cx,dx,etc.). However we can still figure out exact register 96 | # size by looking at the operand 'dtyp' property. 97 | if reg_num < 8: 98 | 99 | # 32-bit register 100 | if insn.Operands[n].dtyp == idaapi.dt_dword: 101 | reg_name = 'e'+reg_name 102 | 103 | # 64-bit register 104 | elif insn.Operands[n].dtyp == idaapi.dt_qword: 105 | reg_name = 'r'+reg_name 106 | 107 | # 16-bit register otherwise 108 | 109 | return reg_name 110 | 111 | def search_retns(self): 112 | 113 | self.retns = list() 114 | 115 | # Iterate over segments in the module 116 | # BUG: Iterating over all loaded segments is more stable than looking up by address 117 | for n in self.segments: 118 | segment = getnseg(n) 119 | 120 | # Locate executable segments in a selected modules 121 | # NOTE: Each module may have multiple executable segments 122 | if segment and segment.perm & idaapi.SEGPERM_EXEC: 123 | 124 | ####################################################### 125 | # Search for ROP gadgets 126 | self.search_rop_gadgets(segment, ret_preamble = 0xc3 ) # RETN 127 | self.search_rop_gadgets(segment, ret_preamble = 0xcb ) # RETF 128 | self.search_rop_gadgets(segment, ret_preamble = 0xc2 ) # RETN imm16 129 | self.search_rop_gadgets(segment, ret_preamble = 0xca ) # RETN imm16 130 | self.search_rop_gadgets(segment, ret_preamble = 0xf2c3 ) # MPX RETN 131 | self.search_rop_gadgets(segment, ret_preamble = 0xf2c2 ) # MPX RETN imm16 132 | 133 | ####################################################### 134 | # Search for JOP gadgets 135 | self.search_job_gadgets(segment, jump_preamble = 0xff ) 136 | self.search_job_gadgets(segment, jump_preamble = 0xf2ff ) # MPX 137 | 138 | ####################################################### 139 | # Search for SYS gadgets 140 | self.search_sys_gadgets(segment) 141 | 142 | print("[IdaRopSearch] Found %d returns" % len(self.retns)) 143 | 144 | 145 | def is_job_gadget(self, jop): 146 | """ jump oriented gadget predicate """ 147 | 148 | ################################################### 149 | # JMP/CALL reg 150 | if jop[0] in ["\xe0","\xe1","\xe2","\xe3","\xe4","\xe5","\xe6","\xe7", 151 | "\xd0","\xd1","\xd2","\xd3","\xd4","\xd5","\xd6","\xd7"]: 152 | return True 153 | 154 | ################################################### 155 | # JMP/CALL [reg] no SIB 156 | # NOTE: Do not include pure [disp] instruction. 157 | 158 | # JMP/CALL [reg] no *SP,*BP 159 | elif jop[0] in ["\x20","\x21","\x22","\x23","\x26","\x27", 160 | "\x10","\x11","\x12","\x13","\x16","\x17"]: 161 | return True 162 | 163 | # JMP/CALL [reg + imm8] no *SP 164 | elif jop[0] in ["\x60","\x61","\x62","\x63","\x65","\x66","\x67", 165 | "\x50","\x51","\x52","\x53","\x55","\x56","\x57"]: 166 | jop_imm8 = jop[1] 167 | jop_imm8 = unpack("b", jop_imm8)[0] # signed 168 | 169 | if jop_imm8 <= self.maxJopImm: 170 | return True 171 | 172 | 173 | # JMP/CALL [reg + imm32] no *SP 174 | elif jop[0] in ["\xa0","\xa1","\xa2","\xa3","\xa5","\xa6","\xa7", 175 | "\x90","\x91","\x92","\x93","\x95","\x96","\x97"]: 176 | jop_imm32 = jop[1:5] 177 | jop_imm32 = unpack(" seg_end: 329 | dbg_read_extra = 0 330 | 331 | for i in range(self.maxRopOffset): 332 | self.dbg_mem_cache = idc.GetManyBytes(ea_end - self.maxRopOffset + i, self.maxRopOffset - i + self.dbg_read_extra) 333 | if self.dbg_mem_cache != None: 334 | break 335 | 336 | # Error while reading memory (Ida sometimes does not want to read uninit data) 337 | if self.dbg_mem_cache == None: 338 | for backward_size in range(self.maxRopOffset, 0, -1): 339 | self.dbg_mem_cache = idc.GetManyBytes(ea_end - backward_size, backward_size) 340 | if self.dbg_mem_cache != None: 341 | break 342 | 343 | # Big problem ahead 344 | if self.dbg_mem_cache == None: 345 | logging.error("[Ida Search Error] could not read bytes [0x%x, 0x%x]" % (ea_end - self.maxRopOffset + i, ea_end - self.maxRopOffset + i + self.maxRopOffset - i + self.dbg_read_extra)) 346 | 347 | # Search all possible gadgets up to maxoffset bytes back 348 | # NOTE: Try all byte combinations to capture longer/more instructions 349 | # even with bad bytes in the middle. 350 | for i in range(1, len(self.dbg_mem_cache) - self.dbg_read_extra): 351 | 352 | ea = ea_end - i 353 | 354 | # Try to build a gadget at the pointer 355 | gadget = self.build_gadget(ea, ea_end) 356 | 357 | # Successfully built the gadget 358 | if gadget: 359 | 360 | # Filter gadgets with too many instruction 361 | if gadget.size > self.maxRopSize: 362 | break 363 | 364 | # Append newly built gadget 365 | self.gadgets.append(gadget) 366 | self.gadgets_cache[ea] = gadget 367 | 368 | # Exceeded maximum number of gadgets 369 | if self.maxRops and len(self.gadgets) >= self.maxRops: 370 | breakFlag = True 371 | print("[Ida Rop] Maximum number of gadgets exceeded.") 372 | break 373 | else: 374 | self.gadgets_cache[ea] = None 375 | 376 | if breakFlag or idaapi.wasBreak(): 377 | breakFlag = True 378 | break 379 | 380 | 381 | # Canceled 382 | # NOTE: Only works when started from GUI not script. 383 | if breakFlag or idaapi.wasBreak(): 384 | breakFlag = True 385 | print ("[IdaRopSearch] Canceled.") 386 | break 387 | 388 | # Progress report 389 | if not self.debug and count_curr >= count_notify: 390 | 391 | # NOTE: Need to use %%%% to escape both Python and IDA's format strings 392 | percent_progression = count_curr*100/count_total 393 | progression_str = """Searching gadgets: {progression:02d} %""".format(progression = percent_progression) 394 | idaapi.replace_wait_box(progression_str) 395 | 396 | count_notify += 0.10 * count_total 397 | 398 | count_curr += 1 399 | 400 | print ("[IdaRopSearch] Found %d gadgets." % len(self.gadgets)) 401 | except: 402 | logging.error ("[IdaRopSearch] Exception raised while search for gadgets : %s." % sys.exc_info()) 403 | pass 404 | 405 | finally: 406 | 407 | if not self.debug: 408 | idaapi.hide_wait_box() 409 | 410 | 411 | # Attempt to build a gadget at the provided start address 412 | # by verifying it properly terminates at the expected RETN. 413 | def build_gadget(self, ea, ea_end): 414 | 415 | instructions = list() 416 | chg_registers = set() 417 | use_registers = set() 418 | operations = set() 419 | pivot = 0 420 | start_ea = ea 421 | 422 | # Process each instruction in the gadget 423 | while ea <= ea_end: 424 | 425 | ################################################################### 426 | # Gadget Level Cache: 427 | # 428 | # Locate a gadget (failed or built) starting at this address. 429 | # If one is located, then we don't need to process any further 430 | # instructions and just get necessary data from the cached 431 | # gadget to never have to process the same address twice. 432 | if ea in self.gadgets_cache: 433 | 434 | # Check if the gadget was build successfully 435 | gadget_cache = self.gadgets_cache[ea] 436 | 437 | # Build the reset of the gadget from cache 438 | if gadget_cache: 439 | 440 | for insn in gadget_cache.instructions: 441 | instructions.append(insn) 442 | 443 | #pivot += gadget_cache.pivot 444 | opcodes = idc.GetManyBytes(start_ea, ea_end - start_ea + 1) 445 | 446 | gadget = Gadget( 447 | address = start_ea, 448 | ret_address = ea_end, 449 | instructions = instructions, 450 | opcodes = opcodes, 451 | size = len(opcodes), 452 | #pivot = pivot 453 | ) 454 | 455 | return gadget 456 | 457 | # Previous attempt to build gadget at this address failed 458 | else: 459 | return None 460 | 461 | # Process new instruction 462 | else: 463 | 464 | # Instruction length 465 | # NOTE: decode_insn also sets global idaapi.cmd 466 | # which contains insn_t structure 467 | insn_size = idaapi.decode_insn(ea) 468 | 469 | # Check successful decoding of the instruction 470 | if insn_size: 471 | 472 | # Decoded instruction is too big to be a RETN or RETN imm16 473 | if ea + insn_size > ea_end + self.dbg_read_extra: 474 | return None 475 | 476 | ############################################################### 477 | # Instruction Level Cache 478 | # 479 | # Most instructions are repetitive so we can just cache 480 | # unique byte combinations to avoid costly decoding more 481 | # than once 482 | 483 | # Read instruction from memory cache 484 | dbg_mem_offset = ea - (ea_end - (len(self.dbg_mem_cache) - self.dbg_read_extra) ) 485 | dbg_mem = self.dbg_mem_cache[dbg_mem_offset:dbg_mem_offset + insn_size] 486 | 487 | # Create instruction cache if it doesn't already exist 488 | if not dbg_mem in self.insn_cache: 489 | 490 | ########################################################### 491 | # Decode instruction 492 | ########################################################### 493 | 494 | # Get global insn_t structure describing the instruction 495 | # NOTE: copy() is expensive, so we keep this single-threaded 496 | insn = idaapi.cmd 497 | 498 | ####################################################### 499 | # Decode and Cache instruction characteristics 500 | self.insn_cache[dbg_mem] = self.decode_instruction(insn, ea, ea_end) 501 | 502 | ################################################################## 503 | # Retrieve cached instruction and apply it to the gadget 504 | 505 | # Check that cached instruction contains valid data 506 | if self.insn_cache[dbg_mem]: 507 | 508 | # Retrieve basic instruction characteristics 509 | insn_mnem = self.insn_cache[dbg_mem]["insn_mnem"] 510 | insn_disas = self.insn_cache[dbg_mem]["insn_disas"] 511 | instructions.append(insn_disas) 512 | 513 | ####################################################### 514 | # Expected ending instruction of the gadget 515 | if ea == ea_end: 516 | opcodes = idc.GetManyBytes(start_ea, ea_end - start_ea + 1) 517 | 518 | gadget = Gadget( 519 | address = start_ea, 520 | ret_address = ea_end, 521 | instructions = instructions, 522 | opcodes = opcodes, 523 | size = len(opcodes), 524 | #pivot = pivot 525 | ) 526 | return gadget 527 | 528 | ####################################################### 529 | # Filter out of place ROP/JOP/COP terminators 530 | # NOTE: retn/jmp/call are allowed, but only in the last position 531 | 532 | # Unexpected return instruction 533 | elif insn_mnem == "retn": 534 | return None 535 | 536 | # Unexpected call/jmp instruction 537 | elif insn_mnem in ["jmp","call"]: 538 | return None 539 | 540 | ####################################################### 541 | # Add instruction instruction characteristics to the gadget 542 | else: 543 | 544 | for reg in self.insn_cache[dbg_mem]["insn_chg_registers"]: 545 | chg_registers.add(reg) 546 | 547 | for reg in self.insn_cache[dbg_mem]["insn_use_registers"]: 548 | use_registers.add(reg) 549 | 550 | for op in self.insn_cache[dbg_mem]["insn_operations"]: 551 | operations.add(op) 552 | 553 | pivot += self.insn_cache[dbg_mem]["insn_pivot"] 554 | 555 | # Previous attempt to decode the instruction invalidated the gadget 556 | else: 557 | return None 558 | 559 | ############################################################### 560 | # Next instruction 561 | # NOTE: This is outside cache 562 | ea += insn_size 563 | 564 | ################################################################### 565 | # Failed decoding of the instruction 566 | # NOTE: Gadgets may have bad instructions in the middle which 567 | # can be tolerated as long as we can find a useful instruction 568 | # further out. 569 | else: 570 | 571 | # HACK: IDA does not disassemble "\x00\x00" unless you enable 572 | # "Disassemble zero opcode instructions" in Processor Options. 573 | # Since this option is normally disabled, I will attempt 574 | # to get this instruction manually. 575 | 576 | # Read two bytes from memory cache at current instruction candidate 577 | dbg_mem_offset = ea - (ea_end - self.maxRopOffset) 578 | dbg_mem = self.dbg_mem_cache[dbg_mem_offset:dbg_mem_offset + 2] 579 | 580 | # Compare to two zero bytes 581 | if dbg_mem[:2] == "\x00\x00": 582 | 583 | if self.sploiter.addr64: 584 | instructions.append("add [rax],al") 585 | else: 586 | instructions.append("add [eax],al") 587 | 588 | use_registers.add("al") 589 | operations.add("reg-to-mem") 590 | 591 | ea += 2 592 | 593 | # "MOV Sreg, r/m16" instructions will result in illegal instruction exception: c000001d 594 | # or the memory couldn't be read exception: c0000005 which we don't want in our gadgets. 595 | elif len(dbg_mem) and dbg_mem[0] == "\x8E": 596 | return None 597 | 598 | # Record a "bad byte" if allowed 599 | elif dbg_mem and not self.ropNoBadBytes: 600 | byte = dbg_mem[0] 601 | 602 | instructions.append("db %sh" % binascii.hexlify(byte)) 603 | 604 | ea += 1 605 | 606 | # Invalidate the gadget 607 | else: 608 | return None 609 | 610 | # Failed to build a gadget, because RETN instruction was not found 611 | else: 612 | return None 613 | 614 | ############################################################### 615 | # Decode instruction 616 | 617 | def decode_instruction(self, insn, ea, ea_end): 618 | 619 | # Instruction specific characteristics 620 | insn_chg_registers = set() 621 | insn_use_registers = set() 622 | insn_operations = set() 623 | insn_pivot = 0 624 | 625 | # Instruction feature 626 | # 627 | # instruc_t.feature 628 | # 629 | # CF_STOP = 0x00001 # Instruction doesn't pass execution to the next instruction 630 | # CF_CALL = 0x00002 # CALL instruction (should make a procedure here) 631 | # CF_CHG1 = 0x00004 # The instruction modifies the first operand 632 | # CF_CHG2 = 0x00008 # The instruction modifies the second operand 633 | # CF_CHG3 = 0x00010 # The instruction modifies the third operand 634 | # CF_CHG4 = 0x00020 # The instruction modifies 4 operand 635 | # CF_CHG5 = 0x00040 # The instruction modifies 5 operand 636 | # CF_CHG6 = 0x00080 # The instruction modifies 6 operand 637 | # CF_USE1 = 0x00100 # The instruction uses value of the first operand 638 | # CF_USE2 = 0x00200 # The instruction uses value of the second operand 639 | # CF_USE3 = 0x00400 # The instruction uses value of the third operand 640 | # CF_USE4 = 0x00800 # The instruction uses value of the 4 operand 641 | # CF_USE5 = 0x01000 # The instruction uses value of the 5 operand 642 | # CF_USE6 = 0x02000 # The instruction uses value of the 6 operand 643 | # CF_JUMP = 0x04000 # The instruction passes execution using indirect jump or call (thus needs additional analysis) 644 | # CF_SHFT = 0x08000 # Bit-shift instruction (shl,shr...) 645 | # CF_HLL = 0x10000 # Instruction may be present in a high level language function. 646 | insn_feature = insn.get_canon_feature() 647 | 648 | # Instruction mnemonic name 649 | insn_mnem = insn.get_canon_mnem() 650 | 651 | #if insn_mnem in self.mnems: self.mnems[insn_mnem] += 1 652 | #else: self.mnems[insn_mnem] = 1 653 | 654 | # Get instruction operand types 655 | # 656 | # op_t.type 657 | # Description Data field 658 | # o_void = 0 # No Operand ---------- 659 | # o_reg = 1 # General Register (al,ax,es,ds...) reg 660 | # o_mem = 2 # Direct Memory Reference (DATA) addr 661 | # o_phrase = 3 # Memory Ref [Base Reg + Index Reg] phrase 662 | # o_displ = 4 # Memory Reg [Base Reg + Index Reg + Displacement] phrase+addr 663 | # o_imm = 5 # Immediate Value value 664 | # o_far = 6 # Immediate Far Address (CODE) addr 665 | # o_near = 7 # Immediate Near Address (CODE) addr 666 | insn_op1 = insn.Operands[0].type 667 | insn_op2 = insn.Operands[1].type 668 | 669 | ############################################################### 670 | # Filter gadget 671 | ############################################################### 672 | 673 | # Do not filter ROP, JOP, COP, always decode them 674 | # NOTE: A separate check must be done to check if they are out of place. 675 | if not insn_mnem in ["retn","jmp","call"]: 676 | 677 | # Filter gadgets with instructions that don't forward execution to the next address 678 | if insn_feature & idaapi.CF_STOP: 679 | return None 680 | 681 | # Filter gadgets with instructions in a bad list 682 | elif insn_mnem in self.ropBadMnems: 683 | return None 684 | 685 | # Filter gadgets with jump instructions 686 | # Note: conditional jumps may still be useful if we can 687 | # set flags prior to calling them. 688 | elif not self.ropAllowJcc and insn_mnem[0] == "j": 689 | return None 690 | 691 | ############################################################### 692 | # Get disassembly 693 | ############################################################### 694 | # NOTE: GENDSM_FORCE_CODE ensures correct decoding 695 | # of split instructions. 696 | insn_disas = idc.GetDisasmEx(ea, idaapi.GENDSM_FORCE_CODE) 697 | insn_disas = insn_disas.partition(';')[0] # Remove comments from disassembly 698 | insn_disas = ' '.join(insn_disas.split()) # Remove extraneous space from disassembly 699 | 700 | ############################################################### 701 | # Analyze instruction 702 | ############################################################### 703 | 704 | # Standalone instruction 705 | if insn_op1 == idaapi.o_void: 706 | 707 | # TODO: Determine and test how these instructions affect the stack 708 | # in 32-bit and 64-bit modes. 709 | if insn_mnem in ["pusha","pushad","popa","popad","pushf","pushfd","pushfq","popf","popfd","popfq"]: 710 | insn_operations.add("stack") 711 | 712 | if insn_mnem in ["popa","popad"]: 713 | insn_pivot += 7*4 714 | elif insn_mnem in ["pusha","pushad"]: 715 | insn_pivot -= 8*4 716 | elif insn_mnem in ["popf","popfd"]: 717 | insn_pivot += 4 718 | elif insn_mnem in ["pushf","pushfd"]: 719 | insn_pivot -= 4 720 | elif insn_mnem == "popfq": # TODO: Needs testing 721 | insn_pivot += 8 722 | elif insn_mnem == "pushfq": # TODO: Needs testing 723 | insn_pivot -= 8 724 | 725 | # Single operand instruction 726 | elif insn_op2 == idaapi.o_void: 727 | 728 | # Single operand register 729 | if insn_op1 == idaapi.o_reg: 730 | insn_operations.add("one-reg") 731 | 732 | if insn_feature & idaapi.CF_CHG1: 733 | reg_name = self.get_o_reg_name(insn, 0) 734 | insn_chg_registers.add(reg_name) 735 | 736 | # Check for stack operation 737 | if reg_name[1:] == "sp": 738 | insn_operations.add("stack") 739 | 740 | if insn_mnem == "inc": 741 | insn_pivot += 1 742 | 743 | elif insn_mnem == "dec": 744 | insn_pivot -= 1 745 | 746 | elif insn_feature & idaapi.CF_USE1: 747 | reg_name = self.get_o_reg_name(insn, 0) 748 | insn_use_registers.add(reg_name) 749 | 750 | # Single operand immediate 751 | elif insn_op1 == idaapi.o_imm: 752 | insn_operations.add("one-imm") 753 | 754 | # Single operand reference 755 | # TODO: determine the [reg + ...] value if present 756 | elif insn_op1 == idaapi.o_phrase or insn_op1 == idaapi.o_displ: 757 | insn_operations.add("one-mem") 758 | 759 | # PUSH/POP mnemonic with a any operand type 760 | if insn_mnem in ["push","pop"]: 761 | insn_operations.add("stack") 762 | 763 | # Adjust pivot based on operand size (32bit vs 64bit) 764 | if insn_mnem == "pop": 765 | if insn.Operands[0].dtyp == idaapi.dt_dword: insn_pivot += 4 766 | elif insn.Operands[0].dtyp == idaapi.dt_qword: insn_pivot += 8 767 | elif insn_mnem == "push": 768 | if insn.Operands[0].dtyp == idaapi.dt_dword: insn_pivot -= 4 769 | elif insn.Operands[0].dtyp == idaapi.dt_qword: insn_pivot -= 8 770 | 771 | # Check for arithmetic operation: 772 | if insn_mnem in self.insn_arithmetic_ops: 773 | insn_operations.add("math") 774 | 775 | # Check for bit-wise operations: 776 | if insn_mnem in self.insn_bit_ops: 777 | insn_operations.add("bit") 778 | 779 | # Two operand instruction 780 | else: 781 | 782 | # Check for arithmetic operations 783 | if insn_mnem in self.insn_arithmetic_ops: 784 | insn_operations.add("math") 785 | 786 | # Check for bit-wise operations 787 | if insn_mnem in self.insn_bit_ops: 788 | insn_operations.add("bit") 789 | 790 | # Two operand instruction with the first operand a register 791 | if insn_op1 == idaapi.o_reg: 792 | 793 | reg_name = self.get_o_reg_name(insn, 0) 794 | 795 | if insn_feature & idaapi.CF_CHG1: 796 | insn_chg_registers.add(reg_name) 797 | 798 | # Check for stack operation 799 | if reg_name[1:] == "sp": 800 | insn_operations.add("stack") 801 | 802 | # Determine stack pivot distance 803 | if insn_op2 == idaapi.o_imm: 804 | 805 | # NOTE: adb and sbb may also be useful, but let the user 806 | # determine their use by locating the operations "stack" 807 | if insn_mnem == "add": 808 | insn_pivot += insn.Operands[1].value 809 | 810 | elif insn_mnem == "sub": 811 | insn_pivot -= insn.Operands[1].value 812 | 813 | # Check for operations 814 | if insn_op2 == idaapi.o_reg: 815 | insn_operations.add("reg-to-reg") 816 | elif insn_op2 == idaapi.o_imm: 817 | insn_operations.add("imm-to-reg") 818 | 819 | # TODO: determine the [reg + ...] value if present 820 | elif insn_op2 == idaapi.o_phrase or insn_op2 == idaapi.o_displ: 821 | insn_operations.add("mem-to-reg") 822 | 823 | if insn_feature & idaapi.CF_USE1: 824 | insn_use_registers.add(reg_name) 825 | 826 | 827 | # Two operand instruction with the second operand a register 828 | if insn_op2 == idaapi.o_reg: 829 | 830 | reg_name = self.get_o_reg_name(insn, 1) 831 | 832 | if insn_feature & idaapi.CF_CHG2: 833 | insn_chg_registers.add(reg_name) 834 | 835 | # Check for stack operation 836 | if reg_name[1:] == "sp": 837 | insn_operations.add("stack") 838 | 839 | if insn_feature & idaapi.CF_USE2: 840 | insn_use_registers.add(reg_name) 841 | 842 | # Check for operations 843 | # TODO: determine the [reg + ...] value if present 844 | if insn_op1 == idaapi.o_phrase or insn_op1 == idaapi.o_displ: 845 | insn_operations.add("reg-to-mem") 846 | 847 | # Build instruction dictionary 848 | insn = dict() 849 | insn["insn_mnem"] = insn_mnem 850 | insn["insn_disas"] = insn_disas 851 | insn["insn_operations"] = insn_operations 852 | insn["insn_chg_registers"] = insn_chg_registers 853 | insn["insn_use_registers"] = insn_use_registers 854 | insn["insn_pivot"] = insn_pivot 855 | 856 | return insn 857 | 858 | 859 | class IdaRopEngine(): 860 | """ Ida ROP Engine class for process all kinds of data """ 861 | 862 | def __init__(self): 863 | self.rop = None 864 | 865 | if not idaapi.ph.id == idaapi.PLFM_386: 866 | logging.error ("[IdaRop] Only Intel 80x86 processors are supported.") 867 | sys.exit(1) 868 | 869 | # Check if processor supports 64-bit addressing 870 | if idaapi.ph.flag & idaapi.PR_USE64: 871 | self.addr64 = True 872 | self.addr_format = "%016X" 873 | self.pack_format_be = ">Q" 874 | self.pack_format_le = "> 2, 906 | w = (seg.perm & idaapi.SEGPERM_WRITE) >> 1, 907 | x = (seg.perm & idaapi.SEGPERM_EXEC), 908 | segclass = idaapi.get_segm_class(seg) 909 | ) 910 | 911 | self.segments.append(segentry) 912 | self.segments_idx.append(n) 913 | 914 | return self.segments 915 | 916 | def clear_rop_list(self): 917 | """ Clear previous rop search results """ 918 | self.rop.gadgets = list() 919 | 920 | def process_rop(self, form, select_list = None): 921 | """ Look for rop gadgets using user-input search options """ 922 | 923 | # Clear previous results 924 | self.clear_rop_list() 925 | 926 | # Get selected segments 927 | self.rop.segments = [self.segments_idx[i] for i in select_list] 928 | 929 | if len(self.rop.segments) > 0: 930 | 931 | # Filter bad characters 932 | buf = form.strBadChars.value 933 | buf = buf.replace(' ','') # remove spaces 934 | buf = buf.replace('\\x','') # remove '\x' prefixes 935 | buf = buf.replace('0x','') # remove '0x' prefixes 936 | try: 937 | buf = binascii.unhexlify(buf) # convert to bytes 938 | self.ptrBadChars = buf 939 | except Exception as e: 940 | idaapi.warning("Invalid input: %s" % e) 941 | self.ptrBadChars = "" 942 | 943 | # Ascii_to_Unicode_transformation table 944 | # BUG: DropdownControl does not work on IDA 6.5 945 | self.unicodeTable = form.radUnicode.value 946 | 947 | # ROP instruction filters 948 | self.rop.ropBadMnems = [mnem.strip().lower() for mnem in form.strBadMnems.value.split(',')] 949 | self.rop.ropAllowJcc = form.cRopAllowJcc.checked 950 | self.rop.ropNoBadBytes = form.cRopNoBadBytes.checked 951 | 952 | # Get ROP engine settings 953 | self.rop.maxRopSize = form.intMaxRopSize.value 954 | self.rop.maxRopOffset = form.intMaxRopOffset.value 955 | self.rop.maxRops = form.intMaxRops.value 956 | self.rop.maxRetnImm = form.intMaxRetnImm.value 957 | 958 | # Gadget search values 959 | self.rop.searchRop = form.cRopSearch.checked 960 | self.rop.searchJop = form.cJopSearch.checked 961 | self.rop.searchSys = form.cSysSearch.checked 962 | 963 | # Search for returns and ROP gadgets 964 | self.rop.search_retns() 965 | self.rop.search_gadgets() 966 | 967 | return True 968 | 969 | else: 970 | idaapi.warning("No segments selected.") 971 | return False 972 | 973 | -------------------------------------------------------------------------------- /idarop/ui.py: -------------------------------------------------------------------------------- 1 | """ IDA ROP view plugin UI functions and classes """ 2 | 3 | # Python libraries 4 | import os 5 | import csv 6 | import logging 7 | 8 | # IDA libraries 9 | import idaapi 10 | import idc 11 | from idaapi import Form, Choose, Choose2 12 | 13 | # Choose2 has disappeared from IdaPython v7 14 | if idaapi.IDA_SDK_VERSION <= 695: 15 | from idaapi import Choose2 as SegmentChoose 16 | elif idaapi.IDA_SDK_VERSION >= 700: 17 | import ida_idaapi 18 | from idaapi import Choose as SegmentChoose 19 | else: 20 | pass 21 | 22 | # IDA plugin 23 | try : 24 | import netnode 25 | netnode_package = True 26 | except ImportError as ie: 27 | netnode_package = False 28 | 29 | 30 | from .engine import IdaRopEngine, IdaRopSearch, Gadget 31 | 32 | 33 | class IdaRopForm(Form): 34 | """ Ida Rop Search input form """ 35 | 36 | def __init__(self, idaropengine, select_list = None): 37 | 38 | self.engine = idaropengine 39 | self.select_list = select_list 40 | self.segments = SegmentView(self.engine) 41 | 42 | 43 | Form.__init__(self, 44 | r"""BUTTON YES* Search 45 | Search ROP gadgets 46 | 47 | {FormChangeCb} 48 | 49 | 50 | Unicode Table {radUnicode}> 51 | 52 | 53 | 54 | 55 | {gadgetGroup}> 56 | 57 | Others settings: 58 | {ropGroup}> 59 | """, { 60 | 'cEChooser' : Form.EmbeddedChooserControl(self.segments, swidth=110), 61 | 'ropGroup' : Form.ChkGroupControl(('cRopAllowJcc','cRopNoBadBytes')), 62 | 'gadgetGroup' : Form.ChkGroupControl(('cRopSearch','cJopSearch','cSysSearch')), 63 | 'intMaxRopSize' : Form.NumericInput(swidth=4,tp=Form.FT_DEC,value=self.engine.rop.maxRopSize), 64 | 'intMaxRopOffset' : Form.NumericInput(swidth=4,tp=Form.FT_DEC,value=self.engine.rop.maxRopOffset), 65 | 'intMaxRops' : Form.NumericInput(swidth=4,tp=Form.FT_DEC,value=self.engine.rop.maxRops), 66 | 'intMaxRetnImm' : Form.NumericInput(swidth=4,tp=Form.FT_HEX,value=self.engine.rop.maxRetnImm), 67 | 'intMaxJopImm' : Form.NumericInput(swidth=4,tp=Form.FT_HEX,value=self.engine.rop.maxJopImm), 68 | 'strBadChars' : Form.StringInput(swidth=92,tp=Form.FT_ASCII), 69 | 'radUnicode' : Form.RadGroupControl(("rUnicodeANSI","rUnicodeOEM","rUnicodeUTF7","rUnicodeUTF8")), 70 | 'strBadMnems' : Form.StringInput(swidth=92,tp=Form.FT_ASCII,value="into, in, out, loop, loope, loopne, lock, rep, repe, repz, repne, repnz"), 71 | 'FormChangeCb' : Form.FormChangeCb(self.OnFormChange), 72 | }) 73 | 74 | self.Compile() 75 | 76 | def OnFormChange(self, fid): 77 | 78 | # Form initialization 79 | if fid == -1: 80 | self.SetFocusedField(self.cEChooser) 81 | 82 | # Preselect executable segments on startup if none were already specified: 83 | if self.select_list == None: 84 | 85 | self.select_list = list() 86 | 87 | for i, seg in enumerate(self.engine.segments): 88 | if seg.x: 89 | self.select_list.append(i) 90 | 91 | self.SetControlValue(self.cEChooser, self.select_list) 92 | 93 | # Enable both ROP and JOP search by default 94 | self.SetControlValue(self.cRopSearch, True) 95 | self.SetControlValue(self.cJopSearch, True) 96 | self.SetControlValue(self.cSysSearch, False) 97 | 98 | # Skip bad instructions by default 99 | self.SetControlValue(self.cRopNoBadBytes, True) 100 | 101 | # Form OK pressed 102 | elif fid == -2: 103 | pass 104 | 105 | return 1 106 | 107 | ############################################################################### 108 | class SegmentView(SegmentChoose): 109 | 110 | def __init__(self, idarop): 111 | 112 | self.idarop = idarop 113 | 114 | SegmentChoose.__init__(self, "Segments", 115 | [ ["Name", 13 | SegmentChoose.CHCOL_PLAIN], 116 | ["Start", 13 | SegmentChoose.CHCOL_HEX], 117 | ["End", 10 | SegmentChoose.CHCOL_HEX], 118 | ["Size", 10 | SegmentChoose.CHCOL_HEX], 119 | ["R", 1 | SegmentChoose.CHCOL_PLAIN], 120 | ["W", 1 | SegmentChoose.CHCOL_PLAIN], 121 | ["X", 1 | SegmentChoose.CHCOL_PLAIN], 122 | ["Class", 8 | SegmentChoose.CHCOL_PLAIN], 123 | ], 124 | flags = SegmentChoose.CH_MULTI, # Select multiple modules 125 | embedded=True) 126 | 127 | self.icon = 150 128 | 129 | # Items for display 130 | self.items = list() 131 | 132 | # Selected items 133 | self.select_list = list() 134 | 135 | # Initialize/Refresh the view 136 | self.refreshitems() 137 | 138 | def OnSelectionChange(self, selection_list): 139 | # "Temporary" crutch since GetEmbSelection() does not work for Ida 7.0 140 | self.select_list = selection_list 141 | 142 | def show(self): 143 | # Attempt to open the view 144 | if self.Show() < 0: return False 145 | 146 | def refreshitems(self): 147 | self.items = list() 148 | 149 | for segment in self.idarop.list_segments(): 150 | self.items.append(segment.get_display_list()) 151 | 152 | def OnCommand(self, n, cmd_id): 153 | 154 | # Search ROP gadgets 155 | if cmd_id == self.cmd_search_gadgets: 156 | 157 | # Initialize ROP gadget form with empty selection 158 | self.idarop.process_rop(select_list = self.select_list) 159 | 160 | def OnSelectLine(self, n): 161 | pass 162 | 163 | def OnGetLine(self, n): 164 | return self.items[n] 165 | 166 | def OnGetIcon(self, n): 167 | 168 | 169 | if not len(self.items) > 0: 170 | return -1 171 | 172 | segment = self.idarop.list_segments()[n] 173 | 174 | if segment.x : # Exec Seg 175 | return 61 176 | else: 177 | return 59 178 | 179 | def OnClose(self): 180 | self.cmd_search_gadgets = None 181 | 182 | def OnGetSize(self): 183 | return len(self.items) 184 | 185 | def OnRefresh(self, n): 186 | self.refreshitems() 187 | return n 188 | 189 | def OnActivate(self): 190 | self.refreshitems() 191 | 192 | 193 | class IdaRopView(Choose2): 194 | """ 195 | Chooser class to display security characteristics of loaded modules. 196 | """ 197 | def __init__(self, idarop): 198 | 199 | self.idarop = idarop 200 | 201 | Choose2.__init__(self, 202 | "ROP gadgets", 203 | [ ["Segment", 13 | Choose2.CHCOL_PLAIN], 204 | ["Address", 13 | Choose2.CHCOL_HEX], 205 | ["Return Address", 13 | Choose2.CHCOL_HEX], 206 | ["Gadget", 30 | Choose2.CHCOL_PLAIN], 207 | ["Opcodes", 20 | Choose2.CHCOL_PLAIN], 208 | ["Size", 3 | Choose2.CHCOL_DEC], 209 | ["Pivot", 4 | Choose2.CHCOL_DEC], 210 | ], 211 | flags = Choose2.CH_MULTI) 212 | 213 | self.icon = 182 214 | 215 | # Items for display 216 | self.items = [] 217 | 218 | # rop list cache for instantaneous loading if there has not been any new data 219 | self.rop_list_cache = None 220 | 221 | # Initialize/Refresh the view 222 | self.refreshitems() 223 | 224 | # export as csv command 225 | self.cmd_export_csv = None 226 | 227 | # clear result command 228 | self.clear_rop_list = None 229 | 230 | 231 | 232 | def show(self): 233 | # Attempt to open the view 234 | if self.Show() < 0: return False 235 | 236 | if self.cmd_export_csv == None: 237 | self.cmd_export_csv = self.AddCommand("Export as csv...", flags = idaapi.CHOOSER_POPUP_MENU, icon=40) 238 | if self.clear_rop_list == None: 239 | self.clear_rop_list = self.AddCommand("Clear rop list", flags = idaapi.CHOOSER_POPUP_MENU, icon=32) 240 | 241 | return True 242 | 243 | def refreshitems(self): 244 | 245 | # Pb : rop engine has not been init 246 | if self.idarop.rop == None: 247 | return 248 | 249 | # No new data present 250 | if self.rop_list_cache == self.idarop.rop.gadgets: 251 | return 252 | 253 | self.items = [] 254 | 255 | # No data present 256 | if len(self.idarop.rop.gadgets) == 0: 257 | return 258 | 259 | 260 | if len(self.idarop.rop.gadgets) > 10000: 261 | idaapi.show_wait_box("Ida rop : loading rop list ...") 262 | 263 | for i,g in enumerate(self.idarop.rop.gadgets): 264 | 265 | # reconstruct disas 266 | if g.opcodes == "": 267 | 268 | bad_gadget = False 269 | opcodes = idc.GetManyBytes(g.address, g.ret_address - g.address + 1) 270 | instructions = list() 271 | ea = g.address 272 | while ea <= g.ret_address: 273 | instructions.append(idc.GetDisasmEx(ea, idaapi.GENDSM_FORCE_CODE)) 274 | ea += idaapi.decode_insn(ea) 275 | 276 | # Badly decoded gadget 277 | if idaapi.decode_insn(ea) == 0: 278 | bad_gadget = True 279 | break 280 | 281 | 282 | if not bad_gadget: 283 | h = Gadget( 284 | address = g.address, 285 | ret_address = g.ret_address, 286 | instructions = instructions, 287 | opcodes = opcodes, 288 | size = len(opcodes) 289 | ) 290 | self.idarop.rop.gadgets[i] = h 291 | 292 | self.items.append(h.get_display_list(self.idarop.addr_format)) 293 | else: 294 | self.items.append(g.get_display_list(self.idarop.addr_format)) 295 | 296 | self.rop_list_cache = self.idarop.rop.gadgets 297 | if len(self.idarop.rop.gadgets) > 10000: 298 | idaapi.hide_wait_box() 299 | 300 | 301 | 302 | def OnCommand(self, n, cmd_id): 303 | 304 | # Export CSV 305 | if cmd_id == self.cmd_export_csv: 306 | 307 | file_name = idaapi.askfile_c(1, "*.csv", "Please enter CSV file name") 308 | if file_name: 309 | print ("[idarop] Exporting gadgets to %s" % file_name) 310 | with open(file_name, 'wb') as csvfile: 311 | csvwriter = csv.writer(csvfile, delimiter=',', 312 | quotechar='"', quoting=csv.QUOTE_MINIMAL) 313 | csvwriter.writerow(["Address","Gadget","Size","Pivot"]) 314 | for item in self.items: 315 | csvwriter.writerow(item) 316 | 317 | elif cmd_id == self.clear_rop_list: 318 | self.idarop.clear_rop_list() 319 | self.refreshitems() 320 | 321 | return 1 322 | 323 | def OnSelectLine(self, n): 324 | """ Callback on double click line : should open a custom view with the disas gadget. 325 | IDA disass view can't show "unaligned" gadgets. 326 | """ 327 | idaapi.jumpto( self.idarop.rop.gadgets[n].address ) 328 | 329 | def OnGetLine(self, n): 330 | return self.items[n] 331 | 332 | def OnClose(self): 333 | self.cmd_export_csv = None 334 | self.clear_rop_list = None 335 | 336 | def OnGetSize(self): 337 | return len(self.items) 338 | 339 | def OnRefresh(self, n): 340 | self.refreshitems() 341 | return n 342 | 343 | def OnActivate(self): 344 | self.refreshitems() 345 | 346 | 347 | if idaapi.IDA_SDK_VERSION >= 700: 348 | class SearchGadgetsHandler(idaapi.action_handler_t): 349 | def __init__(self, manager): 350 | idaapi.action_handler_t.__init__(self) 351 | self._manager = manager 352 | 353 | def activate(self, ctx): 354 | self._manager.proc_rop() 355 | return 1 356 | 357 | def update(self, ctx): 358 | return idaapi.AST_ENABLE_ALWAYS 359 | 360 | class ListGadgetsHandler(idaapi.action_handler_t): 361 | def __init__(self, manager): 362 | idaapi.action_handler_t.__init__(self) 363 | self._manager = manager 364 | 365 | def activate(self, ctx): 366 | self._manager.show_rop_view() 367 | return 1 368 | 369 | def update(self, ctx): 370 | return idaapi.AST_ENABLE_ALWAYS 371 | else: 372 | pass 373 | 374 | class IdaRopManager(): 375 | """ Top-level object managing IDA Rop View plugin """ 376 | 377 | def __init__(self): 378 | 379 | # Initialize ROP gadget search engine 380 | self.engine = IdaRopEngine() 381 | self.engine.rop = IdaRopSearch(self.engine) 382 | self.ropView = IdaRopView(self.engine) 383 | 384 | # Defered csv loading for a faster startup 385 | self.defered_loading = False 386 | 387 | # List of menu item added by the plugin 388 | self.addmenu_item_ctxs = list() 389 | 390 | # blob manager for saving internal db into idb file 391 | self.blob_manager = None 392 | if netnode_package : 393 | self.blob_manager = netnode.Netnode("$ idarop.rop_blob") 394 | else: 395 | print("[IdaRop] IdaRop rely on the Netnode package to save the rop database in the idb file.") 396 | print(" Since it's not present, the results will be discarded when closing IDA.") 397 | 398 | 399 | def add_menu_items(self): 400 | """ Init additions to Ida's menu entries """ 401 | 402 | if idaapi.IDA_SDK_VERSION <= 695: 403 | def add_menu_item_helper(self, menupath, name, hotkey, flags, pyfunc, args): 404 | """ helper for adding a menu item """ 405 | 406 | # add menu item and report on errors 407 | addmenu_item_ctx = idaapi.add_menu_item(menupath, name, hotkey, flags, pyfunc, args) 408 | if addmenu_item_ctx is None: 409 | return 1 410 | else: 411 | self.addmenu_item_ctxs.append(addmenu_item_ctx) 412 | return 0 413 | 414 | if add_menu_item_helper(self, "Search/all error operands", "list rop gadgets...", "Ctrl+Shift+r", 1, self.proc_rop, None): return 1 415 | if add_menu_item_helper(self, "View/Open subviews/Problems", "View rop gadgets...", "Shift+r", 1, self.show_rop_view, None): return 1 416 | return 0 417 | 418 | elif idaapi.IDA_SDK_VERSION >= 700: 419 | 420 | search_gadgets_desc = idaapi.action_desc_t( 421 | 'idarop:searchgadgets', 422 | 'search rop gadgets...', 423 | SearchGadgetsHandler(self), 424 | "Ctrl+Shift+r", 425 | 'search all gadgets available on this binary', 426 | ) 427 | idaapi.register_action(search_gadgets_desc) 428 | 429 | idaapi.attach_action_to_menu( 430 | 'Search/IdaRop/', 431 | 'idarop:searchgadgets', 432 | idaapi.SETMENU_APP 433 | ) 434 | 435 | list_gadgets_desc = idaapi.action_desc_t( 436 | 'idarop:listgadgets', 437 | 'list rop gadgets...', 438 | ListGadgetsHandler(self), 439 | "Shift+r", 440 | 'list all gadgets searched on this binary', 441 | ) 442 | idaapi.register_action(list_gadgets_desc) 443 | 444 | idaapi.attach_action_to_menu( 445 | 'Search/IdaRop/', 446 | 'idarop:listgadgets', 447 | idaapi.SETMENU_APP 448 | ) 449 | 450 | return 0 451 | else: 452 | return 0 453 | 454 | 455 | def del_menu_items(self): 456 | """ Clear Ida Rop plugin menu entries """ 457 | if idaapi.IDA_SDK_VERSION <= 695: 458 | for addmenu_item_ctx in self.addmenu_item_ctxs: 459 | idaapi.del_menu_item(addmenu_item_ctx) 460 | elif idaapi.IDA_SDK_VERSION >= 700: 461 | idaapi.detach_action_from_menu('Search/IdaRop/', 'idarop:listgadgets') 462 | idaapi.detach_action_from_menu('Search/IdaRop/', 'idarop:searchgadgets') 463 | else: 464 | pass 465 | 466 | 467 | def show_rop_view(self): 468 | """ Show the list of rop gadgets found """ 469 | 470 | # If the default csv exist but has not been loaded, load here 471 | if self.defered_loading == True: 472 | idaapi.show_wait_box("loading gadgets db ...") 473 | self.load_default_csv(force = True) 474 | idaapi.hide_wait_box() 475 | self.defered_loading = False 476 | 477 | # Show the ROP gadgets view 478 | self.ropView.refreshitems() 479 | self.ropView.show() 480 | 481 | def proc_rop(self): 482 | """ Search for rop gadgets, based on user input options """ 483 | 484 | # Prompt user for ROP search settings 485 | f = IdaRopForm(self.engine) 486 | ok = f.Execute() 487 | if ok == 1: 488 | # reset previous results 489 | self.defered_loading = False 490 | 491 | select_list = f.segments.select_list 492 | ret = self.engine.process_rop(f, select_list) 493 | 494 | if ret: 495 | self.show_rop_view() 496 | 497 | # force redraw of every list views 498 | idaapi.refresh_lists() 499 | 500 | f.Free() 501 | 502 | 503 | def save_internal_db(self): 504 | """ store the found rop gadget in the default internal db """ 505 | 506 | if len(self.engine.rop.gadgets) == 0 or self.blob_manager == None: 507 | return 508 | 509 | cancel_flag = False 510 | internal_repr = list() 511 | for item in self.engine.rop.gadgets: 512 | 513 | address,ret_addres = item.address, item.ret_address 514 | offset = "0x%x" % (address - idaapi.get_imagebase()) 515 | ret_offset = "0x%x" % (ret_addres - idaapi.get_imagebase()) 516 | 517 | internal_repr.append((offset, ret_offset)) 518 | 519 | if idaapi.wasBreak(): 520 | cancel_flag = True 521 | print("[IdaRop] save internal db interrupted.") 522 | break 523 | 524 | # save only on success 525 | if not cancel_flag: 526 | txt_repr = ";".join( "%s:%s" % (g[0],g[1]) for g in internal_repr) 527 | self.blob_manager["db"] = txt_repr 528 | 529 | def load_internal_db(self, force=False): 530 | """ Load the rop gadgets list from the internal db """ 531 | 532 | if self.blob_manager == None : 533 | return 534 | 535 | internal_repr = self.blob_manager["db"].split(";") 536 | if internal_repr == None: 537 | return 538 | 539 | for item in internal_repr: 540 | offset,ret_offset = item.split(':') 541 | 542 | # Reconstruct linear address based on binary base address and offset 543 | address = int(offset, 16) + idaapi.get_imagebase() 544 | ret_address = int(ret_offset, 16) + idaapi.get_imagebase() 545 | 546 | gadget = Gadget( 547 | address = address, 548 | ret_address = ret_address, 549 | instructions = list(), 550 | opcodes = "", 551 | size = 0 552 | ) 553 | 554 | self.engine.rop.gadgets.append(gadget) 555 | 556 | if idaapi.wasBreak(): 557 | print("[IdaRopLoad] Load csv file interrupted.") 558 | break -------------------------------------------------------------------------------- /netnode/__init__.py: -------------------------------------------------------------------------------- 1 | from netnode import Netnode 2 | -------------------------------------------------------------------------------- /netnode/netnode.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | import json 3 | import logging 4 | 5 | import idaapi 6 | 7 | BLOB_SIZE = 1024 8 | OUR_NETNODE = "$ com.williballenthin" 9 | INT_KEYS_TAG = 'M' 10 | STR_KEYS_TAG = 'N' 11 | STR_TO_INT_MAP_TAG = 'O' 12 | INT_TO_INT_MAP_TAG = 'P' 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class NetnodeCorruptError(RuntimeError): 17 | pass 18 | 19 | 20 | class Netnode(object): 21 | """ 22 | A netnode is a way to persistently store data in an IDB database. 23 | The underlying interface is a bit weird, so you should read the IDA 24 | documentation on the subject. Some places to start: 25 | 26 | - https://www.hex-rays.com/products/ida/support/sdkdoc/netnode_8hpp.html 27 | - The IDA Pro Book, version 2 28 | 29 | Conceptually, this netnode class represents is a key-value store 30 | uniquely identified by a namespace. 31 | 32 | This class abstracts over some of the peculiarities of the low-level 33 | netnode API. Notably, it supports indexing data by strings or 34 | numbers, and allows values to be larger than 1024 bytes in length. 35 | 36 | This class supports keys that are numbers or strings. 37 | Values must be JSON-encodable. They can not be None. 38 | 39 | Implementation: 40 | (You don't have to worry about this section if you just want to 41 | use the library. Its here for potential contributors.) 42 | 43 | The major limitation of the underlying netnode API is the fixed 44 | maximum length of a value. Values must not be larger than 1024 45 | bytes. Otherwise, you must use the `blob` API. We do that for you. 46 | 47 | The first enhancement is transparently zlib-encoding all values. 48 | 49 | To support arbitrarily sized values with keys of either int or str types, 50 | we store the values in different places: 51 | 52 | - integer keys with small values: stored in default supval table 53 | - integer keys with large values: the data is stored in the blob 54 | table named 'M' using an internal key. The link from the given key 55 | to the internal key is stored in the supval table named 'P'. 56 | - string keys with small values: stored in default hashval table 57 | - string keys with large values: the data is stored in the blob 58 | table named 'N' using an integer key. The link from string key 59 | to int key is stored in the supval table named 'O'. 60 | """ 61 | def __init__(self, netnode_name=OUR_NETNODE): 62 | self._netnode_name = netnode_name 63 | #self._n = idaapi.netnode(netnode_name, namelen=0, do_create=True) 64 | self._n = idaapi.netnode(netnode_name, 0, True) 65 | 66 | @staticmethod 67 | def _decompress(data): 68 | return zlib.decompress(data) 69 | 70 | @staticmethod 71 | def _compress(data): 72 | return zlib.compress(data) 73 | 74 | @staticmethod 75 | def _encode(data): 76 | return json.dumps(data) 77 | 78 | @staticmethod 79 | def _decode(data): 80 | return json.loads(data) 81 | 82 | def _intdel(self, key): 83 | assert isinstance(key, (int, long)) 84 | 85 | did_del = False 86 | storekey = self._n.supval(key, INT_TO_INT_MAP_TAG) 87 | if storekey is not None: 88 | storekey = int(storekey) 89 | self._n.delblob(storekey, INT_KEYS_TAG) 90 | self._n.supdel(key) 91 | did_del = True 92 | if self._n.supval(key) is not None: 93 | self._n.supdel(key) 94 | did_del = True 95 | 96 | if not did_del: 97 | raise KeyError("'{}' not found".format(key)) 98 | 99 | def _get_next_slot(self, tag): 100 | ''' 101 | get the first unused supval table key, or 0 if the 102 | table is empty. 103 | useful for filling the supval table sequentially. 104 | ''' 105 | slot = self._n.suplast(tag) 106 | if slot is None or slot == idaapi.BADNODE: 107 | return 0 108 | else: 109 | return slot + 1 110 | 111 | def _intset(self, key, value): 112 | assert isinstance(key, (int, long)) 113 | assert value is not None 114 | 115 | try: 116 | self._intdel(key) 117 | except KeyError: 118 | pass 119 | 120 | if len(value) > BLOB_SIZE: 121 | storekey = self._get_next_slot(INT_KEYS_TAG) 122 | self._n.setblob(value, storekey, INT_KEYS_TAG) 123 | self._n.supset(key, str(storekey), INT_TO_INT_MAP_TAG) 124 | else: 125 | self._n.supset(key, value) 126 | 127 | def _intget(self, key): 128 | assert isinstance(key, (int, long)) 129 | 130 | storekey = self._n.supval(key, INT_TO_INT_MAP_TAG) 131 | if storekey is not None: 132 | storekey = int(storekey) 133 | v = self._n.getblob(storekey, INT_KEYS_TAG) 134 | if v is None: 135 | raise NetnodeCorruptError() 136 | return v 137 | 138 | v = self._n.supval(key) 139 | if v is not None: 140 | return v 141 | 142 | raise KeyError("'{}' not found".format(key)) 143 | 144 | def _strdel(self, key): 145 | assert isinstance(key, (basestring)) 146 | 147 | did_del = False 148 | storekey = self._n.hashval(key, STR_TO_INT_MAP_TAG) 149 | if storekey is not None: 150 | storekey = int(storekey) 151 | self._n.delblob(storekey, STR_KEYS_TAG) 152 | self._n.hashdel(key) 153 | did_del = True 154 | if self._n.hashval(key): 155 | self._n.hashdel(key) 156 | did_del = True 157 | 158 | if not did_del: 159 | raise KeyError("'{}' not found".format(key)) 160 | 161 | def _strset(self, key, value): 162 | assert isinstance(key, (basestring)) 163 | assert value is not None 164 | 165 | try: 166 | self._strdel(key) 167 | except KeyError: 168 | pass 169 | 170 | if len(value) > BLOB_SIZE: 171 | storekey = self._get_next_slot(STR_KEYS_TAG) 172 | self._n.setblob(value, storekey, STR_KEYS_TAG) 173 | self._n.hashset(key, str(storekey), STR_TO_INT_MAP_TAG) 174 | else: 175 | self._n.hashset(key, value) 176 | 177 | def _strget(self, key): 178 | assert isinstance(key, (basestring)) 179 | 180 | storekey = self._n.hashval(key, STR_TO_INT_MAP_TAG) 181 | if storekey is not None: 182 | storekey = int(storekey) 183 | v = self._n.getblob(storekey, STR_KEYS_TAG) 184 | if v is None: 185 | raise NetnodeCorruptError() 186 | return v 187 | 188 | v = self._n.hashval(key) 189 | if v is not None: 190 | return v 191 | 192 | raise KeyError("'{}' not found".format(key)) 193 | 194 | def __getitem__(self, key): 195 | if isinstance(key, basestring): 196 | v = self._strget(key) 197 | elif isinstance(key, (int, long)): 198 | v = self._intget(key) 199 | else: 200 | raise TypeError("cannot use {} as key".format(type(key))) 201 | 202 | return self._decode(self._decompress(v)) 203 | 204 | def __setitem__(self, key, value): 205 | ''' 206 | does not support setting a value to None. 207 | value must be json-serializable. 208 | key must be a string or integer. 209 | ''' 210 | assert value is not None 211 | 212 | v = self._compress(self._encode(value)) 213 | if isinstance(key, basestring): 214 | self._strset(key, v) 215 | elif isinstance(key, (int, long)): 216 | self._intset(key, v) 217 | else: 218 | raise TypeError("cannot use {} as key".format(type(key))) 219 | 220 | def __delitem__(self, key): 221 | if isinstance(key, basestring): 222 | self._strdel(key) 223 | elif isinstance(key, (int, long)): 224 | self._intdel(key) 225 | else: 226 | raise TypeError("cannot use {} as key".format(type(key))) 227 | 228 | def get(self, key, default=None): 229 | try: 230 | return self[key] 231 | except KeyError: 232 | return default 233 | 234 | def __contains__(self, key): 235 | try: 236 | if self[key] is not None: 237 | return True 238 | return False 239 | except KeyError: 240 | return False 241 | 242 | def iterkeys(self): 243 | # integer keys for all small values 244 | i = self._n.sup1st() 245 | while i != idaapi.BADNODE: 246 | yield i 247 | i = self._n.supnxt(i) 248 | 249 | # integer keys for all big values 250 | i = self._n.sup1st(INT_TO_INT_MAP_TAG) 251 | while i != idaapi.BADNODE: 252 | yield i 253 | i = self._n.supnxt(i, INT_TO_INT_MAP_TAG) 254 | 255 | # string keys for all small values 256 | i = self._n.hash1st() 257 | while i != idaapi.BADNODE and i is not None: 258 | yield i 259 | i = self._n.hashnxt(i) 260 | 261 | # string keys for all big values 262 | i = self._n.hash1st(STR_TO_INT_MAP_TAG) 263 | while i != idaapi.BADNODE and i is not None: 264 | yield i 265 | i = self._n.hashnxt(i, STR_TO_INT_MAP_TAG) 266 | 267 | def keys(self): 268 | return [k for k in self.iterkeys()] 269 | 270 | def itervalues(self): 271 | for k in self.iterkeys(): 272 | yield self[k] 273 | 274 | def values(self): 275 | return [v for v in self.itervalues()] 276 | 277 | def iteritems(self): 278 | for k in self.iterkeys(): 279 | yield k, self[k] 280 | 281 | def items(self): 282 | return [(k, v) for k, v in self.iteritems()] 283 | 284 | def kill(self): 285 | self._n.kill() 286 | self._n = idaapi.netnode(self._netnode_name, 0, True) 287 | 288 | -------------------------------------------------------------------------------- /netnode/test_netnode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | import logging 5 | import contextlib 6 | 7 | import idaapi 8 | import pytest 9 | 10 | import netnode 11 | 12 | 13 | TEST_NAMESPACE = '$ some.namespace' 14 | 15 | 16 | def get_random_data(N): 17 | return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N)) 18 | 19 | 20 | @contextlib.contextmanager 21 | def killing_netnode(namespace): 22 | ''' 23 | wraps a netnode in a contextmanager that will 24 | eventually destroy its contents. 25 | probably only useful for testing when a clean state is req'd. 26 | ''' 27 | n = netnode.Netnode(namespace) 28 | try: 29 | yield n 30 | finally: 31 | n.kill() 32 | 33 | 34 | def test_basic_features(): 35 | ''' 36 | demonstrate the basic netnode API (like a dict) 37 | ''' 38 | with killing_netnode(TEST_NAMESPACE) as n: 39 | # there is nothing in the netnode to begin with 40 | assert(False == (1 in n)) 41 | 42 | # when we add one key, there is one thing in it 43 | n[1] = 'hello' 44 | assert(True == (1 in n)) 45 | assert(n[1] == 'hello') 46 | # but nothing else 47 | assert(False == ('2' in n)) 48 | 49 | # then when we add a second thing, its also there 50 | n['2'] = 'world' 51 | assert(True == ('2' in n)) 52 | assert(len(n.keys()) == 2) 53 | assert(n.keys()[0] == 1) 54 | assert(n.keys()[1] == '2') 55 | assert(len(n.values()) == 2) 56 | assert(n.values()[0] == 'hello') 57 | assert(n.values()[1] == 'world') 58 | assert(len(n.items()) == 2) 59 | 60 | # and when we delete the first item, only it is deleted 61 | del n[1] 62 | assert(False == (1 in n)) 63 | 64 | # and finally everything is gone 65 | del n['2'] 66 | 67 | 68 | def test_large_data(): 69 | ''' 70 | demonstrate that netnodes support large data values. 71 | ''' 72 | with killing_netnode(TEST_NAMESPACE) as n: 73 | random_data = get_random_data(1024 * 8) 74 | n[3] = random_data 75 | assert(n[3] == random_data) 76 | 77 | 78 | def test_hash_ordering(): 79 | ''' 80 | the following demonstrates that 'hashes' are iterated alphabetically. 81 | this is an IDAPython implementation feature. 82 | ''' 83 | 84 | with killing_netnode(TEST_NAMESPACE) as n: 85 | m = n._n 86 | 87 | def hashiter(m): 88 | i = m.hash1st() 89 | while i != idaapi.BADNODE and i is not None: 90 | yield i 91 | i = m.hashnxt(i) 92 | 93 | def get_hash_order(hiter): 94 | return [k for k in hiter] 95 | 96 | m.hashset('a', 'a') 97 | assert get_hash_order(hashiter(m)) == ['a'] 98 | 99 | m.hashset('c', 'c') 100 | assert get_hash_order(hashiter(m)) == ['a', 'c'] 101 | 102 | m.hashset('b', 'b') 103 | assert get_hash_order(hashiter(m)) == ['a', 'b', 'c'] 104 | 105 | 106 | def test_iterkeys(): 107 | LARGE_VALUE = get_random_data(16 * 1024) 108 | LARGE_VALUE2 = get_random_data(16 * 1024) 109 | import zlib 110 | assert(zlib.compress(LARGE_VALUE) > 1024) 111 | assert(zlib.compress(LARGE_VALUE2) > 1024) 112 | 113 | assert LARGE_VALUE != LARGE_VALUE2 114 | 115 | with killing_netnode(TEST_NAMESPACE) as n: 116 | n[1] = LARGE_VALUE 117 | assert set(n.keys()) == set([1]) 118 | 119 | n[2] = LARGE_VALUE2 120 | assert set(n.keys()) == set([1, 2]) 121 | 122 | assert n[1] != n[2] 123 | 124 | with killing_netnode(TEST_NAMESPACE) as n: 125 | n['one'] = LARGE_VALUE 126 | assert set(n.keys()) == set(['one']) 127 | 128 | n['two'] = LARGE_VALUE2 129 | assert set(n.keys()) == set(['one', 'two']) 130 | 131 | assert n['one'] != n['two'] 132 | 133 | with killing_netnode(TEST_NAMESPACE) as n: 134 | n[1] = LARGE_VALUE 135 | assert set(n.keys()) == set([1]) 136 | 137 | n[2] = LARGE_VALUE 138 | assert set(n.keys()) == set([1, 2]) 139 | 140 | n['one'] = LARGE_VALUE 141 | assert set(n.keys()) == set([1, 2, 'one']) 142 | 143 | n['two'] = LARGE_VALUE 144 | assert set(n.keys()) == set([1, 2, 'one', 'two']) 145 | 146 | n[3] = "A" 147 | assert set(n.keys()) == set([1, 2, 'one', 'two', 3]) 148 | 149 | n['three'] = "A" 150 | assert set(n.keys()) == set([1, 2, 'one', 'two', 3, 'three']) 151 | 152 | 153 | def main(): 154 | logging.basicConfig(level=logging.DEBUG) 155 | 156 | # cleanup any existing data 157 | netnode.Netnode(TEST_NAMESPACE).kill() 158 | 159 | pytest.main(['--capture=sys', os.path.dirname(__file__)]) 160 | 161 | 162 | if __name__ == '__main__': 163 | main() 164 | -------------------------------------------------------------------------------- /plugins/idarop_plugin_t.py: -------------------------------------------------------------------------------- 1 | # IDA libraries 2 | import idaapi 3 | from idaapi import plugin_t 4 | 5 | from idarop import IDAROP_VERSION, IDAROP_DESCRIPTION 6 | from idarop.ui import IdaRopManager 7 | 8 | class idarop_t(plugin_t): 9 | 10 | flags = idaapi.PLUGIN_UNL 11 | comment = IDAROP_DESCRIPTION 12 | help = IDAROP_DESCRIPTION 13 | wanted_name = "IDA ROP" 14 | wanted_hotkey = "" 15 | 16 | def init(self): 17 | """ On script initalisation : load previous rop results and init menu items """ 18 | 19 | # Only Intel x86/x86-64 are supported 20 | if idaapi.ph_get_id() == idaapi.PLFM_386: 21 | 22 | global idarop_manager 23 | 24 | # Check if already initialized 25 | if not 'idarop_manager' in globals(): 26 | 27 | idarop_manager = IdaRopManager() 28 | if idarop_manager.add_menu_items(): 29 | print("[IdaRop] Failed to initialize IDA Sploiter.") 30 | idarop_manager.del_menu_items() 31 | del idarop_manager 32 | return idaapi.PLUGIN_SKIP 33 | else: 34 | try: 35 | idarop_manager.load_internal_db() 36 | except Exception as e: 37 | pass 38 | 39 | print("[IdaRop] IDA ROP View v%s initialized " % IDAROP_VERSION) 40 | 41 | return idaapi.PLUGIN_KEEP 42 | else: 43 | return idaapi.PLUGIN_SKIP 44 | 45 | def run(self, arg): 46 | pass 47 | 48 | def term(self): 49 | """ On IDA's close event, export the Rop gadget list in a default csv file""" 50 | idaapi.show_wait_box("Saving gadgets ...") 51 | try: 52 | idarop_manager.save_internal_db() 53 | except Exception as e: 54 | pass 55 | idaapi.hide_wait_box() 56 | 57 | def PLUGIN_ENTRY(): 58 | return idarop_t() 59 | 60 | ############################################################################### 61 | # Script / Testing 62 | ############################################################################### 63 | 64 | def idarop_main(): 65 | global idarop_manager 66 | 67 | if 'idarop_manager' in globals(): 68 | idarop_manager.del_menu_items() 69 | del idarop_manager 70 | 71 | idarop_manager = IdaRopManager() 72 | idarop_manager.add_menu_items() 73 | 74 | idarop = idarop_manager.idarop 75 | 76 | if __name__ == '__main__': 77 | #idarop_main() 78 | pass -------------------------------------------------------------------------------- /screenshots/FilteringGadgets.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasg/idarop/9b090f98c3bf61c66c59c5b75c57ad115b6a2366/screenshots/FilteringGadgets.PNG -------------------------------------------------------------------------------- /screenshots/ListingGadgets.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasg/idarop/9b090f98c3bf61c66c59c5b75c57ad115b6a2366/screenshots/ListingGadgets.PNG -------------------------------------------------------------------------------- /screenshots/SearchForGadgets.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasg/idarop/9b090f98c3bf61c66c59c5b75c57ad115b6a2366/screenshots/SearchForGadgets.PNG -------------------------------------------------------------------------------- /screenshots/SearchingAndListingGadgets.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasg/idarop/9b090f98c3bf61c66c59c5b75c57ad115b6a2366/screenshots/SearchingAndListingGadgets.PNG -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import open 3 | from setuptools import setup, find_packages 4 | 5 | from __ida_setup__ import IdaPluginInstallCommand 6 | from idarop import IDAROP_VERSION, IDAROP_DESCRIPTION 7 | 8 | 9 | # read the contents of README file 10 | package_directory = os.path.abspath(os.path.dirname(__file__)) 11 | readme_path = os.path.join(package_directory, 'README.md') 12 | with open(readme_path, "r", encoding = 'utf-8') as f: 13 | long_description = f.read() 14 | 15 | 16 | setup( 17 | name = 'idarop', 18 | version = IDAROP_VERSION, 19 | description = IDAROP_DESCRIPTION, 20 | long_description=long_description, 21 | long_description_content_type='text/markdown', 22 | author = "lucasg", 23 | author_email = "lucas.georges@outlook.com", 24 | url = "https://github.com/lucasg/idarop", 25 | 26 | install_requires = [ 27 | ], 28 | 29 | packages = find_packages(), 30 | py_modules = ['__ida_setup__', 'plugins/idarop_plugin_t'], 31 | 32 | # Declare your ida plugins here 33 | package_data = { 34 | 'ida_plugins': ['plugins/idarop_plugin_t.py'], 35 | }, 36 | 37 | include_package_data =True, 38 | 39 | # monkey patch install script for IDA plugin custom install 40 | cmdclass={'install': IdaPluginInstallCommand} 41 | ) --------------------------------------------------------------------------------