├── fa ├── __init__.py ├── commands │ ├── __init__.py │ ├── trace.py │ ├── run.py │ ├── set_const.py │ ├── find.py │ ├── make_code.py │ ├── clear.py │ ├── make_function.py │ ├── make_literal.py │ ├── make_unknown.py │ ├── alias │ ├── unique.py │ ├── print.py │ ├── add.py │ ├── xref.py │ ├── set_enum.py │ ├── sort.py │ ├── verify_name.py │ ├── offset.py │ ├── align.py │ ├── stop_if_empty.py │ ├── verify_aligned.py │ ├── b.py │ ├── most_common.py │ ├── verify_single.py │ ├── load.py │ ├── single.py │ ├── set_name.py │ ├── store.py │ ├── set_struct_member.py │ ├── max_xrefs.py │ ├── min_xrefs.py │ ├── verify_str.py │ ├── symdiff.py │ ├── operand.py │ ├── add_offset_range.py │ ├── set_type.py │ ├── find_bytes.py │ ├── find_bytes_ida.py │ ├── make_comment.py │ ├── function_end.py │ ├── intersect.py │ ├── union.py │ ├── if.py │ ├── deref_data.py │ ├── python_if.py │ ├── if_not.py │ ├── argument.py │ ├── locate.py │ ├── verify_bytes.py │ ├── find_immediate.py │ ├── find_str.py │ ├── function_start.py │ ├── make_offset.py │ ├── goto_ref.py │ ├── verify_segment.py │ ├── xrefs_to.py │ ├── function_lines.py │ ├── keystone_find_opcodes.py │ ├── verify_opcode.py │ ├── keystone_verify_opcodes.py │ ├── verify_ref.py │ ├── verify_operand.py │ └── next_instruction.py ├── signatures │ ├── test-project-elf │ │ ├── alias │ │ ├── test_dep.dep │ │ ├── test_find.sig │ │ └── test-basic.sig │ └── test-project-ida │ │ ├── alias │ │ ├── test_dep.dep │ │ ├── test_find.sig │ │ ├── explore.py │ │ ├── add_structs.py │ │ ├── test-ida-context.sig │ │ └── test-basic.sig ├── res │ ├── icons │ │ ├── export.png │ │ ├── find.png │ │ ├── save.png │ │ ├── find_all.png │ │ ├── settings.png │ │ ├── suitcase.png │ │ └── create_sig.png │ └── screenshots │ │ └── menu.png ├── context.py ├── fa_types.py ├── ida_launcher.py ├── utils.py ├── fainterp.py └── ida_plugin.py ├── tests ├── __init__.py ├── utils │ ├── __init__.py │ └── mock_fa.py ├── conftest.py └── test_commands │ ├── test_elf.py │ └── test_find_bytes.py ├── _config.yml ├── requirements.txt ├── pytest.ini ├── requirements_testing.txt ├── ide-completions └── sublime │ ├── README.md │ └── sig.sublime-completions ├── scripts └── git │ ├── install.py │ └── pre-commit ├── .github └── workflows │ ├── python-publish.yml │ └── python-app.yml ├── elf_loader.py ├── .gitignore ├── pyproject.toml └── README.md /fa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fa/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /fa/signatures/test-project-elf/alias: -------------------------------------------------------------------------------- 1 | name = set-name 2 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/alias: -------------------------------------------------------------------------------- 1 | name = set-name 2 | -------------------------------------------------------------------------------- /fa/res/icons/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/export.png -------------------------------------------------------------------------------- /fa/res/icons/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/find.png -------------------------------------------------------------------------------- /fa/res/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/save.png -------------------------------------------------------------------------------- /fa/res/icons/find_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/find_all.png -------------------------------------------------------------------------------- /fa/res/icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/settings.png -------------------------------------------------------------------------------- /fa/res/icons/suitcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/suitcase.png -------------------------------------------------------------------------------- /fa/res/icons/create_sig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/icons/create_sig.png -------------------------------------------------------------------------------- /fa/res/screenshots/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doronz88/fa/HEAD/fa/res/screenshots/menu.png -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/test_dep.dep: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "instructions": [ 4 | add 67 5 | set-name test_run 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /fa/signatures/test-project-elf/test_dep.dep: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_dep", 3 | "instructions": [ 4 | add 67 5 | set-name test_run 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /fa/signatures/test-project-elf/test_find.sig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_find", 3 | "instructions": [ 4 | add 76 5 | set-name test_find 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/test_find.sig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_find", 3 | "instructions": [ 4 | add 76 5 | set-name test_find 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keystone-engine 2 | capstone 3 | click 4 | hjson 5 | future 6 | configparser 7 | six 8 | rpyc 9 | click 10 | ipython 11 | termcolor -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:Using or importing the ABCs from 'collections' instead of from 'collections\.abc' is deprecated:DeprecationWarning -------------------------------------------------------------------------------- /requirements_testing.txt: -------------------------------------------------------------------------------- 1 | keystone-engine 2 | capstone 3 | click 4 | hjson 5 | future 6 | configparser 7 | pytest 8 | simpleelf 9 | pyelftools 10 | six 11 | rpyc 12 | click 13 | ipython 14 | termcolor 15 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/explore.py: -------------------------------------------------------------------------------- 1 | TEMPLATE = ''' 2 | arm-find-all 'push {r4, r5, r6, r7, lr}' 3 | make-function 4 | ''' 5 | 6 | 7 | def run(interpreter): 8 | interpreter.find_from_instructions_list(TEMPLATE.splitlines()) 9 | -------------------------------------------------------------------------------- /ide-completions/sublime/README.md: -------------------------------------------------------------------------------- 1 | # Sublime completions 2 | 3 | 4 | To install in sublime: 5 | 6 | - Install hJson schema. 7 | - Goto `Preferences -> Browse Packages...` and place 8 | [sig.sublime-completions](sig.sublime-completions) somewhere inside. 9 | - All done :) 10 | -------------------------------------------------------------------------------- /scripts/git/install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | import shutil 4 | 5 | shutil.copyfile(os.path.join(os.path.dirname(__file__), 6 | 'pre-commit'), 7 | os.path.join( 8 | os.path.dirname( 9 | os.path.dirname(os.path.dirname(__file__))), 10 | '.git', 'hooks', 'pre-commit')) 11 | -------------------------------------------------------------------------------- /fa/commands/trace.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | 3 | from fa import utils 4 | 5 | 6 | def get_parser(): 7 | p = utils.ArgumentParserNoExit('trace', 8 | description='sets a pdb breakpoint') 9 | return p 10 | 11 | 12 | def trace(addresses): 13 | pdb.set_trace() 14 | return addresses 15 | 16 | 17 | def run(segments, args, addresses, interpreter=None, **kwargs): 18 | return trace(addresses) 19 | -------------------------------------------------------------------------------- /fa/commands/run.py: -------------------------------------------------------------------------------- 1 | from fa import utils 2 | 3 | 4 | def get_parser(): 5 | p = utils.ArgumentParserNoExit('run', 6 | description='run another SIG file') 7 | p.add_argument('name', help='SIG filename') 8 | return p 9 | 10 | 11 | def run(segments, args, addresses, interpreter=None, **kwargs): 12 | interpreter.find_from_sig_path(args.name) 13 | 14 | # return an empty result-set 15 | return [] 16 | -------------------------------------------------------------------------------- /fa/commands/set_const.py: -------------------------------------------------------------------------------- 1 | from fa import utils 2 | 3 | 4 | def get_parser(): 5 | p = utils.ArgumentParserNoExit('set-const', 6 | description='define a const value') 7 | p.add_argument('name') 8 | return p 9 | 10 | 11 | def set_const(addresses, name, interpreter): 12 | for ea in addresses: 13 | interpreter.set_const(name, ea) 14 | return addresses 15 | 16 | 17 | def run(segments, args, addresses, interpreter=None, **kwargs): 18 | return set_const(addresses, args.name, interpreter) 19 | -------------------------------------------------------------------------------- /tests/utils/mock_fa.py: -------------------------------------------------------------------------------- 1 | from fa import fainterp 2 | 3 | 4 | class MockFaInterp(fainterp.FaInterp): 5 | def reload_segments(self): 6 | pass 7 | 8 | def set_input(self, input_): 9 | pass 10 | 11 | @property 12 | def segments(self): 13 | return self._segments 14 | 15 | @segments.setter 16 | def segments(self, value): 17 | """ 18 | Set the current segments 19 | :param value: Ordered mapping between start addresses and their data 20 | """ 21 | self._segments = value 22 | -------------------------------------------------------------------------------- /fa/commands/find.py: -------------------------------------------------------------------------------- 1 | from fa import utils 2 | 3 | 4 | def get_parser(): 5 | p = utils.ArgumentParserNoExit('find', 6 | description='find another symbol defined ' 7 | 'in other SIG files') 8 | p.add_argument('name', help='symbol name') 9 | return p 10 | 11 | 12 | def run(segments, args, addresses, interpreter=None, **kwargs): 13 | interpreter.find(args.name, use_cache=interpreter.implicit_use_sig_cache) 14 | 15 | # return an empty result-set 16 | return [] 17 | -------------------------------------------------------------------------------- /fa/commands/make_code.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import idc 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('make-code', 11 | description='convert into a code block') 12 | return p 13 | 14 | 15 | @context.ida_context 16 | def make_code(addresses): 17 | for ea in addresses: 18 | idc.create_insn(ea) 19 | return addresses 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return make_code(addresses) 24 | -------------------------------------------------------------------------------- /fa/commands/clear.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''clears the current result-set 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8] 9 | -> clear 10 | results = [] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('clear', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | return p 19 | 20 | 21 | def run(segments, args, addresses, interpreter=None, **kwargs): 22 | return [] 23 | -------------------------------------------------------------------------------- /fa/commands/make_function.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import ida_funcs 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('make-function', 11 | description='convert into a function') 12 | return p 13 | 14 | 15 | @context.ida_context 16 | def make_function(addresses): 17 | for ea in addresses: 18 | ida_funcs.add_func(ea) 19 | return addresses 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return make_function(addresses) 24 | -------------------------------------------------------------------------------- /fa/commands/make_literal.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import idc 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('make-literal', 11 | description='convert into a literal') 12 | return p 13 | 14 | 15 | @context.ida_context 16 | def make_literal(addresses): 17 | for ea in addresses: 18 | idc.create_strlit(ea, idc.BADADDR) 19 | return addresses 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return make_literal(addresses) 24 | -------------------------------------------------------------------------------- /fa/commands/make_unknown.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import ida_bytes 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('make-unknown', 11 | description='convert into an unknown block') 12 | return p 13 | 14 | 15 | @context.ida_context 16 | def make_unknown(addresses): 17 | for ea in addresses: 18 | ida_bytes.del_items(ea) 19 | return addresses 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return make_unknown(addresses) 24 | -------------------------------------------------------------------------------- /fa/commands/alias: -------------------------------------------------------------------------------- 1 | ppc32-big-find-all = keystone-find-opcodes KS_ARCH_PPC KS_MODE_BIG_ENDIAN|KS_MODE_PPC32 2 | ppc32-find-all = keystone-find-opcodes --bele KS_ARCH_PPC KS_MODE_PPC32 3 | ppc32-big-verify = keystone-verify-opcodes KS_ARCH_PPC KS_MODE_BIG_ENDIAN|KS_MODE_PPC32 4 | ppc32-verify = keystone-verify-opcodes --bele KS_ARCH_PPC KS_MODE_PPC32 5 | arm-find-all = keystone-find-opcodes --bele KS_ARCH_ARM KS_MODE_ARM 6 | thumb-find-all = keystone-find-opcodes --bele KS_ARCH_ARM KS_MODE_THUMB 7 | arm-verify = keystone-verify-opcodes --bele KS_ARCH_ARM KS_MODE_ARM 8 | find-imm = find-immediate 9 | save = store 10 | -------------------------------------------------------------------------------- /fa/commands/unique.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''make the resultset unique 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8, 8, 12] 9 | -> unique 10 | result = [0, 4, 8, 12] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('unique', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | return p 19 | 20 | 21 | def run(segments, args, addresses, interpreter=None, **kwargs): 22 | return list(set(addresses)) 23 | -------------------------------------------------------------------------------- /fa/commands/print.py: -------------------------------------------------------------------------------- 1 | from fa import utils 2 | 3 | DESCRIPTION = '''prints the current result-set (for debugging)''' 4 | 5 | 6 | def get_parser(): 7 | p = utils.ArgumentParserNoExit('print', 8 | description=DESCRIPTION) 9 | p.add_argument('phrase', nargs='?', default='', help='optional string') 10 | return p 11 | 12 | 13 | def run(segments, args, addresses, interpreter=None, **kwargs): 14 | log_line = 'FA Debug Print: {}\n'.format(args.phrase) 15 | for ea in addresses: 16 | log_line += '\t0x{:x}\n'.format(ea) 17 | print(log_line) 18 | return addresses 19 | -------------------------------------------------------------------------------- /fa/commands/add.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''add an hard-coded value into resultset 6 | 7 | EXAMPLE: 8 | results = [] 9 | -> add 80 10 | result = [80] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('add', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('value') 19 | return p 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return addresses + [eval(args.value)] 24 | -------------------------------------------------------------------------------- /fa/commands/xref.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import idautils 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('xref', 11 | description='goto xrefs pointing at' 12 | ' current search results') 13 | return p 14 | 15 | 16 | @context.ida_context 17 | def xref(addresses): 18 | for address in addresses: 19 | for ref in idautils.XrefsTo(address): 20 | yield ref.frm 21 | 22 | 23 | def run(segments, args, addresses, interpreter=None, **kwargs): 24 | return list(xref(addresses)) 25 | -------------------------------------------------------------------------------- /fa/commands/set_enum.py: -------------------------------------------------------------------------------- 1 | from fa import fa_types, utils 2 | 3 | 4 | def get_parser(): 5 | p = utils.ArgumentParserNoExit('set-enum', 6 | description='define an enum value') 7 | p.add_argument('enum_name') 8 | p.add_argument('enum_key') 9 | return p 10 | 11 | 12 | def set_enum(addresses, enum_name, enum_key): 13 | for ea in addresses: 14 | enum = fa_types.FaEnum(enum_name) 15 | enum.add_value(enum_key, ea) 16 | enum.update_idb() 17 | return addresses 18 | 19 | 20 | def run(segments, args, addresses, interpreter=None, **kwargs): 21 | return set_enum(addresses, args.enum_name, args.enum_key) 22 | -------------------------------------------------------------------------------- /fa/commands/sort.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''performs a sort on the current result-set 6 | 7 | EXAMPLE: 8 | results = [4, 12, 0, 8] 9 | -> sort 10 | result = [0, 4, 8 ,12] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('sort', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | return p 19 | 20 | 21 | def sort(addresses): 22 | addresses.sort() 23 | return addresses 24 | 25 | 26 | def run(segments, args, addresses, interpreter=None, **kwargs): 27 | return sort(addresses) 28 | -------------------------------------------------------------------------------- /fa/commands/verify_name.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | from fa.commands.locate import locate 3 | 4 | 5 | def get_parser(): 6 | p = utils.ArgumentParserNoExit('verify-name', 7 | description='verifies the given name ' 8 | 'appears in result set') 9 | p.add_argument('name') 10 | return p 11 | 12 | 13 | @context.ida_context 14 | @utils.yield_unique 15 | def verify_name(addresses, name): 16 | ref = locate(name) 17 | for address in addresses: 18 | if ref == address: 19 | yield address 20 | 21 | 22 | def run(segments, args, addresses, interpreter=None, **kwargs): 23 | return list(verify_name(addresses, args.name)) 24 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/add_structs.py: -------------------------------------------------------------------------------- 1 | from fa import fa_types 2 | 3 | 4 | def run(interpreter): 5 | fa_types.add_const('CONST7', 7) 6 | fa_types.add_const('CONST8', 8) 7 | 8 | foo_e = fa_types.FaEnum('foo_e') 9 | foo_e.add_value('val2', 2) 10 | foo_e.add_value('val1', 1) 11 | foo_e.update_idb() 12 | 13 | special_struct_t = fa_types.FaStruct('special_struct_t') 14 | special_struct_t.add_field('member1', 'const char *') 15 | special_struct_t.add_field('member2', 'const char *', offset=0x20) 16 | special_struct_t.add_field('member3', 'char', offset=0x60) 17 | special_struct_t.add_field('member4', 'char', offset=0x61) 18 | special_struct_t.add_field('member5', 'const char *', offset=0x80) 19 | special_struct_t.update_idb() 20 | -------------------------------------------------------------------------------- /fa/commands/offset.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''advance the result-set by a given offset 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8, 12] 9 | -> offset 4 10 | result = [4, 8, 12, 16] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('offset', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('offset') 19 | return p 20 | 21 | 22 | def offset(addresses, advance_by): 23 | for ea in addresses: 24 | yield ea + advance_by 25 | 26 | 27 | def run(segments, args, addresses, interpreter=None, **kwargs): 28 | return list(offset(addresses, eval(args.offset))) 29 | -------------------------------------------------------------------------------- /fa/commands/align.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''align results to given base (round-up) 6 | 7 | EXAMPLE: 8 | results = [0, 2, 4, 6, 8] 9 | -> align 4 10 | results = [0, 4, 4, 8, 8] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('align', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('value') 19 | return p 20 | 21 | 22 | def align(addresses, value): 23 | return [((ea + (value - 1)) // value) * value for ea in addresses] 24 | 25 | 26 | def run(segments, args, addresses, interpreter=None, **kwargs): 27 | return list(align(addresses, eval(args.value))) 28 | -------------------------------------------------------------------------------- /fa/commands/stop_if_empty.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''exit if current resultset is empty 6 | 7 | EXAMPLE: 8 | results = [] 9 | 10 | -> stop-if-empty 11 | add 1 12 | 13 | results = [] 14 | ''' 15 | 16 | 17 | def get_parser(): 18 | p = utils.ArgumentParserNoExit('stop-if-empty', 19 | description=DESCRIPTION, 20 | formatter_class=RawTextHelpFormatter) 21 | return p 22 | 23 | 24 | def run(segments, args, addresses, interpreter=None, **kwargs): 25 | if len(addresses) == 0: 26 | # just a big enough value which is always greater 27 | # then max available pc 28 | interpreter.set_pc(0xffffffff) 29 | return addresses 30 | -------------------------------------------------------------------------------- /fa/commands/verify_aligned.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''leave only results fitting required alignment 6 | 7 | EXAMPLE: 8 | results = [0, 2, 4, 6, 8] 9 | -> verify-aligned 4 10 | results = [0, 4, 8] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('verify-aligned', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('value', type=int) 19 | return p 20 | 21 | 22 | def aligned(addresses, value): 23 | return [ea for ea in addresses if ea % value == 0] 24 | 25 | 26 | def run(segments, args, addresses, interpreter=None, **kwargs): 27 | return list(aligned(addresses, args.value)) 28 | -------------------------------------------------------------------------------- /fa/commands/b.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''branch unconditionally to label 6 | 7 | EXAMPLE: 8 | results = [] 9 | 10 | add 1 11 | -> b skip 12 | add 2 13 | label skip 14 | add 3 15 | 16 | results = [1, 3] 17 | ''' 18 | 19 | 20 | def get_parser(): 21 | p = utils.ArgumentParserNoExit('b', 22 | description=DESCRIPTION, 23 | formatter_class=RawTextHelpFormatter) 24 | p.add_argument('label', help='label to jump to') 25 | return p 26 | 27 | 28 | def run(segments, args, addresses, interpreter=None, **kwargs): 29 | interpreter.set_pc(args.label) 30 | # pc is incremented by 1, after each instruction 31 | interpreter.dec_pc() 32 | return addresses 33 | -------------------------------------------------------------------------------- /fa/commands/most_common.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''get the result appearing the most in the result-set 6 | 7 | EXAMPLE: 8 | results = [0, 4, 4, 8, 12] 9 | -> most-common 10 | result = [4] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('most-common', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | return p 19 | 20 | 21 | def most_common(addresses): 22 | addresses = list(addresses) 23 | if len(addresses) == 0: 24 | return [] 25 | return [max(set(addresses), key=addresses.count)] 26 | 27 | 28 | def run(segments, args, addresses, interpreter=None, **kwargs): 29 | return most_common(addresses) 30 | -------------------------------------------------------------------------------- /fa/commands/verify_single.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''verifies the result-list contains a single value 6 | 7 | EXAMPLE #1: 8 | results = [4, 12, 0, 8] 9 | -> verify-single 10 | result = [] 11 | 12 | EXAMPLE #2: 13 | results = [4] 14 | -> verify-single 15 | result = [4] 16 | ''' 17 | 18 | 19 | def get_parser(): 20 | p = utils.ArgumentParserNoExit('verify-single', 21 | description=DESCRIPTION, 22 | formatter_class=RawTextHelpFormatter) 23 | return p 24 | 25 | 26 | def verify_single(addresses): 27 | return addresses if len(addresses) == 1 else [] 28 | 29 | 30 | def run(segments, args, addresses, interpreter=None, **kwargs): 31 | return verify_single(addresses) 32 | -------------------------------------------------------------------------------- /fa/commands/load.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''go back to previous result-set saved by 'store' command. 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8] 9 | store foo 10 | 11 | find-bytes 12345678 12 | results = [0, 4, 8, 10, 20] 13 | 14 | -> load foo 15 | results = [0, 4, 8] 16 | ''' 17 | 18 | 19 | def get_parser(): 20 | p = utils.ArgumentParserNoExit('load', 21 | description=DESCRIPTION, 22 | formatter_class=RawTextHelpFormatter) 23 | p.add_argument('name', help='name of variable in history to go back ' 24 | 'to') 25 | return p 26 | 27 | 28 | def run(segments, args, addresses, interpreter=None, **kwargs): 29 | return interpreter.get_variable(args.name) 30 | -------------------------------------------------------------------------------- /fa/commands/single.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''peek a single result from the result-set (zero-based) 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8, 12] 9 | -> single 2 10 | result = [8] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('single', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('index', default='0', help='result index') 19 | return p 20 | 21 | 22 | def single(addresses, index): 23 | if index + 1 > len(addresses): 24 | return [] 25 | else: 26 | return [addresses[index]] 27 | 28 | 29 | def run(segments, args, addresses, interpreter=None, **kwargs): 30 | return single(addresses, eval(args.index)) 31 | -------------------------------------------------------------------------------- /fa/commands/set_name.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fa.utils import ArgumentParserNoExit 4 | 5 | try: 6 | import ida_bytes 7 | except ImportError: 8 | pass 9 | 10 | 11 | def get_parser() -> ArgumentParserNoExit: 12 | p = ArgumentParserNoExit('set-name', 13 | description='set symbol name') 14 | p.add_argument('name') 15 | return p 16 | 17 | 18 | def is_address_nameless(addr: int) -> bool: 19 | return not ida_bytes.f_has_user_name(ida_bytes.get_flags(addr), None) 20 | 21 | 22 | def set_name(addresses: List[int], name: str, interpreter) -> List[int]: 23 | for ea in addresses: 24 | interpreter.set_symbol(name, ea) 25 | return addresses 26 | 27 | 28 | def run(segments, args, addresses: List[int], interpreter=None, **kwargs) -> List[int]: 29 | return set_name(addresses, args.name, interpreter) 30 | -------------------------------------------------------------------------------- /fa/commands/store.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''save current result-set in a variable. 6 | You can later load the result-set using 'load' 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | -> store foo 11 | 12 | find-bytes --or 12345678 13 | results = [0, 4, 8, 10, 20] 14 | 15 | load foo 16 | results = [0, 4, 8] 17 | ''' 18 | 19 | 20 | def get_parser(): 21 | p = utils.ArgumentParserNoExit('store', 22 | description=DESCRIPTION, 23 | formatter_class=RawTextHelpFormatter) 24 | p.add_argument('name', help='name of variable to use') 25 | return p 26 | 27 | 28 | def run(segments, args, addresses, interpreter=None, **kwargs): 29 | interpreter.set_variable(args.name, addresses[:]) 30 | return addresses 31 | -------------------------------------------------------------------------------- /fa/commands/set_struct_member.py: -------------------------------------------------------------------------------- 1 | from fa import fa_types, utils 2 | 3 | 4 | def get_parser(): 5 | p = utils.ArgumentParserNoExit('set-struct-member', 6 | description='add a struct member') 7 | p.add_argument('struct_name') 8 | p.add_argument('member_name') 9 | p.add_argument('member_type') 10 | return p 11 | 12 | 13 | def set_struct_member(addresses, struct_name, member_name, member_type): 14 | for ea in addresses: 15 | enum = fa_types.FaStruct(struct_name) 16 | enum.add_field(member_name, member_type, offset=ea) 17 | enum.update_idb(delete_existing_members=False) 18 | return addresses 19 | 20 | 21 | def run(segments, args, addresses, interpreter=None, **kwargs): 22 | return set_struct_member(addresses, args.struct_name, args.member_name, 23 | args.member_type) 24 | -------------------------------------------------------------------------------- /fa/commands/max_xrefs.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import idautils 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('max-xrefs', 11 | description='get the result with' 12 | ' most xrefs pointing ' 13 | 'at it') 14 | return p 15 | 16 | 17 | @context.ida_context 18 | def max_xrefs(addresses): 19 | xrefs = [] 20 | for address in addresses: 21 | xrefs.append((address, len([ref.frm for ref in 22 | idautils.XrefsTo(address)]))) 23 | 24 | if len(xrefs) > 0: 25 | address, _ = max(xrefs, key=lambda x: x[1]) 26 | return [address] 27 | 28 | return [] 29 | 30 | 31 | def run(segments, args, addresses, interpreter=None, **kwargs): 32 | return max_xrefs(addresses) 33 | -------------------------------------------------------------------------------- /fa/commands/min_xrefs.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import idautils 5 | except ImportError: 6 | pass 7 | 8 | 9 | def get_parser(): 10 | p = utils.ArgumentParserNoExit('min-xrefs', 11 | description='get the result with' 12 | ' least xrefs pointing ' 13 | 'at it') 14 | return p 15 | 16 | 17 | @context.ida_context 18 | def min_xrefs(addresses): 19 | xrefs = [] 20 | for address in addresses: 21 | xrefs.append((address, len([ref.frm for ref in 22 | idautils.XrefsTo(address)]))) 23 | 24 | if len(xrefs) > 0: 25 | address, _ = min(xrefs, key=lambda x: x[1]) 26 | return [address] 27 | 28 | return [] 29 | 30 | 31 | def run(segments, args, addresses, interpreter=None, **kwargs): 32 | return min_xrefs(addresses) 33 | -------------------------------------------------------------------------------- /fa/commands/verify_str.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from argparse import RawTextHelpFormatter 3 | 4 | from fa.commands import verify_bytes 5 | 6 | DESCRIPTION = '''reduce the result-set to those matching the given string 7 | 8 | EXAMPLE: 9 | 0x00000000: 01 02 03 04 10 | 0x00000004: 30 31 32 33 -> ascii '0123' 11 | 12 | results = [0, 2, 4] 13 | -> verify-str '0123' 14 | results = [4] 15 | ''' 16 | 17 | 18 | def get_parser(): 19 | p = verify_bytes.get_parser() 20 | p.add_argument('--null-terminated', action='store_true') 21 | 22 | p.prog = 'verify-str' 23 | p.description = DESCRIPTION 24 | p.formatter_class = RawTextHelpFormatter 25 | return p 26 | 27 | 28 | def run(segments, args, addresses, interpreter=None, **kwargs): 29 | hex_str = binascii.hexlify(args.hex_str.encode()).decode() 30 | hex_str += '00' if args.null_terminated else '' 31 | 32 | setattr(args, 'hex_str', hex_str) 33 | return verify_bytes.run(segments, args, addresses, **kwargs) 34 | -------------------------------------------------------------------------------- /fa/commands/symdiff.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''symmetric difference between two or more variables 6 | 7 | EXAMPLE: 8 | results = [0, 4, 8] 9 | store a 10 | ... 11 | results = [0, 12, 20] 12 | store b 13 | 14 | -> symdiff a b 15 | results = [4, 8, 12, 20] 16 | ''' 17 | 18 | 19 | def get_parser(): 20 | p = utils.ArgumentParserNoExit('symdiff', 21 | description=DESCRIPTION, 22 | formatter_class=RawTextHelpFormatter) 23 | p.add_argument('variables', nargs='+', help='variable names') 24 | return p 25 | 26 | 27 | def run(segments, args, addresses, interpreter=None, **kwargs): 28 | first_var = args.variables[0] 29 | results = set(interpreter.get_variable(first_var)) 30 | 31 | for c in args.variables[1:]: 32 | results.symmetric_difference_update(interpreter.get_variable(c)) 33 | 34 | return list(results) 35 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine build 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python -m build 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /fa/commands/operand.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''get operand value from given instruction 11 | 12 | EXAMPLE #1: 13 | 0x00000000: mov r0, r1 14 | 0x00000004: mov r1, r2 15 | 0x00000008: push {r4} 16 | 17 | results = [4] 18 | -> operand 1 19 | results = [2] # because r2 20 | ''' 21 | 22 | 23 | def get_parser(): 24 | p = utils.ArgumentParserNoExit('operand', 25 | description=DESCRIPTION, 26 | formatter_class=RawTextHelpFormatter) 27 | p.add_argument('op', help='operand number') 28 | return p 29 | 30 | 31 | @context.ida_context 32 | def operand(addresses, op): 33 | for address in addresses: 34 | yield idc.get_operand_value(address, op) 35 | 36 | 37 | def run(segments, args, addresses, interpreter=None, **kwargs): 38 | return list(operand(addresses, eval(args.op))) 39 | -------------------------------------------------------------------------------- /fa/commands/add_offset_range.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''adds a python-range to resultset 6 | 7 | EXAMPLE: 8 | result = [0, 0x200] 9 | -> add-offset-range 0 4 8 10 | result = [0, 4, 8, 0x200, 0x204, 0x208] 11 | ''' 12 | 13 | 14 | def get_parser(): 15 | p = utils.ArgumentParserNoExit('add-offset-range', 16 | description=DESCRIPTION, 17 | formatter_class=RawTextHelpFormatter) 18 | p.add_argument('start') 19 | p.add_argument('end') 20 | p.add_argument('step') 21 | return p 22 | 23 | 24 | def add_offset_range(addresses, start, end, step): 25 | for ea in addresses: 26 | for i in range(start, end, step): 27 | yield ea + i 28 | 29 | 30 | def run(segments, args, addresses, interpreter=None, **kwargs): 31 | gen = add_offset_range(addresses, eval(args.start), eval(args.end), 32 | eval(args.step)) 33 | return list(gen) 34 | -------------------------------------------------------------------------------- /fa/commands/set_type.py: -------------------------------------------------------------------------------- 1 | from fa import context, fa_types, utils 2 | 3 | try: 4 | import ida_auto 5 | import idc 6 | except ImportError: 7 | pass 8 | 9 | 10 | def get_parser(): 11 | p = utils.ArgumentParserNoExit('set-type', 12 | description='sets the type in ' 13 | 'the disassembler') 14 | p.add_argument('type_str') 15 | return p 16 | 17 | 18 | def set_type_single(address, type_): 19 | if isinstance(type_, fa_types.FaStruct) or \ 20 | isinstance(type_, fa_types.FaEnum): 21 | type_str = type_.get_name() 22 | else: 23 | type_str = type_ 24 | 25 | idc.SetType(address, type_str) 26 | ida_auto.auto_wait() 27 | 28 | 29 | @context.ida_context 30 | def set_type(addresses, type_): 31 | for ea in addresses: 32 | set_type_single(ea, type_) 33 | return addresses 34 | 35 | 36 | def run(segments, args, addresses, interpreter=None, **kwargs): 37 | return set_type(addresses, args.type_str) 38 | -------------------------------------------------------------------------------- /fa/commands/find_bytes.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from argparse import RawTextHelpFormatter 3 | 4 | from fa import utils 5 | 6 | DESCRIPTION = '''expands the result-set with the occurrences of the given bytes 7 | 8 | EXAMPLE: 9 | 0x00000000: 01 02 03 04 10 | 0x00000004: 05 06 07 08 11 | 12 | results = [] 13 | -> find-bytes 01020304 14 | result = [0] 15 | 16 | -> find-bytes 05060708 17 | results = [0, 4] 18 | ''' 19 | 20 | 21 | def get_parser(): 22 | p = utils.ArgumentParserNoExit('find-bytes', 23 | description=DESCRIPTION, 24 | formatter_class=RawTextHelpFormatter) 25 | p.add_argument('hex_str') 26 | return p 27 | 28 | 29 | def find_bytes(hex_str, segments=None): 30 | needle = binascii.unhexlify(''.join(hex_str.split(' '))) 31 | return utils.find_raw(needle, segments=segments) 32 | 33 | 34 | def run(segments, args, addresses, interpreter=None, **kwargs): 35 | results = list(find_bytes(args.hex_str, segments=segments)) 36 | return addresses + results 37 | -------------------------------------------------------------------------------- /fa/commands/find_bytes_ida.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | DESCRIPTION = '''expands the result-set with the occurrences of the given bytes 6 | expression in "ida bytes syntax" 7 | 8 | EXAMPLE: 9 | 0x00000000: 01 02 03 04 10 | 0x00000004: 05 06 07 08 11 | 12 | results = [] 13 | -> find-bytes-ida '01 02 03 04' 14 | result = [0] 15 | 16 | -> find-bytes-ida '05 06 ?? 08' 17 | results = [0, 4] 18 | ''' 19 | 20 | 21 | def get_parser(): 22 | p = utils.ArgumentParserNoExit('find-bytes-ida', 23 | description=DESCRIPTION, 24 | formatter_class=RawTextHelpFormatter) 25 | p.add_argument('expression') 26 | return p 27 | 28 | 29 | @context.ida_context 30 | def find_bytes_ida(expression, segments=None): 31 | for address in utils.ida_find_all(expression): 32 | yield address 33 | 34 | 35 | def run(segments, args, addresses, interpreter=None, **kwargs): 36 | return addresses + list(find_bytes_ida(args.expression)) 37 | -------------------------------------------------------------------------------- /fa/commands/make_comment.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | try: 4 | import idc 5 | except ImportError: 6 | pass 7 | 8 | from fa import context, utils 9 | 10 | DESCRIPTION = '''add comment for given addresses 11 | 12 | EXAMPLE: 13 | 0x00000200: 01 02 03 04 14 | 0x00000204: 30 31 32 33 15 | 16 | results = [0x200] 17 | -> make-comment 'bla bla' 18 | results = [0x200] 19 | 20 | 0x00000200: 01 02 03 04 ; bla bla 21 | 0x00000204: 30 31 32 33 22 | ''' 23 | 24 | 25 | @context.ida_context 26 | def make_comment(addresses, comment): 27 | for ea in addresses: 28 | idc.set_cmt(ea, comment, 0) 29 | yield ea 30 | 31 | 32 | def get_parser(): 33 | p = utils.ArgumentParserNoExit() 34 | p.add_argument('comment', help='comment string') 35 | 36 | p.prog = 'make-comment' 37 | p.description = DESCRIPTION 38 | p.formatter_class = RawTextHelpFormatter 39 | return p 40 | 41 | 42 | def run(segments, args, addresses, interpreter=None, **kwargs): 43 | return list(make_comment(addresses, args.comment)) 44 | -------------------------------------------------------------------------------- /fa/commands/function_end.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''goto function's end 11 | 12 | EXAMPLE: 13 | 0x00000000: push {r4-r7, lr} -> function's prolog 14 | ... 15 | 0x000000f0: pop {r4-r7, pc} -> function's epilog 16 | 17 | results = [0] 18 | -> function-end 19 | result = [0xf0] 20 | ''' 21 | 22 | 23 | def get_parser(): 24 | p = utils.ArgumentParserNoExit('function-end', 25 | description=DESCRIPTION, 26 | formatter_class=RawTextHelpFormatter) 27 | return p 28 | 29 | 30 | @context.ida_context 31 | def function_end(addresses): 32 | for ea in addresses: 33 | if ea != idc.BADADDR: 34 | func_end = idc.get_func_attr(ea, idc.FUNCATTR_END) 35 | if func_end != idc.BADADDR: 36 | yield func_end 37 | 38 | 39 | def run(segments, args, addresses, interpreter=None, **kwargs): 40 | return list(function_end(addresses)) 41 | -------------------------------------------------------------------------------- /fa/commands/intersect.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | from typing import List 3 | 4 | from fa import utils 5 | 6 | DESCRIPTION = '''intersect two or more variables 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | store a 11 | ... 12 | results = [0, 12, 20] 13 | store b 14 | 15 | -> intersect a b 16 | results = [0] 17 | ''' 18 | 19 | 20 | def get_parser(): 21 | p = utils.ArgumentParserNoExit('intersect', 22 | description=DESCRIPTION, 23 | formatter_class=RawTextHelpFormatter) 24 | p.add_argument('variables', nargs='+', help='variable names') 25 | p.add_argument('--piped', '-p', action='store_true') 26 | return p 27 | 28 | 29 | def run(segments, args, addresses: List[int], interpreter=None, **kwargs): 30 | if args.piped: 31 | first_var = addresses 32 | else: 33 | first_var = interpreter.get_variable(args.variables.pop(0)) 34 | 35 | results = set(first_var) 36 | 37 | for c in args.variables: 38 | results.intersection_update(interpreter.get_variable(c)) 39 | 40 | return list(results) 41 | -------------------------------------------------------------------------------- /fa/commands/union.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | from typing import List 3 | 4 | from fa import utils 5 | 6 | DESCRIPTION = '''union two or more variables 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | store a 11 | ... 12 | results = [0, 12, 20] 13 | store b 14 | 15 | -> union a b 16 | results = [0, 4, 8, 12, 20] 17 | ''' 18 | 19 | 20 | def get_parser() -> utils.ArgumentParserNoExit: 21 | p = utils.ArgumentParserNoExit('union', 22 | description=DESCRIPTION, 23 | formatter_class=RawTextHelpFormatter) 24 | p.add_argument('variables', nargs='+', help='variable names') 25 | p.add_argument('--piped', '-p', action='store_true') 26 | return p 27 | 28 | 29 | def run(segments, args, addresses: List[int], interpreter=None, **kwargs) -> List[int]: 30 | if args.piped: 31 | first_var = addresses 32 | else: 33 | first_var = interpreter.get_variable(args.variables.pop(0)) 34 | 35 | results = set(first_var) 36 | 37 | for c in args.variables: 38 | results.update(interpreter.get_variable(c)) 39 | 40 | return list(results) 41 | -------------------------------------------------------------------------------- /fa/commands/if.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''perform an 'if' statement to create conditional branches 6 | using an FA command 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | 11 | -> if 'verify-single' a_is_single_label 12 | 13 | set-name a_isnt_single 14 | b end 15 | 16 | label a_is_single_label 17 | set-name a_is_single 18 | 19 | label end 20 | ''' 21 | 22 | 23 | def get_parser(): 24 | p = utils.ArgumentParserNoExit('if', 25 | description=DESCRIPTION, 26 | formatter_class=RawTextHelpFormatter) 27 | p.add_argument('cond', help='condition as an FA command') 28 | p.add_argument('label', help='label to jump to if condition is true') 29 | return p 30 | 31 | 32 | def run(segments, args, addresses, interpreter=None, **kwargs): 33 | if len(interpreter.find_from_instructions_list([args.cond], 34 | addresses=addresses[:])): 35 | interpreter.set_pc(args.label) 36 | 37 | # pc is incremented by 1, after each instruction 38 | interpreter.dec_pc() 39 | return addresses 40 | -------------------------------------------------------------------------------- /fa/commands/deref_data.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''Dereference pointer as integer data type. 11 | 12 | Note that the data is assumed to be stored in little endian format. 13 | 14 | Example #1: 15 | 0x00000000: LDR R1, [SP, #0x34] 16 | 17 | results = [0] 18 | -> deref-data -l 4 19 | results = [0xe5d1034] 20 | 21 | Example #2: 22 | 0x00000000: LDR R1, [SP, #0x34] 23 | 24 | results = [0] 25 | -> deref-data -l 2 26 | results = [0x1034] 27 | ''' 28 | 29 | 30 | def get_parser(): 31 | p = utils.ArgumentParserNoExit('deref-data', 32 | description=DESCRIPTION) 33 | p.add_argument('-l', '--len', type=int, required=True, 34 | help='length of the data in bytes') 35 | return p 36 | 37 | 38 | @context.ida_context 39 | def deref_data(addresses: List[int], data_len: int) -> List[int]: 40 | return [int.from_bytes(idc.get_bytes(ea, data_len), 'little') for ea in addresses] 41 | 42 | 43 | def run(segments, args, addresses, interpreter=None, **kwargs): 44 | return deref_data(addresses, args.len) 45 | -------------------------------------------------------------------------------- /fa/commands/python_if.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''perform an 'if' statement to create conditional branches 6 | using an eval'ed expression 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | 11 | verify-single 12 | store a 13 | 14 | # jump to a_is_single_label since a == [] 15 | -> python-if a a_is_single_label 16 | set-name a_isnt_single 17 | b end 18 | 19 | label a_is_single_label 20 | set-name a_is_single 21 | 22 | label end 23 | ''' 24 | 25 | 26 | def get_parser(): 27 | p = utils.ArgumentParserNoExit('python-if', 28 | description=DESCRIPTION, 29 | formatter_class=RawTextHelpFormatter) 30 | p.add_argument('cond', help='condition to evaluate (being eval\'ed)') 31 | p.add_argument('label', help='label to jump to if condition is true') 32 | return p 33 | 34 | 35 | def run(segments, args, addresses, interpreter=None, **kwargs): 36 | if eval(args.cond, interpreter.get_all_variables()): 37 | interpreter.set_pc(args.label) 38 | # pc is incremented by 1, after each instruction 39 | interpreter.dec_pc() 40 | return addresses 41 | -------------------------------------------------------------------------------- /fa/commands/if_not.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import utils 4 | 5 | DESCRIPTION = '''perform an 'if not' statement to create conditional branches 6 | using an FA command 7 | 8 | EXAMPLE: 9 | results = [0, 4, 8] 10 | 11 | -> if-not 'verify-single' a_is_single_label 12 | 13 | set-name a_is_single 14 | b end 15 | 16 | label a_is_not_single_label 17 | set-name a_is_not_single 18 | 19 | label end 20 | ''' 21 | 22 | 23 | def get_parser(): 24 | p = utils.ArgumentParserNoExit('if-not', 25 | description=DESCRIPTION, 26 | formatter_class=RawTextHelpFormatter) 27 | p.add_argument('cond', help='condition as an FA command') 28 | p.add_argument('label', help='label to jump to if condition is false') 29 | return p 30 | 31 | 32 | def run(segments, args, addresses, interpreter=None, **kwargs): 33 | if len(interpreter.find_from_instructions_list([args.cond], 34 | addresses=addresses[:])) == 0: 35 | interpreter.set_pc(args.label) 36 | 37 | # pc is incremented by 1, after each instruction 38 | interpreter.dec_pc() 39 | return addresses 40 | -------------------------------------------------------------------------------- /fa/commands/argument.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import ida_typeinf 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''get function's argument assignment address 11 | 12 | EXAMPLE: 13 | 0x00000000: ldr r0, =dest 14 | 0x00000004: ldr r1, =src 15 | 0x00000008: mov r2, #4 16 | 0x0000000c: bl memcpy 17 | 18 | results = [0x0c] 19 | -> argument 2 20 | results = [8] # address of 3rd argument 21 | ''' 22 | 23 | 24 | def get_parser(): 25 | p = utils.ArgumentParserNoExit('argument', 26 | description=DESCRIPTION, 27 | formatter_class=RawTextHelpFormatter) 28 | p.add_argument('arg', help='argument number') 29 | return p 30 | 31 | 32 | @context.ida_context 33 | def argument(addresses, arg): 34 | for address in addresses: 35 | args = ida_typeinf.get_arg_addrs(address) 36 | if args is None: 37 | continue 38 | try: 39 | yield args[arg] 40 | except KeyError: 41 | continue 42 | 43 | 44 | def run(segments, args, addresses, interpreter=None, **kwargs): 45 | return list(argument(addresses, eval(args.arg))) 46 | -------------------------------------------------------------------------------- /fa/commands/locate.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | from typing import Iterable, List 3 | 4 | from fa import context, utils 5 | 6 | try: 7 | import idc 8 | except ImportError: 9 | pass 10 | 11 | DESCRIPTION = '''goto symbol by name 12 | 13 | EXAMPLE: 14 | 0x00000000: main: 15 | 0x00000000: mov r0, r1 16 | 0x00000004: foo: 17 | 0x00000004: bx lr 18 | 19 | results = [0, 4] 20 | -> locate foo 21 | result = [4] 22 | ''' 23 | 24 | 25 | def get_parser(): 26 | p = utils.ArgumentParserNoExit('locate', 27 | description=DESCRIPTION, 28 | formatter_class=RawTextHelpFormatter) 29 | p.add_argument('name', nargs='+') 30 | return p 31 | 32 | 33 | @context.ida_context 34 | def locate_single(name) -> int: 35 | return idc.get_name_ea_simple(name) 36 | 37 | 38 | def locate(names: Iterable[str]) -> List[int]: 39 | result = [] 40 | for n in names: 41 | located = locate_single(n) 42 | if located != idc.BADADDR: 43 | result.append(located) 44 | return result 45 | 46 | 47 | def run(segments, args, addresses: Iterable[int], interpreter=None, **kwargs) -> List[int]: 48 | return locate(args.name) 49 | -------------------------------------------------------------------------------- /fa/commands/verify_bytes.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from argparse import RawTextHelpFormatter 3 | 4 | from fa import utils 5 | 6 | DESCRIPTION = '''reduce the result-set to those matching the given bytes 7 | 8 | EXAMPLE: 9 | 0x00000000: 01 02 03 04 10 | 0x00000004: 05 06 07 08 11 | 12 | results = [0, 2, 4, 6, 8] 13 | -> verify-bytes '05 06 07 08' 14 | results = [4] 15 | ''' 16 | 17 | 18 | def get_parser(): 19 | p = utils.ArgumentParserNoExit('verify-bytes', 20 | description=DESCRIPTION, 21 | formatter_class=RawTextHelpFormatter) 22 | p.add_argument('hex_str') 23 | return p 24 | 25 | 26 | def verify_bytes(addresses, hex_str, segments=None, until=None): 27 | magic = binascii.unhexlify(''.join(hex_str.split(' '))) 28 | 29 | results = [ea for ea in addresses 30 | if utils.read_memory(segments, ea, len(magic)) == magic] 31 | 32 | if len(results) > 0: 33 | return results 34 | 35 | return results 36 | 37 | 38 | def run(segments, args, addresses, interpreter=None, **kwargs): 39 | until = None 40 | if 'until' in args and args.until is not None: 41 | until = args.until 42 | return verify_bytes(addresses, args.hex_str, 43 | segments=segments, until=until) 44 | -------------------------------------------------------------------------------- /fa/commands/find_immediate.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import ida_search 7 | import idc 8 | except ImportError: 9 | pass 10 | 11 | DESCRIPTION = '''expands the result-set with the occurrences of the given 12 | immediate in "ida immediate syntax" 13 | 14 | EXAMPLE: 15 | 0x00000000: ldr r0, =0x1234 16 | 0x00000004: add r0, #2 ; 0x1236 17 | 18 | results = [] 19 | -> find-immediate 0x1236 20 | result = [4] 21 | ''' 22 | 23 | 24 | def get_parser(): 25 | p = utils.ArgumentParserNoExit('find-immediate', 26 | description=DESCRIPTION, 27 | formatter_class=RawTextHelpFormatter) 28 | p.add_argument('expression') 29 | return p 30 | 31 | 32 | @context.ida_context 33 | def find_immediate(expression): 34 | if isinstance(expression, str): 35 | expression = eval(expression) 36 | 37 | ea, imm = ida_search.find_imm(0, ida_search.SEARCH_DOWN, expression) 38 | while ea != idc.BADADDR: 39 | yield ea 40 | ea, imm = idc.find_imm(ea + 1, ida_search.SEARCH_DOWN, expression) 41 | 42 | 43 | def run(segments, args, addresses, interpreter=None, **kwargs): 44 | results = list(find_immediate(args.expression)) 45 | return addresses + results 46 | -------------------------------------------------------------------------------- /fa/commands/find_str.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import sys 3 | from argparse import RawTextHelpFormatter 4 | 5 | import six 6 | 7 | from fa.commands import find_bytes 8 | 9 | DESCRIPTION = '''expands the result-set with the occurrences of the given 10 | string 11 | 12 | EXAMPLE: 13 | 0x00000000: 01 02 03 04 14 | 0x00000004: 05 06 07 08 15 | 0x00000008: 30 31 32 33 -> ASCII '0123' 16 | 17 | results = [] 18 | -> find-str '0123' 19 | 20 | result = [8] 21 | ''' 22 | 23 | 24 | def get_parser(): 25 | p = find_bytes.get_parser() 26 | p.prog = 'find-str' 27 | p.description = DESCRIPTION 28 | p.formatter_class = RawTextHelpFormatter 29 | p.add_argument('--null-terminated', action='store_true') 30 | return p 31 | 32 | 33 | def find_str(string: str, null_terminated: bool = False): 34 | hex_str = str(binascii.hexlify(bytearray(string.encode())).decode()) 35 | if null_terminated: 36 | hex_str += '00' 37 | return find_bytes.find_bytes(hex_str) 38 | 39 | 40 | def run(segments, args, addresses, interpreter=None, **kwargs): 41 | hex_str = binascii.hexlify(six.b(args.hex_str)) 42 | 43 | if sys.version[0] == '3': 44 | hex_str = hex_str.decode() 45 | 46 | if args.null_terminated: 47 | hex_str += '00' 48 | setattr(args, 'hex_str', hex_str) 49 | return find_bytes.run(segments, args, addresses, **kwargs) 50 | -------------------------------------------------------------------------------- /fa/commands/function_start.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''goto function's start 11 | 12 | EXAMPLE: 13 | 0x00000000: push {r4-r7, lr} -> function's prolog 14 | ... 15 | 0x000000f0: pop {r4-r7, pc} -> function's epilog 16 | 17 | results = [0xf0] 18 | -> function-start 19 | result = [0] 20 | ''' 21 | 22 | 23 | def get_function_start(segments, ea): 24 | start = idc.get_func_attr(ea, idc.FUNCATTR_START) 25 | return start 26 | 27 | # TODO: consider add support locate of function heads manually 28 | 29 | 30 | def get_parser(): 31 | p = utils.ArgumentParserNoExit('function-start', 32 | description=DESCRIPTION, 33 | formatter_class=RawTextHelpFormatter) 34 | p.add_argument('cmd', nargs='*', default='', help='command') 35 | return p 36 | 37 | 38 | @context.ida_context 39 | def function_start(addresses): 40 | for ea in addresses: 41 | if ea != idc.BADADDR: 42 | func_start = idc.get_func_attr(ea, idc.FUNCATTR_START) 43 | if func_start != idc.BADADDR: 44 | yield func_start 45 | 46 | 47 | def run(segments, args, addresses, interpreter=None, **kwargs): 48 | return list(function_start(addresses)) 49 | -------------------------------------------------------------------------------- /fa/commands/make_offset.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | 3 | try: 4 | import ida_auto 5 | import ida_offset 6 | import idaapi 7 | from idc import REF_OFF8, REF_OFF16, REF_OFF32, REF_OFF64 8 | except ImportError: 9 | pass 10 | 11 | 12 | DESCRIPTION = '''convert into an offset 13 | 14 | EXAMPLE: 15 | 0x00000200: 01 02 03 04 16 | 0x00000204: 00 02 00 00 17 | 18 | results = [0x204] 19 | -> make-offset 20 | results = [0x204] 21 | 22 | 0x00000200: 01 02 03 04 23 | 0x00000204: byte_200 24 | ''' 25 | 26 | 27 | def get_parser(): 28 | p = utils.ArgumentParserNoExit('make-offset', 29 | description=DESCRIPTION) 30 | p.add_argument('-l', '--len', type=int, default=0, help='length of offset in bytes') 31 | return p 32 | 33 | 34 | @context.ida_context 35 | def make_offset(addresses: list[int], offset_len: int = 0): 36 | offset_length_to_ref_type = { 37 | 0: REF_OFF64 if idaapi.get_inf_structure().is_64bit() else REF_OFF32, 38 | 1: REF_OFF8, 39 | 2: REF_OFF16, 40 | 4: REF_OFF32, 41 | 8: REF_OFF64, 42 | } 43 | for ea in addresses: 44 | ida_offset.op_offset(ea, 0, offset_length_to_ref_type[offset_len]) 45 | 46 | ida_auto.auto_wait() 47 | 48 | return addresses 49 | 50 | 51 | def run(segments, args, addresses, interpreter=None, **kwargs): 52 | return make_offset(addresses, args.len) 53 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ '**' ] 9 | pull_request: 10 | branches: [ '**' ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | python-version: [3.8, 3.9, "3.10", 3.11, 3.12] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 31 | pip install -r requirements.txt 32 | pip install -r requirements_testing.txt 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | -------------------------------------------------------------------------------- /elf_loader.py: -------------------------------------------------------------------------------- 1 | import click 2 | from elftools.elf import elffile 3 | 4 | from fa import fainterp 5 | 6 | 7 | class ElfLoader(fainterp.FaInterp): 8 | def __init__(self): 9 | super(ElfLoader, self).__init__() 10 | self._elf = None 11 | 12 | def reload_segments(self): 13 | pass 14 | 15 | def set_input(self, input_): 16 | self._elf = elffile.ELFFile(input_) 17 | self.endianity = '<' if self._elf.little_endian else '>' 18 | 19 | self._segments = {} 20 | for s in self._elf.iter_segments(): 21 | if s.header['p_type'] != 'PT_LOAD': 22 | continue 23 | self.segments[s.header['p_vaddr']] = s.data() 24 | 25 | @property 26 | def segments(self): 27 | return self._segments 28 | 29 | 30 | @click.command() 31 | @click.argument('elf_file', type=click.File('rb')) 32 | @click.argument('signatures_root') 33 | @click.argument('project') 34 | def main(elf_file, signatures_root, project): 35 | interp = ElfLoader() 36 | interp.set_input(elf_file) 37 | interp.set_signatures_root(signatures_root) 38 | interp.set_project(project) 39 | 40 | for k, v in interp.symbols().items(): 41 | if isinstance(v, list) or isinstance(v, set): 42 | if len(v) > 1: 43 | print('# {} multiple matches'.format(k)) 44 | continue 45 | v = v.pop() 46 | print('{} = 0x{:x};'.format(k, v)) 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /fa/context.py: -------------------------------------------------------------------------------- 1 | IDA_MODULE = False 2 | 3 | try: 4 | import idc # noqa: F401 5 | 6 | IDA_MODULE = True 7 | except ImportError: 8 | pass 9 | 10 | 11 | class InvalidContextException(Exception): 12 | pass 13 | 14 | 15 | def get_correct_implementation(function_name, params, ida=None, unknown=None, 16 | **kwargs): 17 | """ 18 | Get and execute the correct implementation according to the currently 19 | executing context 20 | :param function_name: function name to be executes 21 | :param params: parameters to pass to function 22 | :param ida: IDA context implementation 23 | :param unknown: Unknown context implementation 24 | :return: The execution result from the correctly running context 25 | """ 26 | if ida and IDA_MODULE: 27 | return ida(*params, **kwargs) 28 | if unknown: 29 | return unknown(*params, **kwargs) 30 | 31 | raise InvalidContextException( 32 | 'function "{}" must be executed from a specific context' 33 | .format(function_name)) 34 | 35 | 36 | def verify_ida(function_name): 37 | if not IDA_MODULE: 38 | raise InvalidContextException( 39 | 'operation "{}" must be executed from an IDA context' 40 | .format(function_name)) 41 | 42 | 43 | def ida_context(function): 44 | if IDA_MODULE: 45 | return function 46 | else: 47 | def invalid_context(*kwargs): 48 | raise InvalidContextException( 49 | 'function "{}" must be executed from an IDA context' 50 | .format(function.__name__)) 51 | return invalid_context 52 | -------------------------------------------------------------------------------- /fa/commands/goto_ref.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idautils 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''goto reference 11 | 12 | EXAMPLE: 13 | 0x00000000: ldr r0, =0x12345678 14 | 15 | results = [0] 16 | -> goto-ref --data 17 | results = [0x12345678] 18 | ''' 19 | 20 | 21 | def get_parser(): 22 | p = utils.ArgumentParserNoExit('goto-ref', 23 | description=DESCRIPTION, 24 | formatter_class=RawTextHelpFormatter) 25 | p.add_argument('--code', action='store_true', 26 | default=False, help='include code references') 27 | p.add_argument('--data', action='store_true', 28 | default=False, help='include data references') 29 | return p 30 | 31 | 32 | @context.ida_context 33 | def goto_ref(addresses, code=False, data=False): 34 | for address in addresses: 35 | refs = [] 36 | if code: 37 | refs += list(idautils.CodeRefsFrom(address, 0)) 38 | if data: 39 | refs += list(idautils.DataRefsFrom(address)) 40 | 41 | if len(refs) == 0: 42 | continue 43 | 44 | for ref in refs: 45 | if address + 4 != ref: 46 | yield ref 47 | 48 | 49 | def goto_ref_unique(addresses, code=False, data=False): 50 | for address in goto_ref(addresses, code=code, data=data): 51 | yield address 52 | 53 | 54 | def run(segments, args, addresses, interpreter=None, **kwargs): 55 | return list(goto_ref_unique(addresses, 56 | code=args.code, 57 | data=args.data)) 58 | -------------------------------------------------------------------------------- /fa/commands/verify_segment.py: -------------------------------------------------------------------------------- 1 | import re 2 | from argparse import ArgumentParser, RawTextHelpFormatter 3 | from collections.abc import Iterable 4 | from typing import Generator, List 5 | 6 | try: 7 | import idc 8 | except ImportError: 9 | pass 10 | 11 | from fa import context, utils 12 | 13 | DESCRIPTION = '''reduce the result-set to those in the given segment name 14 | 15 | EXAMPLE: 16 | .text:0x00000000 01 02 03 04 17 | .text:0x00000004 30 31 32 33 18 | 19 | .data:0x00000200 01 02 03 04 20 | .data:0x00000204 30 31 32 33 21 | 22 | results = [0, 0x200] 23 | -> verify-segment .data 24 | results = [0x200] 25 | ''' 26 | 27 | 28 | @context.ida_context 29 | def verify_segment(addresses: Iterable[int], segment_name: str, is_regex: bool = False) -> Generator[int, None, None]: 30 | if is_regex: 31 | matcher = re.compile(segment_name) 32 | 33 | def match(n) -> bool: 34 | return bool(matcher.match(n)) 35 | else: 36 | def match(n) -> bool: 37 | return segment_name == n 38 | 39 | for ea in addresses: 40 | real_seg_name = idc.get_segm_name(ea) 41 | if match(real_seg_name): 42 | yield ea 43 | 44 | 45 | def get_parser() -> ArgumentParser: 46 | p = utils.ArgumentParserNoExit() 47 | p.add_argument('name', help='segment name') 48 | p.add_argument('--regex', help='interpret name as a regex', action='store_true') 49 | 50 | p.prog = 'verify-segment' 51 | p.description = DESCRIPTION 52 | p.formatter_class = RawTextHelpFormatter 53 | return p 54 | 55 | 56 | def run(segments, args, addresses: Iterable[int], interpreter=None, **kwargs) -> List[int]: 57 | return list(verify_segment(addresses, args.name, args.regex)) 58 | -------------------------------------------------------------------------------- /fa/commands/xrefs_to.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | from fa.commands import function_start 3 | 4 | try: 5 | import idautils 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | 11 | def get_parser(): 12 | p = utils.ArgumentParserNoExit(prog='xrefs-to', 13 | description='search for xrefs pointing ' 14 | 'at given parameter') 15 | p.add_argument('--function-start', action='store_true', 16 | help='goto function prolog for each xref') 17 | p.add_argument('--or', action='store_true', 18 | help='expand the current result set') 19 | p.add_argument('--and', action='store_true', 20 | help='reduce the current result set') 21 | p.add_argument('--name', help='parameter as label name') 22 | p.add_argument('--bytes', help='parameter as bytes') 23 | return p 24 | 25 | 26 | @context.ida_context 27 | def run(segments, args, addresses, interpreter=None, **kwargs): 28 | if args.name: 29 | ea = idc.LocByName(args.name) 30 | occurrences = [ea] if ea != idc.BADADDR else [] 31 | else: 32 | occurrences = list(utils.ida_find_all(args.bytes)) 33 | 34 | frm = set() 35 | for ea in occurrences: 36 | froms = [ref.frm for ref in idautils.XrefsTo(ea)] 37 | 38 | if args.function_start: 39 | froms = [function_start.get_function_start(segments, ea) 40 | for ea in froms] 41 | 42 | frm.update(frm for frm in froms if frm != idc.BADADDR) 43 | 44 | retval = set() 45 | retval.update(addresses) 46 | 47 | if getattr(args, 'or'): 48 | retval.update(frm) 49 | 50 | elif getattr(args, 'and'): 51 | retval.intersection_update(frm) 52 | 53 | return list(retval) 54 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | import pytest 4 | from keystone import KS_ARCH_ARM, KS_MODE_ARM, KS_MODE_BIG_ENDIAN, Ks 5 | from simpleelf import elf_consts 6 | from simpleelf.elf_builder import ElfBuilder 7 | 8 | 9 | def pytest_addoption(parser): 10 | parser.addoption( 11 | "--ida", action="store", default=None, help="IDA binary" 12 | ) 13 | parser.addoption( 14 | "--idb", action="store", default=None, help="IDB file" 15 | ) 16 | parser.addoption( 17 | "--elf", action="store", default=None, help="ELF file" 18 | ) 19 | 20 | 21 | @pytest.fixture 22 | def sample_elf(request): 23 | with tempfile.NamedTemporaryFile(suffix='.elf', delete=False) as f: 24 | e = ElfBuilder() 25 | e.set_endianity('>') 26 | e.set_machine(elf_consts.EM_ARM) 27 | 28 | # add a segment 29 | text_address = 0x1234 30 | 31 | ks = Ks(KS_ARCH_ARM, KS_MODE_ARM | KS_MODE_BIG_ENDIAN) 32 | 33 | text_buffer = ks.asm(''' 34 | ret_1: 35 | mov r0, #1 36 | bx lr 37 | eloop: 38 | b eloop 39 | data: 40 | .word 0x11223344 41 | .word 0x55667788 42 | .code 32 43 | main: 44 | push {r4-r7, lr} 45 | bl 0x1234 46 | ldr r0, =data 47 | bl 0x1234 48 | pop {r4-r7, pc} 49 | ''', text_address)[0] 50 | text_buffer = bytearray(text_buffer) 51 | 52 | e.add_segment(text_address, text_buffer, 53 | elf_consts.PF_R | elf_consts.PF_W | elf_consts.PF_X) 54 | e.add_code_section(text_address, len(text_buffer), name='.text') 55 | f.write(e.build()) 56 | yield f 57 | 58 | 59 | @pytest.fixture 60 | def ida(request): 61 | return request.config.getoption("--ida") 62 | -------------------------------------------------------------------------------- /tests/test_commands/test_elf.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import elf_loader 4 | 5 | 6 | def test_elf_symbols(sample_elf): 7 | if sample_elf is None: 8 | pytest.skip("--elf param must be passed for this test") 9 | return 10 | 11 | fa_instance = elf_loader.ElfLoader() 12 | fa_instance.set_input(sample_elf) 13 | 14 | fa_instance.set_project('test-project-elf') 15 | symbols = fa_instance.symbols() 16 | 17 | assert symbols['test_add'] == 80 18 | assert symbols['test_pos_offset'] == 81 19 | assert symbols['test_neg_offset'] == 80 20 | assert symbols['test_add_offset_range'] == 100 21 | assert symbols['test_load'] == 80 22 | assert symbols['test_align'] == 84 23 | assert symbols['test_most_common'] == 2 24 | assert symbols['test_sort'] == 3 25 | assert symbols['test_verify_single_success'] == 1 26 | assert 'test_verify_single_fail' not in symbols 27 | assert symbols['test_run'] == 67 28 | assert symbols['test_alias'] == 0x123c 29 | assert symbols['test_keystone_find_opcodes'] == 0x123c 30 | assert symbols['test_keystone_verify_opcodes'] == 0x123c 31 | assert symbols['test_find_bytes'] == 0x1240 32 | assert symbols['test_find_str'] == 0x1242 33 | assert symbols['test_find'] == 76 34 | assert symbols['test_intersect_ab'] == 2 35 | assert 'test_intersect_abc' not in symbols 36 | assert symbols['test_symdiff_ab'] == 4 37 | assert 'test_symdiff_bc' not in symbols 38 | assert symbols['test_symdiff_bcd'] == 8 39 | 40 | # test for branches 41 | assert 'test_is_single_false1' in symbols 42 | assert 'test_is_single_true1' not in symbols 43 | 44 | assert 'test_is_single_false2' not in symbols 45 | assert 'test_is_single_true2' in symbols 46 | 47 | assert 'test_else3' not in symbols 48 | assert 'test_if3' in symbols 49 | -------------------------------------------------------------------------------- /fa/commands/function_lines.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idautils 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''get all function's lines 11 | 12 | EXAMPLE: 13 | 0x00000000: push {r4-r7, lr} -> function's prolog 14 | 0x00000004: mov r1, r0 15 | ... 16 | 0x000000c0: mov r0, r5 17 | ... 18 | 0x000000f0: push {r4-r7, pc} -> function's epilog 19 | 20 | results = [0xc0] 21 | -> function-lines 22 | result = [0, 4, ..., 0xc0, ..., 0xf0] 23 | ''' 24 | 25 | 26 | def get_parser(): 27 | p = utils.ArgumentParserNoExit('function-lines', 28 | description=DESCRIPTION, 29 | formatter_class=RawTextHelpFormatter) 30 | g = p.add_mutually_exclusive_group() 31 | g.add_argument('--after', action='store_true', 32 | help='include only function lines which occur after current' 33 | 'resultset') 34 | g.add_argument('--before', action='store_true', 35 | help='include only function lines which occur before ' 36 | 'current resultset') 37 | return p 38 | 39 | 40 | @context.ida_context 41 | def function_lines(addresses, after=False, before=False): 42 | for address in addresses: 43 | for item in idautils.FuncItems(address): 44 | if after: 45 | if item > address: 46 | yield item 47 | elif before: 48 | if item < address: 49 | yield item 50 | else: 51 | yield item 52 | 53 | 54 | def run(segments, args, addresses, interpreter=None, **kwargs): 55 | return list(function_lines(addresses, after=args.after, 56 | before=args.before)) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # PyCharm 73 | .idea/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # config 110 | config.ini 111 | 112 | # setuptools-scm 113 | fa/_version.py -------------------------------------------------------------------------------- /fa/commands/keystone_find_opcodes.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | try: 4 | # flake8: noqa 5 | from keystone import * 6 | except ImportError: 7 | print('keystone-engine module not installed') 8 | 9 | import binascii 10 | 11 | from fa import utils 12 | from fa.commands import find_bytes 13 | 14 | DESCRIPTION = '''use keystone to search for the supplied opcodes 15 | 16 | EXAMPLE: 17 | 0x00000000: push {r4-r7, lr} 18 | 0x00000004: mov r0, r1 19 | 20 | results = [] 21 | -> keystone-find-opcodes --bele KS_ARCH_ARM KS_MODE_ARM 'mov r0, r1;' 22 | result = [4] 23 | ''' 24 | 25 | 26 | def get_parser(): 27 | p = utils.ArgumentParserNoExit('keystone-find-opcodes', 28 | description=DESCRIPTION, 29 | formatter_class=RawTextHelpFormatter) 30 | p.add_argument('--bele', action='store_true', 31 | help='figure out the endianity from IDA instead of ' 32 | 'explicit mode') 33 | p.add_argument('--or', action='store_true', 34 | help='mandatory. expands search results') 35 | p.add_argument('arch', 36 | help='keystone architecture const (evaled)') 37 | p.add_argument('mode', 38 | help='keystone mode const (evald)') 39 | p.add_argument('code', 40 | help='keystone architecture const (opcodes to compile)') 41 | return p 42 | 43 | 44 | def run(segments, args, addresses, interpreter=None, **kwargs): 45 | arch = eval(args.arch) 46 | mode = eval(args.mode) 47 | 48 | if args.bele: 49 | mode |= KS_MODE_BIG_ENDIAN if \ 50 | interpreter.endianity == '>' else KS_MODE_LITTLE_ENDIAN 51 | 52 | ks = Ks(arch, mode) 53 | compiled_buf = bytearray(ks.asm(args.code)[0]) 54 | 55 | setattr(args, 'hex_str', binascii.hexlify(compiled_buf).decode('utf8')) 56 | return find_bytes.run(segments, args, addresses, **kwargs) 57 | -------------------------------------------------------------------------------- /fa/commands/verify_opcode.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | from typing import Generator, List, Union 3 | 4 | from fa import context, utils 5 | 6 | try: 7 | import idc 8 | except ImportError: 9 | pass 10 | 11 | DESCRIPTION = '''reduce the result-set to those matching the given instruction 12 | 13 | EXAMPLE #1: 14 | 0x00000000: mov r0, r1 15 | 0x00000004: mov r1, r2 16 | 0x00000008: push {r4} 17 | 18 | results = [0, 2, 4, 6, 8] 19 | -> verify-opcode mov 20 | results = [0, 4] 21 | 22 | EXAMPLE #2: 23 | 0x00000000: mov r0, r1 24 | 0x00000004: mov r1, r2 25 | 0x00000008: push {r4} 26 | 27 | results = [0, 2, 4, 6, 8] 28 | -> verify-opcode mov --op1 r2 29 | results = [4] 30 | ''' 31 | 32 | 33 | def get_parser(): 34 | p = utils.ArgumentParserNoExit('verify-opcode', 35 | description=DESCRIPTION, 36 | formatter_class=RawTextHelpFormatter) 37 | p.add_argument('mnem', nargs='+') 38 | utils.add_operand_args(p) 39 | return p 40 | 41 | 42 | @context.ida_context 43 | @utils.yield_unique 44 | def verify_opcode(addresses: List[int], mnems: Union[str, List[str]], regs_description) \ 45 | -> Generator[int, None, None]: 46 | for ea in addresses: 47 | current_mnem = idc.print_insn_mnem(ea).lower() 48 | if current_mnem in mnems: 49 | if not regs_description: 50 | yield ea 51 | continue 52 | 53 | for description in regs_description: 54 | index, values = description 55 | if not utils.compare_operand(ea, index, values): 56 | break 57 | else: 58 | yield ea 59 | 60 | 61 | def run(segments, args, addresses: List[int], interpreter=None, **kwargs) -> List[int]: 62 | regs_description = utils.create_regs_description_from_args(args) 63 | return list(verify_opcode(addresses, args.mnem, regs_description)) 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "fa" 3 | description = "Automation tool for locating symbols & structs in binary (primarily IDA focused)" 4 | readme = "README.md" 5 | requires-python = ">=3.8" 6 | license = { text = "GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007" } 7 | keywords = ["reverse-engineering", "ida", "automation", "signatures", "symbols"] 8 | authors = [ 9 | { name = "doronz88", email = "doron88@gmail.com" } 10 | ] 11 | maintainers = [ 12 | { name = "doronz88", email = "doron88@gmail.com" } 13 | ] 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3 :: Only", 24 | ] 25 | dynamic = ["dependencies", "version"] 26 | 27 | [project.optional-dependencies] 28 | test = ["pytest"] 29 | 30 | [project.urls] 31 | "Homepage" = "https://github.com/doronz88/fa" 32 | "Bug Reports" = "https://github.com/doronz88/fa/issues" 33 | 34 | [tool.setuptools] 35 | package-data = { "fa" = ["res/icons/create_sig.png", 36 | "res/icons/export.png", 37 | "res/icons/find.png", 38 | "res/icons/find_all.png", 39 | "res/icons/save.png", 40 | "res/icons/settings.png", 41 | "res/icons/suitcase.png", 42 | "commands/alias"] } 43 | 44 | [tool.setuptools.packages.find] 45 | exclude = ["docs*", "tests*"] 46 | 47 | [tool.setuptools.dynamic] 48 | dependencies = { file = ["requirements.txt"] } 49 | version = { attr = "fa._version.__version__" } 50 | 51 | [tool.setuptools_scm] 52 | version_file = "fa/_version.py" 53 | 54 | [build-system] 55 | requires = ["setuptools>=43.0.0", "setuptools_scm>=8", "wheel"] 56 | build-backend = "setuptools.build_meta" 57 | -------------------------------------------------------------------------------- /fa/commands/keystone_verify_opcodes.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | try: 4 | # flake8: noqa 5 | from keystone import * 6 | except ImportError: 7 | print('keystone-engine module not installed') 8 | 9 | 10 | import binascii 11 | 12 | from fa import utils 13 | from fa.commands import verify_bytes 14 | 15 | DESCRIPTION = '''use keystone to verify the result-set matches the given 16 | opcodes 17 | 18 | EXAMPLE: 19 | 0x00000000: push {r4-r7, lr} 20 | 0x00000004: mov r0, r1 21 | 22 | results = [0, 4] 23 | -> keystone-verify-opcodes --bele KS_ARCH_ARM KS_MODE_ARM 'mov r0, r1' 24 | result = [4] 25 | ''' 26 | 27 | 28 | def get_parser(): 29 | p = utils.ArgumentParserNoExit('keystone-verify-opcodes', 30 | description=DESCRIPTION, 31 | formatter_class=RawTextHelpFormatter) 32 | p.add_argument('--bele', action='store_true', 33 | help='figure out the endianity from IDA instead of ' 34 | 'explicit mode') 35 | p.add_argument('--until', type=int, 36 | help='keep going onwards opcode-opcode until verified') 37 | p.add_argument('arch', 38 | help='keystone architecture const (evaled)') 39 | p.add_argument('mode', 40 | help='keystone mode const (evald)') 41 | p.add_argument('code', 42 | help='keystone architecture const (opcodes to compile)') 43 | return p 44 | 45 | 46 | def run(segments, args, addresses, interpreter=None, **kwargs): 47 | arch = eval(args.arch) 48 | mode = eval(args.mode) 49 | 50 | if args.bele: 51 | mode |= KS_MODE_BIG_ENDIAN if \ 52 | interpreter.endianity == '>' else KS_MODE_LITTLE_ENDIAN 53 | 54 | ks = Ks(arch, mode) 55 | compiled_buf = bytearray(ks.asm(args.code)[0]) 56 | 57 | setattr(args, 'hex_str', binascii.hexlify(compiled_buf).decode('utf8')) 58 | return verify_bytes.run(segments, args, addresses, **kwargs) 59 | -------------------------------------------------------------------------------- /fa/commands/verify_ref.py: -------------------------------------------------------------------------------- 1 | from fa import context, utils 2 | from fa.commands.locate import locate 3 | 4 | try: 5 | import idautils 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | 11 | def get_parser(): 12 | p = utils.ArgumentParserNoExit('verify-ref', 13 | description='verifies a given reference ' 14 | 'exists to current result set') 15 | p.add_argument('--code', action='store_true', 16 | default=False, help='include code references') 17 | p.add_argument('--data', action='store_true', 18 | default=False, help='include data references') 19 | p.add_argument('--name', default=None, help='symbol name') 20 | return p 21 | 22 | 23 | @context.ida_context 24 | def verify_ref(addresses, name=None, code=False, data=False): 25 | if name is not None: 26 | symbol = locate(name) 27 | 28 | if symbol == idc.BADADDR: 29 | return 30 | 31 | for address in addresses: 32 | refs = [] 33 | if code: 34 | refs += list(idautils.CodeRefsFrom(address, 1)) 35 | if data: 36 | refs += list(idautils.DataRefsFrom(address)) 37 | 38 | if len(refs) == 0: 39 | continue 40 | 41 | for ref in refs: 42 | if name is not None: 43 | if address + 4 != ref and symbol == ref: 44 | yield address 45 | break 46 | else: 47 | if address + 4 != ref: 48 | yield address 49 | break 50 | 51 | 52 | @utils.yield_unique 53 | def verify_ref_unique(addresses, name, code=False, data=False): 54 | for address in verify_ref(addresses, name, code=code, data=data): 55 | yield address 56 | 57 | 58 | @context.ida_context 59 | def run(segments, args, addresses, interpreter=None, **kwargs): 60 | return list(set(verify_ref(addresses, args.name, 61 | code=args.code, data=args.data))) 62 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/test-ida-context.sig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "instructions": [ 4 | find-bytes-ida 11223344 5 | set-name test_find_bytes_ida 6 | 7 | xref 8 | set-name test_xref 9 | 10 | function-start 11 | set-name test_function_start 12 | store func 13 | 14 | function-end 15 | set-name test_function_end 16 | 17 | load func 18 | offset 10 19 | function-lines 20 | single 0 21 | set-name test_function_lines 22 | 23 | function-lines 24 | verify-operand ldr --op0 0 25 | set-name test_verify_operand 26 | store ref 27 | 28 | verify-ref --code --data 29 | set-name test_verify_ref_no_name 30 | 31 | goto-ref --data 32 | set-name test_verify_goto_ref 33 | 34 | load ref 35 | verify-ref --name test_verify_goto_ref --code --data 36 | set-name test_verify_ref_name 37 | 38 | locate test_function_lines 39 | set-name test_locate 40 | 41 | clear 42 | 43 | find_immediate 0x11223344 44 | set-name test_find_immediate 45 | 46 | clear 47 | 48 | add 4 49 | set-const TEST_CONST_VALUE_4 50 | set-enum TEST_ENUM_NAME TEST_ENUM_KEY1_VALUE_4 51 | 52 | clear 53 | 54 | add 6 55 | set-enum TEST_ENUM_NAME TEST_ENUM_KEY2_VALUE_6 56 | 57 | clear 58 | 59 | arm-find-all 'mov r0, 1' 60 | single 0 61 | operand 1 62 | set-name test_operand 63 | 64 | clear 65 | 66 | add 0 67 | set-struct-member test_struct_t test_member_offset_0 'unsigned int' 68 | 69 | offset 4 70 | set-struct-member test_struct_t test_member_offset_4 'unsigned int' 71 | 72 | clear 73 | 74 | arm-find-all 'mov r0, 1; bx lr' 75 | set-name funcy 76 | set-type 'int func(int)' 77 | xref 78 | sort 79 | single 1 80 | argument 0 81 | 82 | set-name test_argument 83 | 84 | clear 85 | 86 | arm-find-all 'mov r0, 1; bx lr' 87 | 88 | verify-operand mov --op0 0 89 | store tmp 90 | 91 | python-if tmp test_branch1 92 | set-name test_branch1_false 93 | 94 | label test_branch1 95 | set-name test_branch1_true 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /fa/commands/verify_operand.py: -------------------------------------------------------------------------------- 1 | from argparse import RawTextHelpFormatter 2 | 3 | from fa import context, utils 4 | 5 | try: 6 | import idc 7 | except ImportError: 8 | pass 9 | 10 | DESCRIPTION = '''reduce the result-set to those matching the given instruction 11 | 12 | EXAMPLE #1: 13 | 0x00000000: mov r0, r1 14 | 0x00000004: mov r1, r2 15 | 0x00000008: push {r4} 16 | 17 | results = [0, 2, 4, 6, 8] 18 | -> verify-operand mov 19 | results = [0, 4] 20 | 21 | EXAMPLE #2: 22 | 0x00000000: mov r0, r1 23 | 0x00000004: mov r1, r2 24 | 0x00000008: push {r4} 25 | 26 | results = [0, 2, 4, 6, 8] 27 | -> verify-operand mov --op1 2 28 | results = [4] 29 | ''' 30 | 31 | 32 | def get_parser(): 33 | p = utils.ArgumentParserNoExit('verify-operand', 34 | description=DESCRIPTION, 35 | formatter_class=RawTextHelpFormatter) 36 | p.add_argument('name') 37 | p.add_argument('--op0') 38 | p.add_argument('--op1') 39 | p.add_argument('--op2') 40 | return p 41 | 42 | 43 | @context.ida_context 44 | @utils.yield_unique 45 | def verify_operand(addresses, mnem, op0=None, op1=None, op2=None): 46 | for address in addresses: 47 | current_mnem = idc.print_insn_mnem(address).lower() 48 | if current_mnem == mnem: 49 | if not op0 and not op1 and not op2: 50 | yield address 51 | continue 52 | 53 | regs_description = [] 54 | 55 | if op0: 56 | regs_description.append((0, op0)) 57 | if op1: 58 | regs_description.append((1, op1)) 59 | if op2: 60 | regs_description.append((2, op2)) 61 | 62 | for description in regs_description: 63 | index, values = description 64 | if idc.get_operand_value(address, index) not in values: 65 | break 66 | else: 67 | yield address 68 | 69 | 70 | def run(segments, args, addresses, interpreter=None, **kwargs): 71 | op0 = [eval(i) for i in args.op0.split(',')] if args.op0 else None 72 | op1 = [eval(i) for i in args.op1.split(',')] if args.op1 else None 73 | op2 = [eval(i) for i in args.op2.split(',')] if args.op2 else None 74 | return list(verify_operand(addresses, 75 | args.name, 76 | op0=op0, 77 | op1=op1, 78 | op2=op2)) 79 | -------------------------------------------------------------------------------- /fa/fa_types.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from collections import namedtuple 3 | 4 | try: 5 | import ida_auto 6 | import ida_bytes 7 | import ida_typeinf 8 | import idaapi 9 | import idc 10 | 11 | IDA_MODULE = True 12 | except ImportError: 13 | IDA_MODULE = False 14 | 15 | 16 | def del_struct_members(sid: int, offset1: int, offset2: int) -> None: 17 | tif = ida_typeinf.tinfo_t() 18 | if tif.get_type_by_tid(sid) and tif.is_udt(): 19 | udm = ida_typeinf.udm_t() 20 | udm.offset = offset1 * 8 21 | idx1 = tif.find_udm(udm, ida_typeinf.STRMEM_OFFSET) 22 | udm = ida_typeinf.udm_t() 23 | udm.offset = offset2 * 8 24 | idx2 = tif.find_udm(udm, ida_typeinf.STRMEM_OFFSET) 25 | idx1 &= 0xffffffff 26 | idx2 &= 0xffffffff 27 | tif.del_udms(idx1, idx2) 28 | 29 | 30 | class FaType(object): 31 | def __init__(self, name): 32 | self._name = name 33 | 34 | def get_name(self): 35 | return self._name 36 | 37 | def exists(self): 38 | return -1 != idc.get_struc_id(self._name) 39 | 40 | @abstractmethod 41 | def update_idb(self): 42 | pass 43 | 44 | 45 | class FaEnum(FaType): 46 | def __init__(self, name): 47 | super(FaEnum, self).__init__(name) 48 | self._values = {} 49 | 50 | def add_value(self, name, value): 51 | self._values[value] = name 52 | 53 | def update_idb(self): 54 | id = idc.get_enum(self._name) 55 | if idc.BADADDR == id: 56 | id = idc.add_enum(idc.BADADDR, self._name, ida_bytes.dec_flag()) 57 | 58 | keys = self._values.keys() 59 | sorted(keys) 60 | 61 | for k in keys: 62 | idc.add_enum_member(id, self._values[k], k, 0xffffffffffffffff) 63 | 64 | 65 | class FaStruct(FaType): 66 | Field = namedtuple('Field', ['name', 'type', 'offset']) 67 | 68 | def __init__(self, name): 69 | super(FaStruct, self).__init__(name) 70 | self._fields = [] 71 | 72 | def add_field(self, name, type_, offset=0xffffffff): 73 | self._fields.append(self.Field(name, type_, offset)) 74 | 75 | def update_idb(self, delete_existing_members: bool = True) -> None: 76 | sid = idc.get_struc_id(self._name) 77 | 78 | if sid == idc.BADADDR: 79 | sid = idc.add_struc(idc.BADADDR, self._name, 0) 80 | else: 81 | if delete_existing_members: 82 | del_struct_members(sid, 0, 0xffffffff) 83 | 84 | for f in self._fields: 85 | idc.add_struc_member(sid, f.name, f.offset, (idc.FF_BYTE | idc.FF_DATA) & 0xFFFFFFFF, 0xFFFFFFFF, 1) 86 | member_name = f'{self._name}.{f.name}' 87 | member_struct_id = idc.get_struc_id(member_name) 88 | idc.SetType(member_struct_id, f.type) 89 | 90 | ida_auto.auto_wait() 91 | 92 | 93 | def add_const(name, value): 94 | fa_consts = FaEnum('FA_CONSTS') 95 | fa_consts.add_value(name, value) 96 | fa_consts.update_idb() 97 | -------------------------------------------------------------------------------- /fa/ida_launcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | import socket 4 | import subprocess 5 | from collections import namedtuple 6 | 7 | import click 8 | import IPython 9 | import rpyc 10 | from termcolor import cprint 11 | 12 | IDA_PLUGIN_PATH = os.path.abspath(os.path.join((os.path.dirname(__file__), 'ida_plugin.py'))) 13 | 14 | TerminalProgram = namedtuple('TerminalProgram', 'executable args') 15 | 16 | 17 | def is_windows(): 18 | return os.name == 'nt' 19 | 20 | 21 | SUPPORTED_TERMINALS = [ 22 | TerminalProgram(executable='kitty', args=['bash', '-c']), 23 | TerminalProgram(executable='gnome-terminal', args=['-x', 'bash', '-c']), 24 | TerminalProgram(executable='xterm', args=['-e']), 25 | ] 26 | 27 | 28 | def get_free_port(): 29 | s = socket.socket() 30 | s.bind(('', 0)) 31 | port = s.getsockname()[1] 32 | s.close() 33 | return port 34 | 35 | 36 | def does_program_exist(program): 37 | return 0 == subprocess.Popen(['which', program]).wait() 38 | 39 | 40 | def execute_in_new_terminal(cmd): 41 | if is_windows(): 42 | subprocess.Popen(cmd) 43 | return 44 | 45 | for terminal in SUPPORTED_TERMINALS: 46 | if does_program_exist(terminal.executable): 47 | subprocess.Popen([terminal.executable] + terminal.args + [' '.join(cmd)]) 48 | return 49 | 50 | 51 | def get_client(ida, payload, loader=None, processor_type=None, accept_defaults=False, log_file_path=None): 52 | port = get_free_port() 53 | args = [ida] 54 | 55 | if processor_type is not None: 56 | args.append('-p{}'.format(processor_type)) 57 | 58 | if loader is not None: 59 | args.append('-T{}'.format(loader)) 60 | 61 | if log_file_path is not None: 62 | args.append('-L{}'.format(log_file_path)) 63 | 64 | if accept_defaults: 65 | args.append('-A') 66 | 67 | args.append('\'-S{} --service {}\''.format(IDA_PLUGIN_PATH, port)) 68 | args.append(payload) 69 | 70 | execute_in_new_terminal(args) 71 | 72 | while True: 73 | try: 74 | client = rpyc.connect('localhost', port, config={ 75 | # this is meant to disable the timeout 76 | 'sync_request_timeout': None, 77 | 'allow_all_attrs': True, 78 | 'allow_setattr': True, 79 | }) 80 | break 81 | except socket.error: 82 | pass 83 | 84 | return client 85 | 86 | 87 | def launch_ida_in_service_mode(ida, payload, loader=None): 88 | client = get_client(ida, payload, loader) 89 | cprint('use `client.root` variable to access the remote object', 'cyan') 90 | IPython.embed() 91 | client.close() 92 | 93 | 94 | @click.command() 95 | @click.argument('ida', type=click.Path(exists=True)) 96 | @click.argument('payload', type=click.Path(exists=True)) 97 | @click.option('-l', '--loader', required=False) 98 | def shell(ida, payload, loader): 99 | launch_ida_in_service_mode(ida, payload, loader) 100 | 101 | 102 | if __name__ == '__main__': 103 | shell() 104 | -------------------------------------------------------------------------------- /fa/signatures/test-project-ida/test-basic.sig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "instructions": [ 4 | add 80 5 | set-name test_add 6 | store 80 7 | 8 | offset 1 9 | set-name test_pos_offset 10 | 11 | offset -1 12 | set-name test_neg_offset 13 | 14 | add-offset-range 0 21 4 15 | single -1 16 | set-name test_add_offset_range 17 | 18 | clear 19 | load 80 20 | set-name test_load 21 | 22 | offset 1 23 | align 4 24 | set-name test_align 25 | 26 | clear 27 | 28 | add 1 29 | add 2 30 | add 3 31 | add 2 32 | most-common 33 | set-name test_most_common 34 | 35 | clear 36 | 37 | add 1 38 | add 2 39 | add 3 40 | add 2 41 | sort 42 | single -1 43 | set-name test_sort 44 | 45 | clear 46 | 47 | add 1 48 | add 1 49 | verify-single 50 | set-name test_verify_single_fail 51 | 52 | clear 53 | 54 | add 1 55 | verify-single 56 | set-name test_verify_single_success 57 | 58 | clear 59 | 60 | run test_dep.dep 61 | 62 | clear 63 | 64 | arm-find-all 'loop: b loop' 65 | set-name test_alias 66 | set-name test_keystone_find_opcodes 67 | 68 | arm-verify 'loop: b loop' 69 | set-name test_keystone_verify_opcodes 70 | 71 | clear 72 | 73 | find-bytes 11223344 74 | set-name test_find_bytes 75 | 76 | verify-bytes 11223344 77 | set-name test_verify_bytes 78 | 79 | clear 80 | 81 | find-str '3DUfw' 82 | set-name test_find_str 83 | 84 | clear 85 | 86 | find test_find 87 | 88 | clear 89 | 90 | add 1 91 | add 2 92 | add 3 93 | 94 | store a 95 | 96 | clear 97 | 98 | add 2 99 | add 8 100 | add 12 101 | 102 | store b 103 | 104 | clear 105 | 106 | store c 107 | 108 | intersect a b 109 | set-name test_intersect_ab 110 | 111 | intersect a b c 112 | set-name test_intersect_abc 113 | 114 | clear 115 | 116 | add 1 117 | add 2 118 | 119 | verify-single 120 | store is_single1 121 | python-if is_single1 is_single_label1 122 | add 1 123 | set-name test_is_single_false1 124 | b end1 125 | 126 | label is_single_label1 127 | set-name test_is_single_true1 128 | 129 | label end1 130 | 131 | clear 132 | 133 | add 1 134 | 135 | verify-single 136 | store is_single2 137 | 138 | python-if is_single2 is_single_label2 139 | set-name test_is_single_false2 140 | b end2 141 | 142 | label is_single_label2 143 | set-name test_is_single_true2 144 | 145 | label end2 146 | 147 | clear 148 | 149 | add 1 150 | 151 | if 'verify-single' is_single_label3 152 | 153 | clear 154 | add 1 155 | set-name test_else3 156 | b end3 157 | 158 | label is_single_label3 159 | 160 | clear 161 | add 1 162 | set-name test_if3 163 | 164 | label end3 165 | 166 | ] 167 | } 168 | -------------------------------------------------------------------------------- /fa/commands/next_instruction.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, RawTextHelpFormatter 2 | from typing import Iterable, List, Optional, Tuple 3 | 4 | from fa import context, utils 5 | 6 | try: 7 | import idautils 8 | import idc 9 | except ImportError: 10 | pass 11 | 12 | DESCRIPTION = '''Map the resultset to the next instruction of a given pattern. The instruction is searched for linearly. 13 | 14 | Example #1: 15 | 0x00000000: mov r0, r1 16 | 0x00000004: mov r1, r2 17 | 0x00000008: push {r4} 18 | 0x0000000c: mov r2, r3 19 | 20 | results = [0, 4, 8] 21 | -> next-instruction mov 22 | results = [0, 4, 12] 23 | 24 | Example #2: 25 | 0x00000000: mov r0, r1 26 | 0x00000004: mov r1, r2 27 | 0x00000008: push {r4} 28 | 0x0000000c: mov r2, r3 29 | 30 | results = [0, 4, 8] 31 | -> next-instruction mov --op 2 32 | results = [12, 12, 12] 33 | ''' 34 | 35 | 36 | def get_parser() -> ArgumentParser: 37 | p = utils.ArgumentParserNoExit('next-instruction', 38 | description=DESCRIPTION, 39 | formatter_class=RawTextHelpFormatter) 40 | 41 | p.add_argument('mnem', nargs='+') 42 | p.add_argument('--limit', type=int, help='Number of instructions to search per address', default=None) 43 | p.add_argument('--back', action='store_true', help='Search backwards instead of forwards') 44 | utils.add_operand_args(p) 45 | return p 46 | 47 | 48 | def _find_next_instruction(mnems: Iterable[str], 49 | regs_description: Iterable[Tuple[int, Iterable[str]]], 50 | address: int, 51 | backwards: bool = False, 52 | limit: Optional[int] = None) -> Optional[int]: 53 | instructions = list(idautils.FuncItems(address)) 54 | 55 | if backwards: 56 | instructions = [ea for ea in instructions if ea <= address][::-1] 57 | else: 58 | instructions = [ea for ea in instructions if ea >= address] 59 | 60 | if limit is not None: 61 | instructions = instructions[:limit] 62 | 63 | for ea in instructions: 64 | current_mnem = idc.print_insn_mnem(ea).lower() 65 | if current_mnem in mnems: 66 | if not regs_description: 67 | return ea 68 | 69 | for description in regs_description: 70 | index, values = description 71 | if not utils.compare_operand(ea, index, values): 72 | break 73 | else: 74 | return ea 75 | 76 | return None 77 | 78 | 79 | @context.ida_context 80 | def next_instruction(addresses: List[int], 81 | mnem: str, 82 | regs_description: Iterable[Tuple[int, Iterable[str]]], 83 | backwards: bool = False, 84 | limit: Optional[int] = None) -> List[int]: 85 | for address in addresses: 86 | r = _find_next_instruction(mnem, regs_description, address, backwards, limit) 87 | if r is not None: 88 | yield r 89 | 90 | 91 | def run(segments, args, addresses: List[int], interpreter=None, **kwargs): 92 | regs_description = utils.create_regs_description_from_args(args) 93 | return list(next_instruction(addresses, args.mnem, regs_description, args.back, args.limit)) 94 | -------------------------------------------------------------------------------- /scripts/git/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import json 3 | import os 4 | import re 5 | import sys 6 | from collections import OrderedDict 7 | 8 | sys.path.append('.') # noqa: E402 9 | 10 | from fa.fainterp import FaInterp 11 | 12 | COMMANDS_ROOT = os.path.join( 13 | os.path.dirname(os.path.abspath(__file__)), '..', '..', 'fa', 'commands') 14 | 15 | COMMANDS_MD = os.path.join( 16 | os.path.dirname(os.path.abspath(__file__)), '..', '..', 'commands.md') 17 | 18 | SUBLIME_COMP = os.path.join( 19 | os.path.dirname(os.path.abspath(__file__)), 20 | '..', '..', 'ide-completions', 'sublime', 'sig.sublime-completions') 21 | 22 | 23 | def main(): 24 | command_usage = OrderedDict() 25 | command_usage['label'] = 'label' 26 | 27 | command_help = OrderedDict() 28 | command_help['label'] = 'builtin interpreter command. mark a label\n' 29 | 30 | commands = os.listdir(COMMANDS_ROOT) 31 | commands.sort() 32 | 33 | sublime_completions = { 34 | 'scope': 'source.hjson meta.structure.dictionary.hjson ' 35 | 'meta.structure.key-value.hjson meta.structure.array.hjson', 36 | 'completions': [] 37 | } 38 | 39 | for filename in commands: 40 | if filename.endswith('.py') and \ 41 | filename not in ('__init__.py',): 42 | command = os.path.splitext(filename)[0] 43 | command = FaInterp.get_command(command) 44 | 45 | p = command.get_parser() 46 | command_help[p.prog] = p.format_help() 47 | 48 | snippet = p.format_usage().split('usage: ', 1)[1]\ 49 | .replace('\n', '').strip().replace(' [-h]', '')\ 50 | .replace('[', '').replace(']', '') 51 | 52 | def replacer(m): 53 | buf = '' 54 | global index 55 | for g in m.groups(): 56 | if g.startswith('--'): 57 | buf += g 58 | else: 59 | buf += '${%d:%s}' % (index, g) 60 | index += 1 61 | return buf 62 | 63 | args = '' 64 | cmd = snippet 65 | 66 | if ' ' in snippet: 67 | cmd, args = snippet.split(' ', 1) 68 | globals()['index'] = 1 69 | args = re.sub('([\-\w]+)', replacer, args) 70 | 71 | sublime_completions['completions'].append({ 72 | 'trigger': p.prog, 73 | 'kind': 'snippet', 74 | 'contents': cmd + ' ' + args, 75 | }) 76 | 77 | commands_md_buf = '' 78 | commands_md_buf += '# FA Command List\n' 79 | commands_md_buf += 'Below is the list of available commands:\n' 80 | 81 | for command in command_help.keys(): 82 | commands_md_buf += '- [{command}](#{command})\n'\ 83 | .format(command=command) 84 | 85 | for command, help in command_help.items(): 86 | commands_md_buf += '## {}\n```\n{}```\n'.format(command, help) 87 | 88 | with open(COMMANDS_MD, 'rt') as f: 89 | current_buf = f.read() 90 | 91 | with open(COMMANDS_MD, 'wt') as f: 92 | f.write(commands_md_buf) 93 | 94 | with open(SUBLIME_COMP, 'wt') as f: 95 | f.write(json.dumps(sublime_completions, indent=4)) 96 | 97 | if current_buf != commands_md_buf: 98 | print('commands.md and / or ide-completions/ has been changed. Please review and then commit again.') 99 | sys.exit(1) 100 | 101 | 102 | if __name__ == '__main__': 103 | main() 104 | -------------------------------------------------------------------------------- /tests/test_commands/test_find_bytes.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import pytest 4 | 5 | from tests.utils.mock_fa import MockFaInterp 6 | 7 | 8 | @pytest.mark.parametrize("segments,instruction,result", [ 9 | # Sanity 10 | ([(0x12345678, b"\x11\x22\x33\x44")], "find-bytes '11223344'", 11 | [0x12345678]), 12 | ([(0x12345678, b"\x00\x00\x00\x00")], "find-bytes '00000000'", 13 | [0x12345678]), 14 | ([(0x12345678, b"\xff\xff\xff\xff")], "find-bytes 'ffffffff'", 15 | [0x12345678]), 16 | # No results 17 | ([(0x12345678, b"\x11\x22\x33\x45")], "find-bytes '11223344'", []), 18 | ([(0x12345678, b"\x00\x00\x00\x00")], "find-bytes '11223344'", []), 19 | ([(0x12345678, b"\xff\xff\xff\xff")], "find-bytes '11223344'", []), 20 | ([(0x12345678, b"\x44\x33\x22\x11")], "find-bytes '11223344'", []), 21 | # Multiple results in the same segment 22 | ([(0x12345678, b"\x11\x22\x33\x44\x00\x00\x00\x00\x11\x22\x33\x44")], 23 | "find-bytes '11223344'", [0x12345678, 0x12345680]), 24 | # Multiple results in the different segments 25 | ([(0x12345678, b"\x11\x22\x33\x44"), (0x55554444, b"\x11\x22\x33\x44")], 26 | "find-bytes '11223344'", [0x12345678, 0x55554444]), 27 | # Multiple results 28 | ([(0x12345678, b"\x11\x22\x33\x44\x00\x00\x00\x00\x11\x22\x33\x44"), 29 | (0x55554444, b"\x11\x22\x33\x44")], 30 | "find-bytes '11223344'", [0x12345678, 0x12345680, 0x55554444]), 31 | # Overlapping results in the same segment 32 | ([(0x12345678, b"\x11\x22\x11\x22\x11\x22")], "find-bytes '11221122'", 33 | [0x12345678, 0x1234567a]), 34 | # Overlapping results in different segments - not supported! 35 | ([(0x12345678, b"\x11\x22\x11\x22"), (0x1234567c, b"\x11\x22\x33\x44")], 36 | "find-bytes '11221122'", [0x12345678]), 37 | ]) 38 | def test_find_bytes_or(segments, instruction, result): 39 | analyzer = MockFaInterp() 40 | analyzer.segments = OrderedDict(segments) 41 | assert analyzer.find_from_instructions_list( 42 | [instruction, "sort"]) == result 43 | 44 | 45 | @pytest.mark.parametrize("segments,instructions,result", [ 46 | # Sanity 47 | ([(0x12345678, b"\x11\x22\x33\x44\x11\x22\x11\x22")], 48 | ["find-bytes '11223344'", "find-bytes '11221122'", "sort"], 49 | [0x12345678, 0x1234567c]), 50 | # Results across segments 51 | ([(0x12345678, b"\x11\x22\x33\x44"), (0x55554444, b"\x11\x22\x11\x22")], 52 | ["find-bytes '11223344'", "find-bytes '11221122'", "sort"], 53 | [0x12345678, 0x55554444]), 54 | # First find has no results 55 | ([(0x12345678, b"\x11\x22\x33\x45\x11\x22\x11\x22")], 56 | ["find-bytes '11223344'", "find-bytes '11221122'", "sort"], 57 | [0x1234567c]), 58 | # Second find has no results 59 | ([(0x12345678, b"\x11\x22\x33\x44\x11\x22\x11\x23")], 60 | ["find-bytes '11223344'", "find-bytes '11221122'", "sort"], 61 | [0x12345678]), 62 | # Same address across finds 63 | ([(0x12345678, b"\x11\x22\x33\x44\x11\x22\x11\x23")], 64 | ["find-bytes '11223344'", "find-bytes '11223344'", "unique", "sort"], 65 | [0x12345678]), 66 | ]) 67 | def test_multiple_find_bytes_or(segments, instructions, result): 68 | analyzer = MockFaInterp() 69 | analyzer.segments = OrderedDict(segments) 70 | assert analyzer.find_from_instructions_list(instructions) == result 71 | 72 | 73 | @pytest.mark.parametrize("instruction", [ 74 | "find-bytes --and '11223344'" 75 | ]) 76 | def test_find_bytes_with_wrong_manner(instruction): 77 | analyzer = MockFaInterp() 78 | with pytest.raises(ValueError): 79 | assert analyzer.find_from_instructions_list([instruction]) 80 | -------------------------------------------------------------------------------- /fa/signatures/test-project-elf/test-basic.sig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "instructions": [ 4 | add 80 5 | set-name test_add 6 | store 80 7 | 8 | offset 1 9 | set-name test_pos_offset 10 | 11 | offset -1 12 | set-name test_neg_offset 13 | 14 | add-offset-range 0 21 4 15 | single -1 16 | set-name test_add_offset_range 17 | 18 | clear 19 | load 80 20 | set-name test_load 21 | 22 | offset 1 23 | align 4 24 | set-name test_align 25 | 26 | clear 27 | 28 | add 1 29 | add 2 30 | add 3 31 | add 2 32 | most-common 33 | set-name test_most_common 34 | 35 | clear 36 | 37 | add 1 38 | add 2 39 | add 3 40 | add 2 41 | sort 42 | single -1 43 | set-name test_sort 44 | 45 | clear 46 | 47 | add 1 48 | add 1 49 | verify-single 50 | set-name test_verify_single_fail 51 | 52 | clear 53 | 54 | add 1 55 | verify-single 56 | set-name test_verify_single_success 57 | 58 | clear 59 | 60 | run test_dep.dep 61 | 62 | clear 63 | 64 | arm-find-all 'loop: b loop' 65 | set-name test_alias 66 | set-name test_keystone_find_opcodes 67 | 68 | arm-verify 'loop: b loop' 69 | set-name test_keystone_verify_opcodes 70 | 71 | clear 72 | 73 | find-bytes 11223344 74 | set-name test_find_bytes 75 | 76 | verify-bytes 11223344 77 | set-name test_verify_bytes 78 | 79 | clear 80 | 81 | find-str '3DUfw' 82 | set-name test_find_str 83 | 84 | clear 85 | 86 | find test_find 87 | 88 | clear 89 | 90 | add 1 91 | add 2 92 | add 3 93 | 94 | store a 95 | 96 | clear 97 | 98 | add 2 99 | add 8 100 | add 12 101 | 102 | store b 103 | 104 | clear 105 | 106 | store c 107 | 108 | intersect a b 109 | set-name test_intersect_ab 110 | 111 | intersect a b c 112 | set-name test_intersect_abc 113 | 114 | clear 115 | 116 | add 2 117 | 118 | store a 119 | 120 | add 4 121 | 122 | store b 123 | store c 124 | 125 | clear 126 | 127 | add 8 128 | 129 | store d 130 | 131 | clear 132 | 133 | symdiff a b 134 | set-name test_symdiff_ab 135 | 136 | symdiff b c 137 | set-name test_symdiff_bc 138 | 139 | symdiff b c d 140 | set-name test_symdiff_bcd 141 | 142 | clear 143 | 144 | add 1 145 | add 2 146 | 147 | verify-single 148 | store is_single1 149 | python-if is_single1 is_single_label1 150 | add 1 151 | set-name test_is_single_false1 152 | b end1 153 | 154 | label is_single_label1 155 | set-name test_is_single_true1 156 | 157 | label end1 158 | 159 | clear 160 | 161 | add 1 162 | 163 | verify-single 164 | store is_single2 165 | 166 | python-if is_single2 is_single_label2 167 | set-name test_is_single_false2 168 | b end2 169 | 170 | label is_single_label2 171 | set-name test_is_single_true2 172 | 173 | label end2 174 | 175 | clear 176 | 177 | add 1 178 | 179 | if 'verify-single' is_single_label3 180 | 181 | clear 182 | add 1 183 | set-name test_else3 184 | b end3 185 | 186 | label is_single_label3 187 | 188 | clear 189 | add 1 190 | set-name test_if3 191 | 192 | label end3 193 | 194 | ] 195 | } 196 | -------------------------------------------------------------------------------- /ide-completions/sublime/sig.sublime-completions: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "source.hjson meta.structure.dictionary.hjson meta.structure.key-value.hjson meta.structure.array.hjson", 3 | "completions": [ 4 | { 5 | "trigger": "add", 6 | "kind": "snippet", 7 | "contents": "add ${1:value}" 8 | }, 9 | { 10 | "trigger": "add-offset-range", 11 | "kind": "snippet", 12 | "contents": "add-offset-range ${1:start} ${2:end} ${3:step}" 13 | }, 14 | { 15 | "trigger": "align", 16 | "kind": "snippet", 17 | "contents": "align ${1:value}" 18 | }, 19 | { 20 | "trigger": "argument", 21 | "kind": "snippet", 22 | "contents": "argument ${1:arg}" 23 | }, 24 | { 25 | "trigger": "b", 26 | "kind": "snippet", 27 | "contents": "b ${1:label}" 28 | }, 29 | { 30 | "trigger": "clear", 31 | "kind": "snippet", 32 | "contents": "clear " 33 | }, 34 | { 35 | "trigger": "deref-data", 36 | "kind": "snippet", 37 | "contents": "deref-data ${1:-l} ${2:LEN}" 38 | }, 39 | { 40 | "trigger": "find", 41 | "kind": "snippet", 42 | "contents": "find ${1:name}" 43 | }, 44 | { 45 | "trigger": "find-bytes", 46 | "kind": "snippet", 47 | "contents": "find-bytes ${1:hex_str}" 48 | }, 49 | { 50 | "trigger": "find-bytes-ida", 51 | "kind": "snippet", 52 | "contents": "find-bytes-ida ${1:expression}" 53 | }, 54 | { 55 | "trigger": "find-immediate", 56 | "kind": "snippet", 57 | "contents": "find-immediate ${1:expression}" 58 | }, 59 | { 60 | "trigger": "find-str", 61 | "kind": "snippet", 62 | "contents": "find-str --null-terminated ${1:hex_str}" 63 | }, 64 | { 65 | "trigger": "function-end", 66 | "kind": "snippet", 67 | "contents": "function-end " 68 | }, 69 | { 70 | "trigger": "function-lines", 71 | "kind": "snippet", 72 | "contents": "function-lines --after | --before" 73 | }, 74 | { 75 | "trigger": "function-start", 76 | "kind": "snippet", 77 | "contents": "function-start ${1:cmd} ..." 78 | }, 79 | { 80 | "trigger": "goto-ref", 81 | "kind": "snippet", 82 | "contents": "goto-ref --code --data" 83 | }, 84 | { 85 | "trigger": "if", 86 | "kind": "snippet", 87 | "contents": "if ${1:cond} ${2:label}" 88 | }, 89 | { 90 | "trigger": "if-not", 91 | "kind": "snippet", 92 | "contents": "if-not ${1:cond} ${2:label}" 93 | }, 94 | { 95 | "trigger": "intersect", 96 | "kind": "snippet", 97 | "contents": "intersect --piped ${1:variables} ${2:variables} ..." 98 | }, 99 | { 100 | "trigger": "keystone-find-opcodes", 101 | "kind": "snippet", 102 | "contents": "keystone-find-opcodes --bele --or ${1:arch} ${2:mode} ${3:code}" 103 | }, 104 | { 105 | "trigger": "keystone-verify-opcodes", 106 | "kind": "snippet", 107 | "contents": "keystone-verify-opcodes --bele --until ${1:UNTIL} ${2:arch} ${3:mode} ${4:code}" 108 | }, 109 | { 110 | "trigger": "load", 111 | "kind": "snippet", 112 | "contents": "load ${1:name}" 113 | }, 114 | { 115 | "trigger": "locate", 116 | "kind": "snippet", 117 | "contents": "locate ${1:name} ${2:name} ..." 118 | }, 119 | { 120 | "trigger": "make-code", 121 | "kind": "snippet", 122 | "contents": "make-code " 123 | }, 124 | { 125 | "trigger": "make-comment", 126 | "kind": "snippet", 127 | "contents": "make-comment ${1:comment}" 128 | }, 129 | { 130 | "trigger": "make-function", 131 | "kind": "snippet", 132 | "contents": "make-function " 133 | }, 134 | { 135 | "trigger": "make-literal", 136 | "kind": "snippet", 137 | "contents": "make-literal " 138 | }, 139 | { 140 | "trigger": "make-offset", 141 | "kind": "snippet", 142 | "contents": "make-offset ${1:-l} ${2:LEN}" 143 | }, 144 | { 145 | "trigger": "make-unknown", 146 | "kind": "snippet", 147 | "contents": "make-unknown " 148 | }, 149 | { 150 | "trigger": "max-xrefs", 151 | "kind": "snippet", 152 | "contents": "max-xrefs " 153 | }, 154 | { 155 | "trigger": "min-xrefs", 156 | "kind": "snippet", 157 | "contents": "min-xrefs " 158 | }, 159 | { 160 | "trigger": "most-common", 161 | "kind": "snippet", 162 | "contents": "most-common " 163 | }, 164 | { 165 | "trigger": "next-instruction", 166 | "kind": "snippet", 167 | "contents": "next-instruction --limit ${1:LIMIT} --back --op0 ${2:OP0} --op1 ${3:OP1} --op2 ${4:OP2} --op3 ${5:OP3} --op4 ${6:OP4} --op5 ${7:OP5} ${8:mnem} ${9:mnem} ..." 168 | }, 169 | { 170 | "trigger": "offset", 171 | "kind": "snippet", 172 | "contents": "offset ${1:offset}" 173 | }, 174 | { 175 | "trigger": "operand", 176 | "kind": "snippet", 177 | "contents": "operand ${1:op}" 178 | }, 179 | { 180 | "trigger": "print", 181 | "kind": "snippet", 182 | "contents": "print ${1:phrase}" 183 | }, 184 | { 185 | "trigger": "python-if", 186 | "kind": "snippet", 187 | "contents": "python-if ${1:cond} ${2:label}" 188 | }, 189 | { 190 | "trigger": "run", 191 | "kind": "snippet", 192 | "contents": "run ${1:name}" 193 | }, 194 | { 195 | "trigger": "set-const", 196 | "kind": "snippet", 197 | "contents": "set-const ${1:name}" 198 | }, 199 | { 200 | "trigger": "set-enum", 201 | "kind": "snippet", 202 | "contents": "set-enum ${1:enum_name} ${2:enum_key}" 203 | }, 204 | { 205 | "trigger": "set-name", 206 | "kind": "snippet", 207 | "contents": "set-name ${1:name}" 208 | }, 209 | { 210 | "trigger": "set-struct-member", 211 | "kind": "snippet", 212 | "contents": "set-struct-member ${1:struct_name} ${2:member_name} ${3:member_type}" 213 | }, 214 | { 215 | "trigger": "set-type", 216 | "kind": "snippet", 217 | "contents": "set-type ${1:type_str}" 218 | }, 219 | { 220 | "trigger": "single", 221 | "kind": "snippet", 222 | "contents": "single ${1:index}" 223 | }, 224 | { 225 | "trigger": "sort", 226 | "kind": "snippet", 227 | "contents": "sort " 228 | }, 229 | { 230 | "trigger": "stop-if-empty", 231 | "kind": "snippet", 232 | "contents": "stop-if-empty " 233 | }, 234 | { 235 | "trigger": "store", 236 | "kind": "snippet", 237 | "contents": "store ${1:name}" 238 | }, 239 | { 240 | "trigger": "symdiff", 241 | "kind": "snippet", 242 | "contents": "symdiff ${1:variables} ${2:variables} ..." 243 | }, 244 | { 245 | "trigger": "trace", 246 | "kind": "snippet", 247 | "contents": "trace " 248 | }, 249 | { 250 | "trigger": "union", 251 | "kind": "snippet", 252 | "contents": "union --piped ${1:variables} ${2:variables} ..." 253 | }, 254 | { 255 | "trigger": "unique", 256 | "kind": "snippet", 257 | "contents": "unique " 258 | }, 259 | { 260 | "trigger": "verify-aligned", 261 | "kind": "snippet", 262 | "contents": "verify-aligned ${1:value}" 263 | }, 264 | { 265 | "trigger": "verify-bytes", 266 | "kind": "snippet", 267 | "contents": "verify-bytes ${1:hex_str}" 268 | }, 269 | { 270 | "trigger": "verify-name", 271 | "kind": "snippet", 272 | "contents": "verify-name ${1:name}" 273 | }, 274 | { 275 | "trigger": "verify-opcode", 276 | "kind": "snippet", 277 | "contents": "verify-opcode --op0 ${1:OP0} --op1 ${2:OP1} --op2 ${3:OP2} --op3 ${4:OP3} --op4 ${5:OP4} --op5 ${6:OP5} ${7:mnem} ${8:mnem} ..." 278 | }, 279 | { 280 | "trigger": "verify-operand", 281 | "kind": "snippet", 282 | "contents": "verify-operand --op0 ${1:OP0} --op1 ${2:OP1} --op2 ${3:OP2} ${4:name}" 283 | }, 284 | { 285 | "trigger": "verify-ref", 286 | "kind": "snippet", 287 | "contents": "verify-ref --code --data --name ${1:NAME}" 288 | }, 289 | { 290 | "trigger": "verify-segment", 291 | "kind": "snippet", 292 | "contents": "verify-segment --regex ${1:name}" 293 | }, 294 | { 295 | "trigger": "verify-single", 296 | "kind": "snippet", 297 | "contents": "verify-single " 298 | }, 299 | { 300 | "trigger": "verify-str", 301 | "kind": "snippet", 302 | "contents": "verify-str --null-terminated ${1:hex_str}" 303 | }, 304 | { 305 | "trigger": "xref", 306 | "kind": "snippet", 307 | "contents": "xref " 308 | }, 309 | { 310 | "trigger": "xrefs-to", 311 | "kind": "snippet", 312 | "contents": "xrefs-to --function-start --or --and --name ${1:NAME} --bytes ${2:BYTES}" 313 | } 314 | ] 315 | } -------------------------------------------------------------------------------- /fa/utils.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import inspect 3 | import os 4 | import warnings 5 | from typing import Generator, Iterable, List, Optional, Tuple, Union 6 | 7 | IDA_MODULE = False 8 | 9 | try: 10 | import ida_bytes 11 | import ida_ida 12 | import ida_idp 13 | import ida_ua 14 | import idaapi 15 | import idautils 16 | import idc 17 | 18 | IDA_MODULE = True 19 | except ImportError: 20 | pass 21 | 22 | MAX_NUMBER_OF_OPERANDS = 6 23 | 24 | 25 | def index_of(needle, haystack): 26 | try: 27 | return haystack.index(needle) 28 | except ValueError: 29 | return -1 30 | 31 | 32 | def find_raw(needle, segments=None): 33 | if segments is None: 34 | segments = dict() 35 | 36 | if IDA_MODULE: 37 | # ida optimization 38 | needle = bytearray(needle) 39 | payload = ' '.join(['{:02x}'.format(b) for b in needle]) 40 | for address in ida_find_all(payload): 41 | yield address 42 | return 43 | 44 | for segment_ea, data in segments.items(): 45 | offset = index_of(needle, data) 46 | extra_offset = 0 47 | 48 | while offset != -1: 49 | address = segment_ea + offset + extra_offset 50 | yield address 51 | 52 | extra_offset += offset + 1 53 | data = data[offset + 1:] 54 | 55 | offset = index_of(needle, data) 56 | 57 | 58 | def ida_find_all(payload: Union[bytes, bytearray, str]) -> Generator[int, None, None]: 59 | if float(idaapi.get_kernel_version()) < 9: 60 | ea = idc.find_binary(0, idc.SEARCH_DOWN | idc.SEARCH_REGEX, payload) 61 | while ea != idc.BADADDR: 62 | yield ea 63 | ea = idc.find_binary(ea + 1, idc.SEARCH_DOWN | idc.SEARCH_REGEX, payload) 64 | else: 65 | ea = ida_bytes.find_bytes(payload, 0) 66 | while ea != idc.BADADDR: 67 | yield ea 68 | ea = ida_bytes.find_bytes(payload, ea + 1) 69 | 70 | 71 | def read_memory(segments, ea, size): 72 | if IDA_MODULE: 73 | return idc.get_bytes(ea, size) 74 | 75 | for segment_ea, data in segments.items(): 76 | if (ea <= segment_ea + len(data)) and (ea >= segment_ea): 77 | offset = ea - segment_ea 78 | return data[offset:offset + size] 79 | 80 | 81 | def yield_unique(func): 82 | def wrapper(*args, **kwargs): 83 | results = set() 84 | for i in func(*args, **kwargs): 85 | if i not in results: 86 | yield i 87 | results.add(i) 88 | 89 | return wrapper 90 | 91 | 92 | class ArgumentParserNoExit(argparse.ArgumentParser): 93 | def error(self, message): 94 | raise ValueError(message) 95 | 96 | 97 | def deprecated(function): 98 | frame = inspect.stack()[1] 99 | module = inspect.getmodule(frame[0]) 100 | filename = module.__file__ 101 | command_name = os.path.splitext(os.path.basename(filename))[0] 102 | 103 | warnings.warn('command: "{}" is deperected and will be removed in ' 104 | 'the future.'.format(command_name, DeprecationWarning)) 105 | return function 106 | 107 | 108 | def add_struct_to_idb(name): 109 | idc.import_type(-1, name) 110 | 111 | 112 | def find_or_create_struct(name): 113 | sid = idc.get_struc_id(name) 114 | if sid == idc.BADADDR: 115 | sid = idc.add_struc(-1, name, 0) 116 | print("added struct \"{0}\", id: {1}".format(name, sid)) 117 | else: 118 | print("struct \"{0}\" already exists, id: ".format(name, sid)) 119 | 120 | add_struct_to_idb(name) 121 | 122 | return sid 123 | 124 | 125 | def create_regs_description(*regs) -> List[Tuple[int, str]]: 126 | result = [] 127 | for i, r in enumerate(regs): 128 | if r is not None: 129 | result.append((i, r)) 130 | return result 131 | 132 | 133 | def add_operand_args(parser: argparse.ArgumentParser) -> None: 134 | for op_ix in range(MAX_NUMBER_OF_OPERANDS): 135 | parser.add_argument(f'--op{op_ix}', default=None) 136 | 137 | 138 | def create_regs_description_from_args(args) -> List[Tuple[int, str]]: 139 | regs = [] 140 | for op_ix in range(MAX_NUMBER_OF_OPERANDS): 141 | v = getattr(args, f'op{op_ix}', None) 142 | if v is not None: 143 | v = [i.strip() for i in v.split(',')] 144 | regs.append(v) 145 | return create_regs_description(*regs) 146 | 147 | 148 | def size_of_operand(op: 'ida_ua.op_t') -> int: 149 | """ 150 | See https://reverseengineering.stackexchange.com/questions/19843/how-can-i-get-the-byte-size-of-an-operand-in-ida-pro 151 | """ 152 | tbyte = 8 153 | dt_ldbl = 8 154 | n_bytes = [1, 2, 4, 4, 8, 155 | tbyte, -1, 8, 16, -1, 156 | -1, 6, -1, 4, 4, 157 | dt_ldbl, 32, 64] 158 | return n_bytes[op.dtype] 159 | 160 | 161 | def get_operand_width(ea: int, index: int) -> int: 162 | """ 163 | See https://reverseengineering.stackexchange.com/questions/19843/how-can-i-get-the-byte-size-of-an-operand-in-ida-pro 164 | """ 165 | insn = idautils.DecodeInstruction(ea) 166 | return size_of_operand(insn.ops[index]) 167 | 168 | 169 | def resolve_expr(s: str, raise_on_failure: bool = True) -> Optional[int]: 170 | try: 171 | return int(s, 0) 172 | except ValueError: 173 | v = idc.get_name_ea_simple(s) 174 | if v == idc.BADADDR: 175 | if raise_on_failure: 176 | raise 177 | return None 178 | return v 179 | 180 | 181 | def is_arch_arm() -> bool: 182 | return ida_ida.getinf_str(ida_ida.INF_PROCNAME).lower().split('\x00', 1)[0] == 'arm' 183 | 184 | 185 | def get_reg_num(reg_name: str, raise_on_failure: bool = True) -> Optional[int]: 186 | ri = ida_idp.reg_info_t() 187 | status = ida_idp.parse_reg_name(ri, reg_name) 188 | if not status: 189 | if raise_on_failure: 190 | raise ValueError(f'invalid register name: {reg_name}') 191 | return None 192 | return ri.reg 193 | 194 | 195 | def parse_displacement_syntax(string: str) -> Tuple[Optional[int], Optional[int]]: 196 | split_plus = string.find('+') 197 | split_minus = string.find('-') 198 | if split_plus != -1 and split_minus != -1: 199 | raise ValueError(f'Invalid values format: "{string}" (both "+" and "-" signs found)') 200 | if split_minus != -1: 201 | parts = string.split('-') 202 | minus = True 203 | else: 204 | parts = string.split('+') 205 | minus = False 206 | 207 | if len(parts) > 2: 208 | raise ValueError(f'Invalid values format: "{string}" (too many values)') 209 | 210 | if len(parts) == 1: 211 | disp = 0 212 | else: 213 | displacement_str = parts[1].strip() 214 | if not displacement_str: 215 | disp = None 216 | else: 217 | disp = resolve_expr(displacement_str) 218 | if minus: 219 | disp = -disp 220 | 221 | reg_str = parts[0].strip() 222 | if not reg_str: 223 | reg_num = None 224 | else: 225 | reg_num = get_reg_num(reg_str) 226 | 227 | return reg_num, disp 228 | 229 | 230 | def compare_immediate_value(op_val: Optional[int], values: Iterable[str]) -> bool: 231 | return any(op_val == resolve_expr(v, raise_on_failure=False) for v in values) 232 | 233 | 234 | def compare_reg_value(op_val: Optional[int], values: Iterable[str]) -> bool: 235 | return any(op_val == get_reg_num(v, raise_on_failure=False) for v in values) 236 | 237 | 238 | def compare_cr_reg(op_val: Optional[int], values: Iterable[str]) -> bool: 239 | for v in values: 240 | if v.startswith('c'): 241 | try: 242 | n = int(v[1:]) 243 | except ValueError: 244 | continue 245 | if n == op_val: 246 | return True 247 | return False 248 | 249 | 250 | def compare_arm_coprocessor_operand(ea: int, index: int, values: Iterable[str]) -> bool: 251 | assert idc.get_operand_type(ea, 0) == idc.o_imm 252 | assert idc.get_operand_type(ea, 1) >= 8 # processor specific type 253 | assert idc.get_operand_type(ea, 2) == idc.o_imm 254 | 255 | if index == 0: 256 | return any(v.lower() == 'p15' for v in values) 257 | 258 | if index == 1: 259 | op_val = idc.get_operand_value(ea, 0) 260 | return compare_immediate_value(op_val, values) 261 | 262 | if index == 5: 263 | op_val = idc.get_operand_value(ea, 2) 264 | return compare_immediate_value(op_val, values) 265 | 266 | insn = idautils.DecodeInstruction(ea) 267 | operand = insn.ops[1] 268 | 269 | if index == 2: 270 | op_val = operand.reg 271 | return compare_reg_value(op_val, values) 272 | 273 | if index == 3: 274 | op_val = operand.specflag1 275 | return compare_cr_reg(op_val, values) 276 | 277 | if index == 4: 278 | op_val = operand.specflag2 279 | return compare_cr_reg(op_val, values) 280 | 281 | raise ValueError(f'Unrecognized index {index} for "MCR" or "MRC" opcode') 282 | 283 | 284 | def compare_operand_arm(ea: int, index: int, values: Iterable[str]) -> bool: 285 | """ 286 | Compare a list of values to the operand in the given index, at the given address. 287 | Supports various formats, including: 288 | 289 | 0x00000000: LDR R1, [SP, #0x34] 290 | 0x00000004: ADD, R2, SP, #0x2C 291 | 292 | > add 0 293 | > verify-opcode ldr --op0 r1 294 | > verify-opcode ldr --op1 +0x34 295 | > verify-opcode ldr --op1 sp+ 296 | > verify-opcode ldr --op1 sp+52 297 | 298 | > offset 4 299 | > verify-opcode add --op2 0x2c 300 | 301 | Note that the following syntax 302 | > verify-opcode ldr --op1 sp 303 | implies the displacement must be 0 (or non-existent), whereas sp+ implies that the displacement is unimportant. 304 | """ 305 | insn = idautils.DecodeInstruction(ea) 306 | operand = insn.ops[index] 307 | op_type = operand.type 308 | op_val = idc.get_operand_value(ea, index) 309 | op_width = size_of_operand(operand) 310 | 311 | mnem = insn.get_canon_mnem() 312 | 313 | if mnem.lower() in ('mcr', 'mrc'): 314 | return compare_arm_coprocessor_operand(ea, index, values) 315 | 316 | if op_type == idc.o_void: 317 | return False 318 | 319 | if op_type == idc.o_imm: 320 | return compare_immediate_value(op_val, values) 321 | 322 | if op_type == idc.o_reg: 323 | return compare_reg_value(op_val, values) 324 | 325 | if op_type == idc.o_mem: 326 | for v in values: 327 | comp = op_val 328 | if v.startswith('='): 329 | v = v[1:] 330 | bs = idc.get_bytes(op_val, op_width) 331 | comp = int.from_bytes(bs, 'little') 332 | expected = resolve_expr(v) 333 | if comp == expected: 334 | return True 335 | return False 336 | 337 | if op_type == idc.o_displ: 338 | for v in values: 339 | try: 340 | reg_num, disp = parse_displacement_syntax(v) 341 | except ValueError as e: 342 | message = str(e) 343 | print(f'{message}, skipping...') 344 | continue 345 | 346 | found = True 347 | if reg_num is not None and reg_num != operand.reg: 348 | found = False 349 | if disp is not None and disp != op_val: 350 | found = False 351 | if found: 352 | return True 353 | return False 354 | 355 | print(f'Unknown op_type 0x{op_type:x} @ ea 0x{ea:x}, skipping...') 356 | return False 357 | 358 | 359 | def compare_operand(ea: int, index: int, values: Iterable[str]) -> bool: 360 | # First handle specialized cases 361 | if is_arch_arm(): 362 | return compare_operand_arm(ea, index, values) 363 | 364 | # Default logic 365 | return idc.get_operand_value(ea, index) in values 366 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python application](https://github.com/doronz88/fa/workflows/Python%20application/badge.svg) 2 | 3 | # FA 4 | 5 | ## What is it? 6 | 7 | FA stands for Firmware Analysis and intended **For Humans**. 8 | 9 | FA allows one to easily perform code exploration, symbol searching and 10 | other functionality with ease. 11 | 12 | Usually such tasks would require you to understand complicated APIs, 13 | write machine-dependant code and perform other tedious tasks. 14 | FA is meant to replace the steps one usually performs like a robot 15 | (find X string, goto xref, find the next call function, ...) in 16 | a much friendlier and maintainable manner. 17 | 18 | The current codebase is very IDA-plugin-oriented. In the future I'll 19 | consider adding compatibility for other disassemblers such as: 20 | Ghidra, Radare and etc... 21 | 22 | 23 | Pull Requests are of course more than welcome :smirk:. 24 | 25 | ## Requirements 26 | 27 | Supported IDA 8.0+. 28 | 29 | In your IDA's python directory, run: 30 | 31 | ```sh 32 | python -m pip install -r requirements.txt 33 | ``` 34 | 35 | And for testing: 36 | ```sh 37 | python -m pip install -r requirements_testing.txt 38 | ``` 39 | 40 | ## Where to start? 41 | 42 | Before using, you should understand the terminology for: 43 | Projects, SIG files and Loaders. 44 | 45 | So, grab a cup of coffee, listen to some [nice music](https://www.youtube.com/watch?v=5rrIx7yrxwQ), and please devote 46 | a few minutes of your time into reading this README. 47 | 48 | ### Projects 49 | 50 | The "project" is kind of the namespace for different signatures. 51 | For example, either: linux, linux_x86, linux_arm etc... are good 52 | project names that can be specified if you are working on either 53 | platforms. 54 | 55 | By dividing the signatures into such projects, Windows symbols for 56 | example won't be searched for Linux projects, which will result 57 | in a better directory organization layout, better performance and 58 | less rate for false-positives. 59 | 60 | The signatures are located by default in the `signatures` directory. 61 | If you wish to use a different location, you may create `config.ini` 62 | at FA's root with the following contents: 63 | 64 | ```ini 65 | [global] 66 | signatures_root = /a/b/c 67 | ``` 68 | 69 | ### SIG format 70 | 71 | The SIG format is a core feature of FA regarding symbol searching. Each 72 | SIG file is residing within the project directory and is automatically searched 73 | when requested to generate the project's symbol list. 74 | 75 | The format is Hjson-based and is used to describe what you, 76 | **as a human**, would do in order to perform the given task (symbol searching 77 | or binary exploration). 78 | 79 | SIG syntax (single): 80 | ```hjson 81 | { 82 | name: name 83 | instructions : [ 84 | # Available commands are listed below 85 | command1 86 | command2 87 | ] 88 | } 89 | ``` 90 | 91 | Each line in the `instructions` list behaves like a shell 92 | command-line that gets the previous results as the input 93 | and outputs the next results 94 | to the next line. 95 | 96 | Confused? That's alright :grinning:. [Just look at the examples below](#examples) 97 | 98 | User may also use his own python script files to perform 99 | the search. Just create a new `.py` file in your project 100 | directory and implement the `run(interpreter)` method. 101 | Also, the project's path is appended to python's `sys.path` 102 | so you may import your scripts from one another. 103 | 104 | To view the list of available commands, [view the list below](#available-commands) 105 | 106 | ### Examples 107 | 108 | #### Finding a global struct 109 | 110 | ```hjson 111 | { 112 | name: g_awsome_global, 113 | instructions: [ 114 | # find the byte sequence '11 22 33 44' 115 | find-bytes '11 22 33 44' 116 | 117 | # advance offset by 20 118 | offset 20 119 | 120 | # verify the current bytes are 'aa bb cc dd' 121 | verify-bytes 'aa bb cc dd' 122 | 123 | # go back by 20 bytes offset 124 | offset -20 125 | 126 | # set global name 127 | set-name g_awsome_global 128 | ] 129 | } 130 | ``` 131 | 132 | #### Find function by reference to string 133 | 134 | ```hjson 135 | { 136 | name: free 137 | instructions: [ 138 | # search the string "free" 139 | find-str 'free' --null-terminated 140 | 141 | # goto xref 142 | xref 143 | 144 | # goto function's prolog 145 | function-start 146 | 147 | # reduce to the singletone with most xrefs to 148 | max-xrefs 149 | 150 | # set name and type 151 | set-name free 152 | set-type 'void free(void *block)' 153 | ] 154 | } 155 | ``` 156 | 157 | #### Performing code exploration 158 | 159 | ```hjson 160 | { 161 | name: arm-explorer 162 | instructions: [ 163 | # search for some potential function prologs 164 | arm-find-all 'push {r4, lr}' 165 | arm-find-all 'push {r4, r5, lr}' 166 | thumb-find-all 'push {r4, lr}' 167 | thumb-find-all 'push {r4, r5, lr}' 168 | 169 | # convert into functions 170 | make-function 171 | ] 172 | } 173 | ``` 174 | 175 | #### Performing string exploration 176 | 177 | ```hjson 178 | { 179 | name: arm-string-explorer 180 | instructions: [ 181 | # goto printf 182 | locate printf 183 | 184 | # iterate every xref 185 | xref 186 | 187 | # and for each, go word-word backwards 188 | add-offset-range 0 -40 -4 189 | 190 | # if ldr to r0 191 | verify-operand ldr --op0 r0 192 | 193 | # go to the global string 194 | goto-ref --data 195 | 196 | # and make it literal 197 | make-literal 198 | ] 199 | } 200 | ``` 201 | 202 | #### Finding enums and constants 203 | 204 | ```hjson 205 | { 206 | name: consts-finder 207 | instructions: [ 208 | # goto printf 209 | locate printf 210 | 211 | # iterate all its function lines 212 | function-lines 213 | 214 | # save this result 215 | store printf-lines 216 | 217 | # look for: li r7, ??? 218 | verify-operand li --op0 7 219 | 220 | # extract second operand 221 | operand 1 222 | 223 | # define the constant 224 | set-const IMPORTANT_OFFSET 225 | 226 | # load previous results 227 | load printf-lines 228 | 229 | # look for: li r7, ??? 230 | verify-operand li --op0 8 231 | 232 | # get second operand 233 | operand 1 234 | 235 | # set this enum value 236 | set-enum important_enum_t some_enum_key 237 | ] 238 | } 239 | ``` 240 | 241 | #### Adding struct member offsets 242 | 243 | ```hjson 244 | { 245 | name: structs-finder 246 | instructions: [ 247 | # add hard-coded '0' into resultset 248 | add 0 249 | 250 | # add a first member at offset 0 251 | set-struct-member struct_t member_at_0 'unsigned int' 252 | 253 | # advance offset by 4 254 | offset 4 255 | 256 | # add a second member 257 | set-struct-member struct_t member_at_4 'const char *' 258 | 259 | # goto function printf 260 | locate printf 261 | 262 | # iterate its function lines 263 | function-lines 264 | 265 | # look for the specific mov opcode (MOV R8, ???) 266 | verify-operand mov --op0 8 267 | 268 | # extract the offset 269 | operand 1 270 | 271 | # define this offset into the struct 272 | set-struct-member struct_t member_at_r8_offset 'const char *' 273 | ] 274 | } 275 | ``` 276 | 277 | #### Finding several functions in a row 278 | 279 | ```hjson 280 | { 281 | name: cool_functions 282 | instructions: [ 283 | # find string 284 | find-str 'init_stuff' --null-terminated 285 | 286 | # goto to xref 287 | xref 288 | 289 | # goto function start 290 | function-start 291 | 292 | # verify only one single result 293 | unique 294 | 295 | # iterating every 4-byte opcode 296 | add-offset-range 0 80 4 297 | 298 | # if mnemonic is bl 299 | verify-operand bl 300 | 301 | # sort results 302 | sort 303 | 304 | # store resultset in 'BLs' 305 | store BLs 306 | 307 | # set first bl to malloc function 308 | single 0 309 | goto-ref --code 310 | set-name malloc 311 | set-type 'void *malloc(unsigned int size)' 312 | 313 | # go back to the results from 4 commands ago 314 | # (the sort results) 315 | load BLs 316 | 317 | # rename next symbol :) 318 | single 1 319 | goto-ref --code 320 | set-name free 321 | set-type 'void free(void *block)' 322 | ] 323 | } 324 | ``` 325 | 326 | #### Conditional branches 327 | 328 | ```hjson 329 | { 330 | name: set_opcode_const 331 | instructions: [ 332 | # goto printf function 333 | locate printf 334 | 335 | # goto 'case_opcode_bl' if current opcode is bl 336 | if 'verify-operand bl' case_opcode_bl 337 | 338 | # make: #define is_bl (0) 339 | clear 340 | add 0 341 | set-const is_bl 342 | 343 | # finish script by jumping to end 344 | b end 345 | 346 | # mark as 'case_opcode_bl' label 347 | label case_opcode_bl 348 | 349 | # make: #define is_bl (1) 350 | clear 351 | add 1 352 | set-const is_bl 353 | 354 | # mark script end 355 | label end 356 | ] 357 | } 358 | ``` 359 | 360 | #### Python script to find a list of symbols 361 | 362 | ```python 363 | from fa.commands.find_str import find_str 364 | from fa.commands.set_name import set_name 365 | from fa import context 366 | 367 | def run(interpreter): 368 | # throw an exception if not running within ida context 369 | context.verify_ida('script-name') 370 | 371 | # locate the global string 372 | set_name(find_str('hello world', null_terminated=True), 373 | 'g_hello_world', interpreter) 374 | ``` 375 | 376 | #### Python script to automate SIG files interpreter 377 | 378 | ```python 379 | TEMPLATE = ''' 380 | find-str '{unique_string}' 381 | xref 382 | function-start 383 | unique 384 | set-name '{function_name}' 385 | ''' 386 | 387 | def run(interpreter): 388 | for function_name in ['func1', 'func2', 'func3']: 389 | instructions = TEMPLATE.format(unique_string=function_name, 390 | function_name=function_name).split('\n') 391 | 392 | interpreter.find_from_instructions_list(instructions) 393 | ``` 394 | 395 | #### Python script to dynamically add structs 396 | 397 | ```python 398 | from fa.commands.set_type import set_type 399 | from fa import fa_types 400 | 401 | TEMPLATE = ''' 402 | find-str '{unique_string}' 403 | xref 404 | ''' 405 | 406 | def run(interpreter): 407 | fa_types.add_const('CONST7', 7) 408 | fa_types.add_const('CONST8', 8) 409 | 410 | foo_e = fa_types.FaEnum('foo_e') 411 | foo_e.add_value('val2', 2) 412 | foo_e.add_value('val1', 1) 413 | foo_e.update_idb() 414 | 415 | special_struct_t = fa_types.FaStruct('special_struct_t') 416 | special_struct_t.add_field('member1', 'const char *') 417 | special_struct_t.add_field('member2', 'const char *', offset=0x20) 418 | special_struct_t.update_idb() 419 | 420 | for function_name in ['unique_magic1', 'unique_magic2']: 421 | instructions = TEMPLATE.format(unique_string=function_name, 422 | function_name=function_name).split('\n') 423 | 424 | results = interpreter.find_from_instructions_list(instructions) 425 | for ea in results: 426 | # the set_type can receive either a string, FaStruct 427 | # or FaEnum :-) 428 | set_type(ea, special_struct_t) 429 | ``` 430 | 431 | ### Aliases 432 | 433 | Each command can be "alias"ed using the file 434 | found in `fa/commands/alias` or in `/alias` 435 | 436 | Syntax for each line is as follows: `alias_command = command` 437 | For example: 438 | ``` 439 | ppc32-verify = keystone-verify-opcodes --bele KS_ARCH_PPC KS_MODE_PPC32 440 | ``` 441 | 442 | Project aliases have higher priority. 443 | 444 | ### Loaders 445 | 446 | Loaders are the entry point into running FA. 447 | In the future we'll possibly add Ghidra and other tools. 448 | 449 | Please first install the package as follows: 450 | 451 | Clone the repository and install locally: 452 | 453 | ```sh 454 | # clone 455 | git clone git@github.com:doronz88/fa.git 456 | cd fa 457 | 458 | # install 459 | python -m pip install -e . 460 | ``` 461 | 462 | #### IDA 463 | 464 | Within IDA Python run: 465 | 466 | ```python 467 | from fa import ida_plugin 468 | ida_plugin.install() 469 | ``` 470 | 471 | You should get a nice prompt inside the output window welcoming you 472 | into using FA. Also, a quick usage guide will also be printed so you 473 | don't have to memorize everything. 474 | 475 | Also, an additional `FA Toolbar` will be added with quick functions that 476 | are also available under the newly created `FA` menu. 477 | 478 | ![FA Menu](https://github.com/doronz88/fa/raw/master/fa/res/screenshots/menu.png "FA Menu") 479 | 480 | A QuickStart Tip: 481 | 482 | `Ctrl+6` to select your project, then `Ctrl+7` to find all its defined symbols. 483 | 484 | 485 | You can also run IDA in script mode just to extract symbols using: 486 | 487 | ```sh 488 | ida -S"fa/ida_plugin.py --project-name --symbols-file=/tmp/symbols.txt" foo.idb 489 | ``` 490 | 491 | 492 | #### ELF 493 | 494 | In order to use FA on a RAW ELF file, simply use the following command-line: 495 | 496 | ```sh 497 | python elf_loader.py 498 | ``` 499 | 500 | ### Available commands 501 | 502 | See [commands.md](commands.md) 503 | 504 | -------------------------------------------------------------------------------- /fa/fainterp.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import importlib.util 3 | import os 4 | import re 5 | import shlex 6 | import sys 7 | import time 8 | from abc import ABCMeta, abstractmethod 9 | from collections import OrderedDict 10 | from configparser import ConfigParser 11 | from tkinter import Tk, ttk 12 | 13 | import hjson 14 | 15 | from fa.utils import ArgumentParserNoExit 16 | 17 | CONFIG_PATH = os.path.join( 18 | os.path.dirname(os.path.abspath(__file__)), '..', 'config.ini') 19 | DEFAULT_SIGNATURES_ROOT = os.path.join( 20 | os.path.dirname(os.path.abspath(__file__)), 'signatures') 21 | COMMANDS_ROOT = os.path.join( 22 | os.path.dirname(os.path.abspath(__file__)), 'commands') 23 | 24 | 25 | class InterpreterState: 26 | def __init__(self): 27 | self.pc = 0 28 | self.variables = {} 29 | self.labels = {} 30 | 31 | 32 | class FaInterp: 33 | """ 34 | FA Interpreter base class 35 | """ 36 | __metaclass__ = ABCMeta 37 | 38 | def __init__(self, config_path=CONFIG_PATH): 39 | """ 40 | Constructor 41 | :param config_path: config.ini path. used to load global settings 42 | instead of setting each of the options manually 43 | (signatures_root, project, ...) 44 | """ 45 | self._project = None 46 | self._input = None 47 | self._segments = OrderedDict() 48 | self._signatures_root = DEFAULT_SIGNATURES_ROOT 49 | self._symbols = {} 50 | self._consts = {} 51 | self.history = [] 52 | self.endianity = '<' 53 | self._config_path = config_path 54 | self.stack = [] 55 | self._sig_cache = [] 56 | self.implicit_use_sig_cache = False 57 | 58 | if (config_path is not None) and (os.path.exists(config_path)): 59 | self._signatures_root = os.path.expanduser( 60 | self.config_get('global', 'signatures_root')) 61 | self._project = self.config_get('global', 'project', None) 62 | 63 | def _push_stack_frame(self): 64 | self.stack.append(InterpreterState()) 65 | 66 | def _pop_stack_frame(self): 67 | self.stack.pop() 68 | 69 | def set_labels(self, labels): 70 | self.stack[-1].labels = labels 71 | 72 | def get_pc(self): 73 | return self.stack[-1].pc 74 | 75 | def set_pc(self, pc): 76 | if isinstance(pc, int): 77 | self.stack[-1].pc = pc 78 | elif isinstance(pc, str): 79 | self.stack[-1].pc = self.stack[-1].labels[pc] 80 | else: 81 | raise KeyError('invalid pc: {}'.format(pc)) 82 | 83 | def dec_pc(self): 84 | self.stack[-1].pc -= 1 85 | 86 | def inc_pc(self): 87 | self.stack[-1].pc += 1 88 | 89 | def set_variable(self, name, value): 90 | self.stack[-1].variables[name] = value 91 | 92 | def get_variable(self, name): 93 | return self.stack[-1].variables[name] 94 | 95 | def get_all_variables(self): 96 | return self.stack[-1].variables 97 | 98 | @abstractmethod 99 | def set_input(self, input_): 100 | """ 101 | Set file input 102 | :param input_: file to work on 103 | :return: 104 | """ 105 | pass 106 | 107 | def config_get(self, section, key, default=None): 108 | """ 109 | Read configuration setting. This is loaded from INI config file. 110 | :param section: section name 111 | :param key: key name 112 | :param default: default value, if key doesn't exist inside section 113 | :return: the value in the specified section-key 114 | """ 115 | if not os.path.exists(self._config_path): 116 | return default 117 | 118 | config = ConfigParser() 119 | 120 | with open(self._config_path) as f: 121 | config.read_file(f) 122 | 123 | if not config.has_section(section) or \ 124 | not config.has_option(section, key): 125 | return default 126 | 127 | return config.get(section, key) 128 | 129 | def config_set(self, section, key, value): 130 | """ 131 | Write configuration setting. This is saved into INI config file 132 | :param section: section name 133 | :param key: key name 134 | :param value: value to set 135 | :return: None 136 | """ 137 | config = ConfigParser() 138 | 139 | if sys.version[0] == '2': 140 | section = section.decode('utf8') 141 | key = key.decode('utf8') 142 | value = value.decode('utf8') 143 | 144 | if os.path.exists(self._config_path): 145 | config.read(self._config_path) 146 | 147 | if not config.has_section(section): 148 | config.add_section(section) 149 | 150 | config.set(section, key, value) 151 | 152 | with open(self._config_path, 'w') as f: 153 | config.write(f) 154 | 155 | def set_signatures_root(self, path, save=False): 156 | """ 157 | Change signatures root path (where the projects are searched). 158 | :param path: signatures root path (where the projects are searched). 159 | :param save: should save into configuration file? 160 | :return: None 161 | """ 162 | self.log('signatures root: {}'.format(path)) 163 | self._signatures_root = path 164 | 165 | if save: 166 | self.config_set('global', 'signatures_root', path) 167 | 168 | def verify_project(self): 169 | """ 170 | Throws IOError if no project has been selected or points into an 171 | invalid path 172 | :return: None 173 | """ 174 | if self._project is None: 175 | raise IOError('No project has been selected') 176 | 177 | if not os.path.exists(os.path.join(self._signatures_root, 178 | self._project)): 179 | raise IOError("Selected project's path doesn't exist.\n" 180 | "Please re-select)") 181 | 182 | def set_project(self, project, save=True): 183 | """ 184 | Set currently active project (where SIG files are placed) 185 | :param project: project name 186 | :param save: should save this setting into configuration file? 187 | :return: None 188 | """ 189 | self._project = project 190 | self.log('project set: {}'.format(project)) 191 | 192 | self.set_signatures_root(self._signatures_root, save=save) 193 | if save: 194 | self.config_set('global', 'project', project) 195 | 196 | def symbols(self): 197 | """ 198 | Run find for all SIG files in currently active project 199 | :return: dictionary of found symbols 200 | """ 201 | try: 202 | self._sig_cache = [] 203 | self.implicit_use_sig_cache = True 204 | self.get_python_symbols() 205 | 206 | for sig in self.get_json_signatures(): 207 | self.find(sig['name'], use_cache=True) 208 | finally: 209 | self._sig_cache = [] 210 | self.implicit_use_sig_cache = False 211 | 212 | return self._symbols 213 | 214 | def interactive_set_project(self): 215 | """ 216 | Show GUI for selecting a project from signatures_root 217 | :return: None 218 | """ 219 | app = Tk() 220 | # app.geometry('200x30') 221 | 222 | label = ttk.Label(app, 223 | text="Choose current project") 224 | label.grid(column=0, row=0) 225 | 226 | combo = ttk.Combobox(app, 227 | values=self.list_projects()) 228 | combo.grid(column=0, row=1) 229 | 230 | def combobox_change_project(event): 231 | self.set_project(combo.get()) 232 | 233 | combo.bind("<>", combobox_change_project) 234 | 235 | app.mainloop() 236 | 237 | def list_projects(self): 238 | """ 239 | Get a list of all available projects in signatures_root 240 | :return: list of all available projects in signatures_root 241 | """ 242 | projects = [] 243 | for root, dirs, files in os.walk(self._signatures_root): 244 | projects += \ 245 | [os.path.relpath(os.path.join(root, filename), 246 | self._signatures_root) for filename in dirs] 247 | return [str(p) for p in projects if p[0] != '.'] 248 | 249 | @staticmethod 250 | def log(message): 251 | """ 252 | Log message 253 | :param message: 254 | :return: 255 | """ 256 | for line in str(message).splitlines(): 257 | print('FA> {}'.format(line)) 258 | 259 | @abstractmethod 260 | def reload_segments(self): 261 | """ 262 | Reload memory segments 263 | :return: 264 | """ 265 | pass 266 | 267 | @staticmethod 268 | def get_module(name, filename): 269 | """ 270 | Load a python module by filename 271 | :param name: module name 272 | :param filename: module filename 273 | :return: loaded python module 274 | """ 275 | if not os.path.exists(filename): 276 | raise NotImplementedError("no such filename: {}".format(filename)) 277 | 278 | spec = importlib.util.spec_from_file_location(name, filename) 279 | module = importlib.util.module_from_spec(spec) 280 | spec.loader.exec_module(module) 281 | 282 | return module 283 | 284 | @staticmethod 285 | def get_command(command): 286 | """ 287 | Get fa command as a loaded python-module 288 | :param command: command name 289 | :return: command's python-module 290 | """ 291 | filename = os.path.join(COMMANDS_ROOT, "{}.py".format(command)) 292 | return FaInterp.get_module(command, filename) 293 | 294 | def run_command(self, command, addresses): 295 | """ 296 | Run fa command with given address list and output the result 297 | :param command: fa command name 298 | :param addresses: input address list 299 | :return: output address list 300 | """ 301 | args = '' 302 | if ' ' in command: 303 | command, args = command.split(' ', 1) 304 | args = shlex.split(args) 305 | 306 | command = command.replace('-', '_') 307 | 308 | module = self.get_command(command) 309 | p = module.get_parser() 310 | args = p.parse_args(args) 311 | return module.run(self._segments, args, addresses, 312 | interpreter=self) 313 | 314 | def get_alias(self): 315 | """ 316 | Get dictionary of all defined aliases globally and by project. 317 | Project aliases loaded last so are considered stronger. 318 | :return: dictionary of all fa command aliases 319 | """ 320 | retval = {} 321 | 322 | with open(os.path.join(COMMANDS_ROOT, 'alias')) as f: 323 | for line in f.readlines(): 324 | line = line.strip() 325 | k, v = line.split('=') 326 | retval[k.strip()] = v.strip() 327 | 328 | if self._project: 329 | # include also project alias 330 | project_root = os.path.join(self._signatures_root, self._project) 331 | project_alias_filename = os.path.join(project_root, 'alias') 332 | if os.path.exists(project_alias_filename): 333 | with open(project_alias_filename) as f: 334 | for line in f.readlines(): 335 | line = line.strip() 336 | k, v = line.split('=') 337 | retval[k.strip()] = v.strip() 338 | 339 | return retval 340 | 341 | def save_signature(self, filename): 342 | """ 343 | Save given signature object (by dictionary) into active project 344 | as a new SIG file. If symbol name already exists, then create another 345 | file (never overwrites). 346 | :param filename: Dictionary of signature object 347 | :return: None 348 | """ 349 | with open(filename) as f: 350 | sig = hjson.load(f) 351 | f.seek(0) 352 | sig_text = f.read() 353 | 354 | filename = os.path.join( 355 | self._signatures_root, 356 | self._project, 357 | sig['name'] + '.sig') 358 | i = 1 359 | while os.path.exists(filename): 360 | filename = os.path.join(self._signatures_root, self._project, 361 | sig['name'] + '.{}.sig'.format(i)) 362 | i += 1 363 | 364 | with open(filename, 'w') as f: 365 | f.write(sig_text) 366 | 367 | def _get_labeled_instructions(self, instructions): 368 | labels = {} 369 | processed_instructions = [] 370 | 371 | label_parser = ArgumentParserNoExit('label') 372 | label_parser.add_argument('name') 373 | 374 | alias_items = self.get_alias().items() 375 | 376 | pc = 0 377 | for line in instructions: 378 | line = line.strip() 379 | 380 | if len(line) == 0: 381 | continue 382 | 383 | if line.startswith('#'): 384 | # treat as comment 385 | continue 386 | 387 | if line.startswith('label '): 388 | args = label_parser.parse_args(shlex.split(line)[1:]) 389 | labels[args.name] = pc 390 | continue 391 | 392 | for alias, replacement in alias_items: 393 | # handle aliases 394 | pattern = r'^\b' + re.escape(alias) + r'\b' 395 | line = re.sub(pattern, replacement, line) 396 | 397 | processed_instructions.append(line) 398 | pc += 1 399 | 400 | return labels, processed_instructions 401 | 402 | def find_from_instructions_list(self, instructions, addresses=None): 403 | """ 404 | Run the given instruction list and output the result 405 | :param instructions: instruction list 406 | :param addresses: input address list (if any) 407 | :return: output address list 408 | """ 409 | if addresses is None: 410 | addresses = [] 411 | 412 | self._push_stack_frame() 413 | 414 | labels, instructions = self._get_labeled_instructions(instructions) 415 | 416 | self.set_labels(labels) 417 | 418 | n = len(instructions) 419 | 420 | while self.get_pc() < n: 421 | line = instructions[self.get_pc()] 422 | 423 | new_addresses = [] 424 | try: 425 | new_addresses = self.run_command(line, addresses) 426 | except ImportError as m: 427 | FaInterp.log('failed to run: {}. error: {}' 428 | .format(line, str(m))) 429 | 430 | addresses = new_addresses 431 | self.inc_pc() 432 | 433 | self._pop_stack_frame() 434 | return addresses 435 | 436 | def find_from_sig_json(self, signature_json, decremental=False): 437 | """ 438 | Find a signature from a signature JSON data. 439 | :param dict signature_json: Data of signature's JSON. 440 | :param bool decremental: 441 | :return: Addresses of matching signatures. 442 | :rtype: result list of last returns instruction 443 | """ 444 | self.log('interpreting SIG for: {}'.format(signature_json['name'])) 445 | start = time.time() 446 | retval = self.find_from_instructions_list( 447 | signature_json['instructions']) 448 | self.log('interpretation took: {}s'.format(time.time() - start)) 449 | return retval 450 | 451 | def find_from_sig_path(self, signature_path, decremental=False): 452 | """ 453 | Find a signature from a signature file path. 454 | :param str signature_path: Path to a signature file. 455 | :param bool decremental: 456 | :return: Addresses of matching signatures. 457 | :rtype: result list of last returns instruction 458 | """ 459 | local_path = os.path.join( 460 | self._signatures_root, self._project, signature_path) 461 | if os.path.exists(local_path): 462 | # prefer local signatures, then external 463 | signature_path = local_path 464 | 465 | with open(signature_path) as f: 466 | sig = hjson.load(f) 467 | return self.find_from_sig_json(sig, decremental) 468 | 469 | def get_python_symbols(self, file_name=None): 470 | """ 471 | Run all python scripts found in currently active project and return 472 | the dictionary of all found symbols 473 | :param file_name: optional, specify which python script to execute 474 | inside the currently active project 475 | :return: dictionary of all found symbols 476 | """ 477 | project_root = os.path.join(self._signatures_root, self._project) 478 | sys.path.append(project_root) 479 | 480 | for root, dirs, files in os.walk(project_root): 481 | for filename in sorted(files): 482 | if not filename.lower().endswith('.py'): 483 | continue 484 | 485 | if not file_name or file_name == filename: 486 | name = os.path.splitext(filename)[0] 487 | filename = os.path.join(root, filename) 488 | m = FaInterp.get_module(name, filename) 489 | if not hasattr(m, 'run'): 490 | self.log('skipping: {}'.format(filename)) 491 | else: 492 | m.run(self) 493 | 494 | def get_json_signatures(self, symbol_name=None): 495 | """ 496 | Get a list of all json SIG objects in currently active project. 497 | :param symbol_name: optional, select a specific SIG file by symbol name 498 | :return: list of all json SIG objects in currently active project. 499 | """ 500 | signatures = [] 501 | project_root = os.path.join(self._signatures_root, self._project) 502 | 503 | for root, dirs, files in os.walk(project_root): 504 | for filename in sorted(files): 505 | if not filename.lower().endswith('.sig'): 506 | continue 507 | 508 | filename = os.path.join(project_root, filename) 509 | 510 | with open(filename) as f: 511 | try: 512 | signature = hjson.load(f) 513 | except ValueError as e: 514 | self.log('error in json: {}'.format(filename)) 515 | raise e 516 | 517 | if (symbol_name is None) or (signature['name'] == symbol_name): 518 | signatures.append(signature) 519 | 520 | return signatures 521 | 522 | def set_const(self, name, value): 523 | self._consts[name] = value 524 | 525 | def set_symbol(self, symbol_name, value): 526 | self._symbols[symbol_name] = value 527 | 528 | def get_consts(self): 529 | return self._consts 530 | 531 | def find(self, symbol_name, use_cache=False): 532 | """ 533 | Find symbol by its name in the SIG file 534 | :param symbol_name: symbol name 535 | :return: list of matches for the given symbol 536 | """ 537 | if use_cache and symbol_name in self._sig_cache: 538 | return 539 | 540 | results = [] 541 | signatures = self.get_json_signatures(symbol_name) 542 | if len(signatures) == 0: 543 | raise NotImplementedError('no signature found for: {}' 544 | .format(symbol_name)) 545 | 546 | for sig in signatures: 547 | sig_results = self.find_from_sig_json(sig) 548 | 549 | if isinstance(sig_results, dict): 550 | if symbol_name in sig_results: 551 | results += sig_results[symbol_name] 552 | else: 553 | results += sig_results 554 | 555 | if use_cache and symbol_name not in self._sig_cache: 556 | self._sig_cache.append(symbol_name) 557 | 558 | return list(set(results)) 559 | -------------------------------------------------------------------------------- /fa/ida_plugin.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import os 3 | import re 4 | import subprocess 5 | import sys 6 | import tempfile 7 | import traceback 8 | from collections import namedtuple 9 | 10 | import rpyc 11 | from rpyc import OneShotServer 12 | 13 | sys.path.append('.') # noqa: E402 14 | 15 | import click 16 | import hjson 17 | import ida_auto 18 | import ida_bytes 19 | import ida_ida 20 | import ida_kernwin 21 | import ida_pro 22 | import ida_segment 23 | import ida_typeinf 24 | import idaapi 25 | import idautils 26 | import idc 27 | from ida_kernwin import Form 28 | 29 | from fa import fa_types, fainterp 30 | 31 | # Filename for the temporary created signature 32 | TEMP_SIG_FILENAME = os.path.join(tempfile.gettempdir(), 'fa_tmp_sig.sig') 33 | 34 | # IDA fa plugin filename 35 | PLUGIN_FILENAME = 'fa_ida_plugin.py' 36 | 37 | 38 | def open_file(filename): 39 | """ 40 | Attempt to open the given filename by OS' default editor 41 | :param filename: filename to open 42 | :return: None 43 | """ 44 | if sys.platform == "win32": 45 | try: 46 | os.startfile(filename) 47 | except Exception as error_code: 48 | if error_code[0] == 1155: 49 | os.spawnl(os.P_NOWAIT, 50 | os.path.join(os.environ['WINDIR'], 51 | 'system32', 'Rundll32.exe'), 52 | 'Rundll32.exe SHELL32.DLL, OpenAs_RunDLL {}' 53 | .format(filename)) 54 | else: 55 | print("other error") 56 | else: 57 | opener = "open" if sys.platform == "darwin" else "xdg-open" 58 | subprocess.call([opener, filename]) 59 | 60 | 61 | class IdaLoader(fainterp.FaInterp): 62 | """ 63 | IDA loader 64 | Includes improved GUI interaction for accessing the interpreter 65 | functionality. 66 | """ 67 | 68 | def __init__(self): 69 | super(IdaLoader, self).__init__() 70 | self._create_template_symbol = eval(self.config_get( 71 | 'global', 72 | 'create_symbol_template', 73 | 'False' 74 | )) 75 | 76 | def set_symbol_template(self, status): 77 | """ 78 | Should the create-temp-signature feature attempt to create a default 79 | signature by predefined template? 80 | :param status: new boolean setting 81 | :return: None 82 | """ 83 | self._create_template_symbol = status 84 | self.config_set('global', 'create_symbol_template', str(status)) 85 | 86 | def create_symbol(self): 87 | """ 88 | Create a temporary symbol signature from the current function on the 89 | IDA screen. 90 | """ 91 | self.log('creating temporary signature') 92 | 93 | current_ea = idc.get_screen_ea() 94 | 95 | signature = { 96 | 'name': idc.get_func_name(current_ea), 97 | 'instructions': [] 98 | } 99 | 100 | if self._create_template_symbol: 101 | find_bytes_ida = "find-bytes-ida '" 102 | 103 | for ea in idautils.FuncItems(current_ea): 104 | mnem = idc.print_insn_mnem(ea).lower() 105 | opcode_size = idc.get_item_size(ea) 106 | 107 | # ppc 108 | if mnem.startswith('b') or mnem in ('lis', 'lwz', 'addi'): 109 | # relative opcodes 110 | find_bytes_ida += '?? ' * opcode_size 111 | continue 112 | 113 | # arm 114 | if mnem.startswith('b') or mnem in ('ldr', 'str'): 115 | # relative opcodes 116 | find_bytes_ida += '?? ' * opcode_size 117 | continue 118 | 119 | opcode = binascii.hexlify(idc.get_bytes(ea, opcode_size)) 120 | formatted_hex = ' '.join(opcode[i:i + 2].decode() for i in 121 | range(0, len(opcode), 2)) 122 | find_bytes_ida += formatted_hex + ' ' 123 | 124 | find_bytes_ida += "'" 125 | 126 | signature['instructions'].append(find_bytes_ida) 127 | signature['instructions'].append('function-start') 128 | signature['instructions'].append('set-name "{}"'.format( 129 | idc.get_func_name(current_ea))) 130 | 131 | with open(TEMP_SIG_FILENAME, 'w') as f: 132 | hjson.dump(signature, f, indent=4) 133 | 134 | self.log('Signature created at {}'.format(TEMP_SIG_FILENAME)) 135 | return TEMP_SIG_FILENAME 136 | 137 | def extended_create_symbol(self): 138 | """ 139 | Creates a temporary symbol of the currently active function 140 | and open it using OS default editor 141 | :return: None 142 | """ 143 | filename = self.create_symbol() 144 | open_file(filename) 145 | 146 | def find_symbol(self): 147 | """ 148 | Find the last create symbol signature. 149 | :return: None 150 | """ 151 | with open(TEMP_SIG_FILENAME) as f: 152 | sig = hjson.load(f) 153 | 154 | results = self.find_from_sig_json(sig, decremental=False) 155 | 156 | for address in results: 157 | self.log('Search result: 0x{:x}'.format(address)) 158 | self.log('Search done') 159 | 160 | if len(results) == 1: 161 | # if remote sig has a proper name, but current one is not 162 | ida_kernwin.jumpto(results[0]) 163 | 164 | def verify_project(self): 165 | """ 166 | Verify a valid project is currently active. 167 | Show IDA warning if not. 168 | :return: None 169 | """ 170 | try: 171 | super(IdaLoader, self).verify_project() 172 | except IOError as e: 173 | ida_kernwin.warning(str(e)) 174 | raise e 175 | 176 | def prompt_save_signature(self): 177 | """ 178 | Save last-created-temp-signature if user agrees to in IDA prompt 179 | :return: None 180 | """ 181 | self.verify_project() 182 | 183 | if ida_kernwin.ask_yn(1, 'Are you sure you want ' 184 | 'to save this signature?') != 1: 185 | return 186 | 187 | self.save_signature(TEMP_SIG_FILENAME) 188 | 189 | def find(self, symbol_name, use_cache=False): 190 | """ 191 | Find symbol by name (as specified in SIG file) 192 | Show an IDA waitbox while doing so 193 | :param symbol_name: symbol name 194 | :return: output address list 195 | """ 196 | ida_kernwin.replace_wait_box('Searching symbol: \'{}\'...' 197 | .format(symbol_name)) 198 | return super(IdaLoader, self).find(symbol_name, use_cache=use_cache) 199 | 200 | def get_python_symbols(self, file_name=None): 201 | """ 202 | Run all python scripts inside the currently active project. 203 | Show an IDA waitbox while doing so 204 | :param file_name: filter a specific filename to execute 205 | :return: dictionary of all found symbols 206 | """ 207 | ida_kernwin.replace_wait_box('Running python scripts...') 208 | return super(IdaLoader, self).get_python_symbols(file_name=file_name) 209 | 210 | @staticmethod 211 | def extract_all_user_names(filename=None): 212 | """ 213 | Get all user-named labels inside IDA. Also prints into output window. 214 | :return: dictionary of all user named labels: label_name -> ea 215 | """ 216 | results = {} 217 | output = '' 218 | 219 | for ea, name in idautils.Names(): 220 | if ida_kernwin.user_cancelled(): 221 | return results 222 | 223 | if '_' in name: 224 | if name.split('_')[0] in ('def', 'sub', 'loc', 'jpt', 'j', 225 | 'nullsub'): 226 | continue 227 | flags = ida_bytes.get_full_flags(ea) 228 | if ida_bytes.has_user_name(flags): 229 | results[name] = ea 230 | output += '{} = 0x{:08x};\n'.format(name, ea) 231 | 232 | if filename is not None: 233 | with open(filename, 'w') as f: 234 | f.write(output) 235 | 236 | return results 237 | 238 | def set_const(self, name, value): 239 | super(IdaLoader, self).set_const(name, value) 240 | fa_types.add_const(name, value) 241 | 242 | def set_symbol(self, symbol_name, value): 243 | super(IdaLoader, self).set_symbol(symbol_name, value) 244 | idc.set_name(value, symbol_name, idc.SN_CHECK) 245 | 246 | def symbols(self, output_file_path=None): 247 | """ 248 | Run find for all SIG files in currently active project. 249 | Show an IDA waitbox while doing so 250 | :param output_file_path: optional, save found symbols into output file 251 | :return: dictionary of found symbols 252 | """ 253 | self.verify_project() 254 | results = {} 255 | 256 | try: 257 | ida_kernwin.show_wait_box('Searching...') 258 | results = super(IdaLoader, self).symbols() 259 | 260 | ida_kernwin.replace_wait_box('Extracting...') 261 | ida_symbols = IdaLoader.extract_all_user_names(output_file_path) 262 | 263 | results.update(ida_symbols) 264 | 265 | except Exception: 266 | traceback.print_exc() 267 | finally: 268 | ida_kernwin.hide_wait_box() 269 | 270 | return results 271 | 272 | def export(self): 273 | """ 274 | Show an export dialog to export symbols and header file for given 275 | IDB. 276 | :return: None 277 | """ 278 | 279 | class ExportForm(Form): 280 | def __init__(self): 281 | description = ''' 282 |

Export

283 | 284 | Select a directory to export IDB data into. 285 | ''' 286 | 287 | Form.__init__(self, 288 | r"""BUTTON YES* Save 289 | Export 290 | {StringLabel} 291 | <#Symbols#Symbols filename:{iSymbolsFilename}> 292 | <#C Header#C Header filename:{iHeaderFilename}> 293 | <#ifdef_macro#ifdef'ed:{iIfdef}> 294 | <#Select dir#Browse for dir:{iDir}> 295 | """, { 296 | 'iDir': Form.DirInput(), 297 | 'StringLabel': 298 | Form.StringLabel(description, 299 | tp=Form.FT_HTML_LABEL), 300 | 'iSymbolsFilename': Form.StringInput( 301 | value='symbols.txt'), 302 | 'iHeaderFilename': Form.StringInput( 303 | value='fa_structs.h'), 304 | 'iIfdef': Form.StringInput( 305 | value='FA_STRUCTS_H'), 306 | }) 307 | self.__n = 0 308 | 309 | def OnFormChange(self, fid): 310 | return 1 311 | 312 | form = ExportForm() 313 | form, args = form.Compile() 314 | ok = form.Execute() 315 | if ok == 1: 316 | # save symbols 317 | symbols_filename = os.path.join(form.iDir.value, 318 | form.iSymbolsFilename.value) 319 | with open(symbols_filename, 'w') as f: 320 | results = IdaLoader.extract_all_user_names(None) 321 | for k, v in sorted(results.items()): 322 | f.write('{} = 0x{:08x};\n'.format(k, v)) 323 | 324 | # save c header 325 | idati = ida_typeinf.get_idati() 326 | c_header_filename = os.path.join(form.iDir.value, 327 | form.iHeaderFilename.value) 328 | 329 | consts_ordinal = None 330 | ordinals = [] 331 | for ordinal in range(1, ida_typeinf.get_ordinal_qty(idati) + 1): 332 | ti = ida_typeinf.tinfo_t() 333 | if ti.get_numbered_type(idati, ordinal): 334 | if ti.get_type_name() == 'FA_CONSTS': 335 | # convert into macro definitions 336 | consts_ordinal = ordinal 337 | elif ti.get_type_name() in ('__va_list_tag', 338 | 'va_list'): 339 | continue 340 | elif '$' in ti.get_type_name(): 341 | # skip deleted types 342 | continue 343 | else: 344 | ordinals.append(str(ordinal)) 345 | 346 | with open(c_header_filename, 'w') as f: 347 | ifdef_name = form.iIfdef.value.strip() 348 | 349 | if len(ifdef_name) > 0: 350 | f.write('#ifndef {ifdef_name}\n' 351 | '#define {ifdef_name}\n\n' 352 | .format(ifdef_name=ifdef_name)) 353 | 354 | if consts_ordinal is not None: 355 | consts = re.findall('\\s*(.+?) = (.+?),', 356 | idc.print_decls( 357 | str(consts_ordinal), 0)) 358 | for k, v in consts: 359 | f.write('#define {} ({})\n'.format(k, v)) 360 | 361 | # ida exports using this type 362 | f.write('#define _BYTE char\n') 363 | f.write('\n') 364 | 365 | structs_buf = idc.print_decls(','.join(ordinals), 366 | idc.PDF_DEF_BASE) 367 | 368 | for struct_type, struct_name in re.findall( 369 | r'(struct|enum) .*?([a-zA-Z0-9_\-]+?)\s+\{', 370 | structs_buf): 371 | f.write( 372 | 'typedef {struct_type} {struct_name} {struct_name};\n' 373 | .format(struct_type=struct_type, 374 | struct_name=struct_name)) 375 | 376 | structs_buf = structs_buf.replace('__fastcall', '') 377 | f.write('\n') 378 | f.write(structs_buf) 379 | f.write('\n') 380 | 381 | if len(ifdef_name) > 0: 382 | f.write('#endif // {ifdef_name}\n' 383 | .format(ifdef_name=ifdef_name)) 384 | 385 | form.Free() 386 | 387 | def set_input(self, input_): 388 | """ 389 | Mock for change_input. Just reload current loaded data settings. 390 | :param input_: doesn't matter 391 | :return: None 392 | """ 393 | self.endianity = '>' if ida_ida.inf_is_be() else '<' 394 | self._input = input_ 395 | self.reload_segments() 396 | 397 | def reload_segments(self): 398 | """ 399 | memory searches will use IDA's API instead 400 | which is much faster so this is just a stub. 401 | :return: None 402 | """ 403 | return 404 | 405 | def interactive_settings(self): 406 | """ 407 | Show settings dialog 408 | :return: None 409 | """ 410 | 411 | class SettingsForm(Form): 412 | def __init__(self, signatures_root, use_template): 413 | description = ''' 414 |

Settings

415 |
416 | Here you can change global FA settings. 417 |
418 | 422 | ''' 423 | 424 | Form.__init__(self, 425 | r"""BUTTON YES* Save 426 | FA Settings 427 | {{FormChangeCb}} 428 | {{StringLabel}} 429 | 430 | 431 | """.format(signatures_root), { 432 | 'FormChangeCb': 433 | Form.FormChangeCb(self.OnFormChange), 434 | 'signaturesRoot': 435 | Form.DirInput(value=signatures_root), 436 | 'StringLabel': 437 | Form.StringLabel(description, 438 | tp=Form.FT_HTML_LABEL), 439 | 'signatureGeneration': 440 | Form.DropdownListControl( 441 | items=['Default', 442 | 'Using function bytes'], 443 | readonly=True, 444 | selval=use_template), 445 | }) 446 | self.__n = 0 447 | 448 | def OnFormChange(self, fid): 449 | return 1 450 | 451 | f = SettingsForm(self._signatures_root, self._create_template_symbol) 452 | f, args = f.Compile() 453 | ok = f.Execute() 454 | if ok == 1: 455 | self.set_signatures_root(f.signaturesRoot.value, save=True) 456 | self.set_symbol_template(f.signatureGeneration.value == 1) 457 | f.Free() 458 | 459 | def interactive_set_project(self): 460 | """ 461 | Show set-project dialog 462 | :return: None 463 | """ 464 | 465 | class SetProjectForm(Form): 466 | def __init__(self, signatures_root, projects, current): 467 | description = ''' 468 |

Project Selector

469 |
470 | Select project you wish to work on from your 471 | signatures root: 472 |
473 |
{}
474 |
(Note: You may change this in config.ini)
475 | 479 | '''.format(signatures_root) 480 | 481 | Form.__init__(self, 482 | r"""BUTTON YES* OK 483 | FA Project Select 484 | {{FormChangeCb}} 485 | {{StringLabel}} 486 | 487 | """.format(signatures_root), { 488 | 'FormChangeCb': 489 | Form.FormChangeCb(self.OnFormChange), 490 | 'cbReadonly': 491 | Form.DropdownListControl( 492 | items=projects, 493 | readonly=True, 494 | selval=projects.index(current) 495 | if current in projects else 0), 496 | 'StringLabel': 497 | Form.StringLabel(description, 498 | tp=Form.FT_HTML_LABEL), 499 | }) 500 | self.__n = 0 501 | 502 | def OnFormChange(self, fid): 503 | return 1 504 | 505 | projects = self.list_projects() 506 | f = SetProjectForm(self._signatures_root, projects, self._project) 507 | f, args = f.Compile() 508 | ok = f.Execute() 509 | if ok == 1: 510 | self.set_project(projects[f.cbReadonly.value]) 511 | f.Free() 512 | 513 | 514 | fa_instance = None 515 | 516 | Action = namedtuple('action', 'name icon_filename handler label hotkey') 517 | 518 | 519 | def add_action(action): 520 | """ 521 | Add an ida-action 522 | :param action: action given as the `Action` namedtuple 523 | :return: None 524 | """ 525 | 526 | class Handler(ida_kernwin.action_handler_t): 527 | def __init__(self): 528 | ida_kernwin.action_handler_t.__init__(self) 529 | 530 | def activate(self, ctx): 531 | action.handler() 532 | return 1 533 | 534 | def update(self, ctx): 535 | return ida_kernwin.AST_ENABLE_FOR_WIDGET 536 | 537 | act_icon = -1 538 | if action.icon_filename: 539 | icon_full_filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'res', 'icons', action.icon_filename) 540 | with open(icon_full_filename, 'rb') as f: 541 | icon_data = f.read() 542 | act_icon = ida_kernwin.load_custom_icon(data=icon_data, format="png") 543 | 544 | act_name = action.name 545 | 546 | ida_kernwin.unregister_action(act_name) 547 | if ida_kernwin.register_action(ida_kernwin.action_desc_t( 548 | act_name, # Name. Acts as an ID. Must be unique. 549 | action.label, # Label. That's what users see. 550 | Handler(), # Handler. Called when activated, and for updating 551 | action.hotkey, # Shortcut (optional) 552 | None, # Tooltip (optional) 553 | act_icon)): # Icon ID (optional) 554 | 555 | # Insert the action in the menu 556 | if not ida_kernwin.attach_action_to_menu( 557 | "FA/", act_name, ida_kernwin.SETMENU_APP): 558 | print("Failed attaching to menu.") 559 | 560 | # Insert the action in a toolbar 561 | if not ida_kernwin.attach_action_to_toolbar("fa", act_name): 562 | print("Failed attaching to toolbar.") 563 | 564 | class Hooks(ida_kernwin.UI_Hooks): 565 | def finish_populating_widget_popup(self, widget, popup): 566 | if ida_kernwin.get_widget_type(widget) == \ 567 | ida_kernwin.BWN_DISASM: 568 | ida_kernwin.attach_action_to_popup(widget, 569 | popup, 570 | act_name, 571 | None) 572 | 573 | hooks = Hooks() 574 | hooks.hook() 575 | 576 | 577 | def load_ui(): 578 | """ 579 | Load FA's GUI buttons 580 | :return: None 581 | """ 582 | actions = [ 583 | Action(name='fa:settings', 584 | icon_filename='settings.png', 585 | handler=fa_instance.interactive_settings, 586 | label='Settings', 587 | hotkey=None), 588 | 589 | Action(name='fa:set-project', 590 | icon_filename='suitcase.png', 591 | handler=fa_instance.interactive_set_project, 592 | label='Set project...', 593 | hotkey='Ctrl+6'), 594 | 595 | Action(name='fa:symbols', icon_filename='find_all.png', 596 | handler=fa_instance.symbols, 597 | label='Find all project\'s symbols', 598 | hotkey='Ctrl+7'), 599 | 600 | Action(name='fa:export', icon_filename='export.png', 601 | handler=fa_instance.export, 602 | label='Export symbols', 603 | hotkey=None), 604 | 605 | Action(name='fa:extended-create-signature', 606 | icon_filename='create_sig.png', 607 | handler=fa_instance.extended_create_symbol, 608 | label='Create temp signature...', 609 | hotkey='Ctrl+8'), 610 | 611 | Action(name='fa:find-symbol', 612 | icon_filename='find.png', 613 | handler=fa_instance.find_symbol, 614 | label='Find last created temp signature', 615 | hotkey='Ctrl+9'), 616 | 617 | Action(name='fa:prompt-save', 618 | icon_filename='save.png', 619 | handler=fa_instance.prompt_save_signature, 620 | label='Save last created temp signature', 621 | hotkey='Ctrl+0'), 622 | ] 623 | 624 | # init toolbar 625 | ida_kernwin.delete_toolbar('fa') 626 | ida_kernwin.create_toolbar('fa', 'FA Toolbar') 627 | 628 | # init menu 629 | ida_kernwin.delete_menu('fa') 630 | ida_kernwin.create_menu('fa', 'FA') 631 | 632 | for action in actions: 633 | add_action(action) 634 | 635 | 636 | def install(): 637 | """ 638 | Install FA ida plugin 639 | :return: None 640 | """ 641 | fa_plugin_dir = os.path.join( 642 | idaapi.get_user_idadir(), 'plugins') 643 | 644 | if not os.path.exists(fa_plugin_dir): 645 | os.makedirs(fa_plugin_dir) 646 | 647 | fa_plugin_filename = os.path.join(fa_plugin_dir, PLUGIN_FILENAME) 648 | if os.path.exists(fa_plugin_filename): 649 | IdaLoader.log('already installed') 650 | return 651 | 652 | with open(fa_plugin_filename, 'w') as f: 653 | f.writelines("""from __future__ import print_function 654 | try: 655 | from fa.ida_plugin import PLUGIN_ENTRY, FAIDAPlugIn 656 | except ImportError: 657 | print("[WARN] Could not load FA plugin. " 658 | "FA Python package doesn\'t seem to be installed.") 659 | """) 660 | 661 | idaapi.load_plugin(PLUGIN_FILENAME) 662 | 663 | IdaLoader.log('Successfully installed :)') 664 | 665 | 666 | @click.command() 667 | @click.option('-s', '--service', type=click.IntRange(1024, 65535), help='execute in rpyc service mode at given port') 668 | def main(service): 669 | plugin_main(service) 670 | 671 | 672 | class FaService(rpyc.Service): 673 | ida_segment = ida_segment 674 | ida_kernwin = ida_kernwin 675 | ida_typeinf = ida_typeinf 676 | ida_bytes = ida_bytes 677 | idautils = idautils 678 | ida_auto = ida_auto 679 | ida_pro = ida_pro 680 | ida_ida = ida_ida 681 | idaapi = idaapi 682 | idc = idc 683 | 684 | @staticmethod 685 | def load_module(name, filename): 686 | return fainterp.FaInterp.get_module(name, filename) 687 | 688 | 689 | def plugin_main(service=None): 690 | global fa_instance 691 | 692 | fa_instance = IdaLoader() 693 | fa_instance.set_input('ida') 694 | 695 | load_ui() 696 | 697 | IdaLoader.log(''' 698 | --------------------------------- 699 | FA Loaded successfully 700 | 701 | Quick usage: 702 | print(fa_instance.find(symbol_name)) # searches for the specific symbol 703 | fa_instance.get_python_symbols(filename=None) # run project's python 704 | scripts (all or single) 705 | fa_instance.set_symbol_template(status) # enable/disable template temp 706 | signature 707 | fa_instance.symbols() # searches for the symbols in the current project 708 | --------------------------------- 709 | ''') 710 | 711 | if service: 712 | t = OneShotServer(FaService, port=service, protocol_config={ 713 | 'allow_all_attrs': True, 714 | 'allow_setattr': True, 715 | }) 716 | t.start() 717 | 718 | # TODO: consider adding as autostart script 719 | # install() 720 | 721 | 722 | try: 723 | class FAIDAPlugIn(idaapi.plugin_t): 724 | wanted_name = "FA" 725 | wanted_hotkey = "Shift-," 726 | flags = 0 727 | comment = "" 728 | help = "Load FA in IDA Pro" 729 | 730 | def init(self): 731 | plugin_main() 732 | return idaapi.PLUGIN_KEEP 733 | 734 | def run(self, args): 735 | pass 736 | 737 | def term(self): 738 | pass 739 | except TypeError: 740 | print('ignoring rpyc bug') 741 | 742 | 743 | def PLUGIN_ENTRY(): 744 | """ 745 | Entry point for IDA plugins 746 | :return: 747 | """ 748 | return FAIDAPlugIn() 749 | 750 | 751 | if __name__ == '__main__': 752 | # Entry point for IDA in script mode (-S) 753 | main(standalone_mode=False, args=idc.ARGV[1:]) 754 | --------------------------------------------------------------------------------