diff --git a/fa/ida_launcher.py b/fa/ida_launcher.py new file mode 100644 index 0000000..8beb854 --- /dev/null +++ b/fa/ida_launcher.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +import os +import socket +import subprocess +from collections import namedtuple + +import IPython +import click +import rpyc +from termcolor import cprint + +IDA_PLUGIN_PATH = os.path.abspath(os.path.join((os.path.dirname(__file__), 'ida_plugin.py'))) + +TerminalProgram = namedtuple('TerminalProgram', 'executable args') + + +def is_windows(): + return os.name == 'nt' + + +SUPPORTED_TERMINALS = [ + TerminalProgram(executable='kitty', args=['bash', '-c']), + TerminalProgram(executable='gnome-terminal', args=['-x', 'bash', '-c']), + TerminalProgram(executable='xterm', args=['-e']), +] + + +def get_free_port(): + s = socket.socket() + s.bind(('', 0)) + port = s.getsockname()[1] + s.close() + return port + + +def does_program_exist(program): + return 0 == subprocess.Popen(['which', program]).wait() + + +def execute_in_new_terminal(cmd): + if is_windows(): + subprocess.Popen(cmd) + return + + for terminal in SUPPORTED_TERMINALS: + if does_program_exist(terminal.executable): + subprocess.Popen([terminal.executable] + terminal.args + [' '.join(cmd)]) + return + + +def get_client(ida, payload, loader=None, processor_type=None, accept_defaults=False, log_file_path=None): + port = get_free_port() + args = [ida] + + if processor_type is not None: + args.append('-p{}'.format(processor_type)) + + if loader is not None: + args.append('-T{}'.format(loader)) + + if log_file_path is not None: + args.append('-L{}'.format(log_file_path)) + + if accept_defaults: + args.append('-A') + + args.append('\'-S{} --service {}\''.format(IDA_PLUGIN_PATH, port)) + args.append(payload) + + execute_in_new_terminal(args) + + while True: + try: + client = rpyc.connect('localhost', port, config={ + # this is meant to disable the timeout + 'sync_request_timeout': None, + 'allow_all_attrs': True, + 'allow_setattr': True, + }) + break + except socket.error: + pass + + return client + + +def launch_ida_in_service_mode(ida, payload, loader=None): + client = get_client(ida, payload, loader) + cprint('use `client.root` variable to access the remote object', 'cyan') + IPython.embed() + client.close() + + +@click.command() +@click.argument('ida', type=click.Path(exists=True)) +@click.argument('payload', type=click.Path(exists=True)) +@click.option('-l', '--loader', required=False) +def shell(ida, payload, loader): + launch_ida_in_service_mode(ida, payload, loader) + + +if __name__ == '__main__': + shell() diff --git a/fa/ida_plugin.py b/fa/ida_plugin.py index 8cc68a9..a9b3fa7 100644 --- a/fa/ida_plugin.py +++ b/fa/ida_plugin.py @@ -8,16 +8,22 @@ import re import os +import rpyc +from rpyc import OneShotServer + sys.path.append('.') # noqa: E402 import hjson import click from ida_kernwin import Form +import ida_segment import ida_kernwin import ida_typeinf +import ida_struct import ida_bytes import idautils +import ida_auto import ida_pro import idaapi import idc @@ -258,7 +264,7 @@ def symbols(self, output_file_path=None): results.update(ida_symbols) - except Exception as e: + except Exception: traceback.print_exc() finally: ida_kernwin.hide_wait_box() @@ -271,6 +277,7 @@ def export(self): IDB. :return: None """ + class ExportForm(Form): def __init__(self): description = ''' @@ -347,7 +354,7 @@ def OnFormChange(self, fid): .format(ifdef_name=ifdef_name)) if consts_ordinal is not None: - consts = re.findall('\s*(.+?) = (.+?),', + consts = re.findall('\\s*(.+?) = (.+?),', idc.print_decls( str(consts_ordinal), 0)) for k, v in consts: @@ -365,8 +372,8 @@ def OnFormChange(self, fid): structs_buf): f.write( 'typedef {struct_type} {struct_name} {struct_name};\n' - .format(struct_type=struct_type, - struct_name=struct_name)) + .format(struct_type=struct_type, + struct_name=struct_name)) structs_buf = structs_buf.replace('__fastcall', '') f.write('\n') @@ -402,6 +409,7 @@ def interactive_settings(self): Show settings dialog :return: None """ + class SettingsForm(Form): def __init__(self, signatures_root, use_template): description = ''' @@ -455,6 +463,7 @@ def interactive_set_project(self): Show set-project dialog :return: None """ + class SetProjectForm(Form): def __init__(self, signatures_root, projects, current): description = ''' @@ -515,6 +524,7 @@ def add_action(action): :param action: action given as the `Action` namedtuple :return: None """ + class Handler(ida_kernwin.action_handler_t): def __init__(self): ida_kernwin.action_handler_t.__init__(self) @@ -661,25 +671,38 @@ def install(): @click.command() -@click.argument('signatures_root', default='.') -@click.option('--project_name', default=None) -@click.option('--symbols-file', default=None) -def main(signatures_root, project_name, symbols_file=None): - plugin_main(signatures_root, project_name, symbols_file) +@click.option('-s', '--service', type=click.IntRange(1024, 65535), help='execute in rpyc service mode at given port') +def main(service): + plugin_main(service) + + +class FaService(rpyc.Service): + ida_segment = ida_segment + ida_kernwin = ida_kernwin + ida_typeinf = ida_typeinf + ida_struct = ida_struct + ida_bytes = ida_bytes + idautils = idautils + ida_auto = ida_auto + ida_pro = ida_pro + idaapi = idaapi + idc = idc + + @staticmethod + def load_module(name, filename): + return fainterp.FaInterp.get_module(name, filename) -def plugin_main(signatures_root, project_name, symbols_file=None): +def plugin_main(service=None): global fa_instance fa_instance = IdaLoader() fa_instance.set_input('ida') - if project_name is not None: - fa_instance.set_project(project_name) - load_ui() - IdaLoader.log(''' --------------------------------- + IdaLoader.log(''' + --------------------------------- FA Loaded successfully Quick usage: @@ -689,12 +712,15 @@ def plugin_main(signatures_root, project_name, symbols_file=None): fa_instance.set_symbol_template(status) # enable/disable template temp signature fa_instance.symbols() # searches for the symbols in the current project - ---------------------------------''') + --------------------------------- + ''') - if symbols_file is not None: - fa_instance.set_signatures_root(signatures_root) - fa_instance.symbols(symbols_file) - ida_pro.qexit(0) + if service: + t = OneShotServer(FaService, port=service, protocol_config={ + 'allow_all_attrs': True, + 'allow_setattr': True, + }) + t.start() # TODO: consider adding as autostart script # install() @@ -709,7 +735,7 @@ class FAIDAPlugIn(idaapi.plugin_t): help = "Load FA in IDA Pro" def init(self): - plugin_main('.', None, None) + plugin_main() return idaapi.PLUGIN_KEEP def run(self, args): diff --git a/requirements.txt b/requirements.txt index b6bb25f..62376bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,9 @@ capstone click hjson future -configparser \ No newline at end of file +configparser +six +rpyc +click +ipython +termcolor \ No newline at end of file diff --git a/requirements_testing.txt b/requirements_testing.txt index ecdad29..eb95f01 100644 --- a/requirements_testing.txt +++ b/requirements_testing.txt @@ -7,4 +7,9 @@ configparser idalink pytest simpleelf -pyelftools \ No newline at end of file +pyelftools +six +rpyc +click +ipython +termcolor diff --git a/setup.py b/setup.py index 54dbed4..792424c 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,31 @@ +from pathlib import Path + from setuptools import setup +BASE_DIR = Path(__file__).parent.resolve(strict=True) + + +def parse_requirements(): + reqs = [] + with open(BASE_DIR / 'requirements.txt', 'r') as fd: + for line in fd.readlines(): + line = line.strip() + if line: + reqs.append(line) + return reqs + + setup( name='fa', - version='0.2.2', + version='0.3.0', description='FA Plugin', author='DoronZ', author_email='doron88@gmail.com', url='https://github.com/doronz88/fa', packages=['fa', 'fa.commands'], package_dir={'fa': 'fa'}, + package_data={'': ['*.png', '*'], }, + include_package_data=True, data_files=[(r'fa/res/icons', [r'fa/res/icons/create_sig.png', r'fa/res/icons/export.png', r'fa/res/icons/find.png', @@ -18,11 +35,6 @@ r'fa/res/icons/suitcase.png']), (r'fa/commands', ['fa/commands/alias']), ], - install_requires=['keystone-engine', - 'capstone', - 'click', - 'hjson', - 'future', - 'configparser'], - python_requires='>=2.7' + install_requires=parse_requirements(), + python_requires='>=2.7', )