├── .gitignore ├── flaredbg ├── __init__.py ├── utils.py ├── disargfinder.py ├── vivargtracker.py └── flaredbg.py ├── plugins ├── __init__.py ├── importfind.py ├── membreak.py └── injectfind.py ├── setup.py ├── examples ├── README.md ├── example.py └── example2.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /flaredbg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | 7 | requirements = [ 8 | "pykd", 9 | #"vivisect", # install from https://github.com/williballenthin/vivisect 10 | "winappdbg" 11 | ] 12 | 13 | description = "Utilities and plugins for debugger scripting." 14 | 15 | setup( 16 | name='flaredbg', 17 | version='0.1.0', 18 | description=description, 19 | long_description=description, 20 | author="Tyler Dean", 21 | author_email='tyler.dean@fireeye.com', 22 | url='https://github.com/fireeye/flare-dbg', 23 | packages=[ 24 | 'flaredbg', 25 | 'plugins', 26 | 'examples', 27 | ], 28 | include_package_data=True, 29 | install_requires=requirements, 30 | zip_safe=False, 31 | ) 32 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Windbg decoder scripts 2 | The general idea behind the ```flaredbg.py``` is to create a collection of convenience functions to quickly decode obfuscated strings and automate other debugger tasks. 3 | 4 | ## Finding arguments 5 | The ```get_call_list``` function will look for the number of push arguments and the specific registers you ask for. Each register argument is its own dictionary, i.e. ```{'eax':5}```. Each push argument is simply append to the end of the list. 6 | 7 | ```get_call_list``` returns the calling virtual address and the arguments. 8 | 9 | ## Allocating memory 10 | The ```flaredbg.py``` script contains several wrapper functions to read/write/malloc/free memory. 11 | 12 | ## Running the function 13 | Usually you will just call the ```flaredbg.DebugUtils.call``` function, passing the function address, the argument list, and the from virtual address. Other convenience functions exist including ```run_to_return``` to only run to a specific virtual address. 14 | 15 | # Examples 16 | An example script named ```example.py``` can be used as a basic template for new decoder scripts. 17 | An example script named ```example2.py``` is slightly more complicated. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | flare-dbg is a project meant to aid malware reverse engineers in rapidly developing debugger scripts. 3 | 4 | # Installation/setup 5 | 1. Install the ```pykd``` windbg extension from: https://pykd.codeplex.com/releases 6 | 1. Download the Bootstrapper dll. 7 | 2. Add the Bootstrapper pykd.dll file into your winext directory. Something like ```%ProgramFiles%\Debugging Tools for Windows\winext```. 8 | 3. Install the latest 0.3.x version of pykd using ```pip install pykd```. 9 | 4. Ensure you can import ```pykd``` from within windbg: ```.load pykd```. 10 | 2. Install ```winappdbg``` 11 | 1. ```pip install winappdbg``` 12 | 3. Setup ```vivisect``` 13 | 1. Install vivisect using one of the following options: 14 | 1. Install source using pip: ```pip install https://github.com/williballenthin/vivisect/zipball/master``` 15 | 2. Download and extract upstream [vivisect](https://github.com/vivisect/vivisect) and set ```PYTHONPATH``` to the extracted directory. 16 | 2. Ensure you can import vivisect from a python shell: ```import vivisect```. 17 | 4. Setup ```flaredbg``` 18 | 1. Install flaredbg using ```setup.py``` 19 | 20 | # Running scripts 21 | There are two options for running scripts: 22 | 1. Create a script directory and set ```PYTHONPATH``` to the newly created script directory and add your scripts here. 23 | 2. Copy scripts to the root of your windbg directory. Something like: ```%ProgramFiles%\Debugging Tools for Windows\```. 24 | Once your script path is setup, scripts are run from the windbg console as follows: 25 | ``` 26 | > .load pykd 27 | > !py 28 | ``` 29 | 30 | # Installing and running plugins 31 | The recommended way to install scripts is to add the plugins directory of this project to your ```PYTHONPATH```. 32 | Another option is to follow the second option described above in the ```Running scripts``` section. Simply copy the plugin scripts to the root of your windbg directory. 33 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | from flaredbg import flaredbg, utils 18 | 19 | def main(): 20 | # Function virtual address for the string decoder function 21 | fva = 0x401000 22 | 23 | dbg = flaredbg.DebugUtils() 24 | 25 | # Get all the locations the fva function was called from as well as the arguments 26 | # get_call_list accepts the number of push arguments and the required registers 27 | # The function of interest in this example only accepts push arguments 28 | call_list = dbg.get_call_list(fva, 3) 29 | 30 | # Create a list of output decoded strings for an IDA python script 31 | out_list = [] 32 | 33 | # Iterate through all the times the fva was called 34 | for fromva, args in call_list: 35 | # Allocate some memory for the output string and the output string size 36 | str_va = dbg.malloc(args[2]) 37 | args[1] = str_va 38 | 39 | try: 40 | # Make the call! 41 | dbg.call(fva, args, fromva) 42 | # Read the string output 43 | out_str = dbg.read_string(str_va) 44 | except flaredbg.AccessViolationException as e: 45 | print "Access violation at: 0x%x" % e.va 46 | out_str = '' 47 | 48 | # Print out the result 49 | print hex(fromva), out_str 50 | # Free the memory 51 | dbg.free(str_va) 52 | 53 | # arg 0 contains the "unknown" bytes offset, and out contains the decoded string 54 | out_list.append((args[0], out_str)) 55 | 56 | # Generate an IDA script and write it out 57 | ida_script = utils.generate_ida_comments(out_list, True) 58 | open('C:\\ida_comments.py', 'wb').write(ida_script) 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /examples/example2.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | ######################################################################## 18 | # 19 | # sogu strings decoder. Written for: 41d059f059795e91137fd411b5f4d74d 20 | # 21 | # 1) Run the malware and attach to the malware with windbg. 22 | # 2) Run the example2.py decoder script. 23 | # 3) Retrieve the output ida_comments.py script and run it in IDA to annotate the decoded strings. 24 | # 25 | ######################################################################## 26 | 27 | from flaredbg import flaredbg, utils 28 | 29 | def main(): 30 | # Function virtual address for the string decoder function 31 | fva = 0x10002F6C 32 | fva_end = 0x10003071 33 | 34 | dbg = flaredbg.DebugUtils() 35 | 36 | # Use get call list to retrieve two push args and three register args 37 | call_list = dbg.get_call_list(fva, 2, ['eax', 'ecx', 'edi']) 38 | 39 | # Create a list of output decoded strings for an IDA python script 40 | out_list = [] 41 | 42 | # Iterate through all the times the fva was called 43 | for fromva, args in call_list: 44 | # Allocate some memory for a stack variable that will receive the output 45 | str_buf = dbg.malloc(0x20) 46 | # Update ecx with the new memory 47 | dbg.set_reg_arg(args, 'ecx', str_buf) 48 | 49 | try: 50 | # Make the call, and specify the last address of the function, this makes the function run much faster 51 | # as it will run until a breakpoint instead of single stepping a function looking for a return. 52 | out_buf = dbg.call(fva, args, fromva, tova=fva_end) 53 | # Read the string output 54 | str_va = dbg.read_pointer(out_buf) 55 | out_str = dbg.read_string(str_va) 56 | except flaredbg.AccessViolationException as e: 57 | print "Access violation at: 0x%x" % e.va 58 | out_str = '' 59 | 60 | # Print out the result 61 | print hex(fromva), repr(out_str) 62 | # Free the memory 63 | dbg.free(str_buf) 64 | 65 | # Append the result to the IDA comments list 66 | out_list.append((fromva, out_str)) 67 | 68 | # Generate an IDA script and write it out 69 | ida_script = utils.generate_ida_comments(out_list, True) 70 | open('C:\\ida_comments.py', 'wb').write(ida_script) 71 | 72 | if __name__ == '__main__': 73 | main() -------------------------------------------------------------------------------- /plugins/importfind.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2016 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | # 17 | # The importfind plugin searches for references to runtime imported functions. 18 | # The plugin iterates all loaded DLLs and parses their export tables to find the in-memory 19 | # virtual address for each named function. The plugin will then search for references to 20 | # those addresses within a memory region. 21 | # 22 | # For example, if a program imported a function using: 23 | # call GetProcAddress(esi, 'WriteProcessMemory'); 24 | # mov dword_403220, eax 25 | # 26 | # This global dword may be called later: 27 | # call dword_403220 28 | # 29 | # The importfind plugin will rename the dword_403220 to WriteProcessMemory 30 | # 31 | # usage: !py importfind [-h] [-o SCRIPT_PATH] addr 32 | # 33 | # positional arguments: 34 | # addr Address in a memory region 35 | # 36 | # optional arguments: 37 | # -h, --help show this help message and exit 38 | # -o SCRIPT_PATH, --script-path SCRIPT_PATH 39 | # Output IDAPython script full path 40 | # 41 | # The output script will be an IDAPython script which can be run in IDA to automatically 42 | # rename global variables that contain the runtime imported functions. 43 | # 44 | 45 | import sys 46 | import argparse 47 | from flaredbg import flaredbg, utils 48 | 49 | def hexint(x): 50 | return int(x, 16) 51 | 52 | def main(): 53 | script_name = sys.argv[0].split('\\')[-1] 54 | parser = argparse.ArgumentParser(description='!py %s is used to find all library function addresses in a memory region' % script_name) 55 | parser.add_argument('-o', '--script-path', help='Output IDAPython script full path') 56 | parser.add_argument('addr', type=hexint, help='Address in a memory region') 57 | args = parser.parse_args() 58 | 59 | if args.addr and args.script_path: 60 | pu = flaredbg.ProcessUtils() 61 | 62 | base_addr = pu.get_allocation_base(args.addr) 63 | imports = pu.find_imports(base_addr) 64 | 65 | if imports: 66 | name_list = [] 67 | for va, func in imports.iteritems(): 68 | if 'func_name' in func: 69 | name_list.append((va, func['func_name'])) 70 | idascript = utils.generate_ida_names(name_list) 71 | open(args.script_path, 'wb').write(idascript) 72 | print ' [+] Successfully wrote IDA Python script to %s' % args.script_path 73 | else: 74 | print ' [-] Failed to find library function addresses' 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /plugins/membreak.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2016 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | # 17 | # The membreak plugin is used to set breakpoints on an entire memory region. 18 | # Memory breakpoints can be achieved in two primary ways: by adjusting the 19 | # page guard protection or removing the execute protection. If any part of 20 | # a memory region begins to execute, an exception is generated and handled 21 | # by the plugin and the debugger regains control of the program. After 22 | # a memory breakpoint is hit, the original memory protections are restored. 23 | # 24 | # The plugin will fail if VirtualProtect is called by the running program 25 | # and changes the protection. 26 | # 27 | # If passed the -a flag, the membreak plugin will remove the execute 28 | # protection and waits for an access violation. However, if the executable 29 | # is running in an environment where DEP is disabled, this breakpoint type 30 | # will fail. In this case, use the page guard breakpoint instead. 31 | # 32 | # usage: !py membreak [-h] [-a] addr [addr ...] 33 | # 34 | # positional arguments: 35 | # addr 36 | # 37 | # optional arguments: 38 | # -h, --help show this help message and exit 39 | # -a, --access-breakpoint 40 | # Use access violation breakpoint instead guard page 41 | # 42 | # Multiple addresses can be specified. If more than one address is specified, 43 | # the plugin will set multiple memory breakpoints. When one is hit, all the 44 | # memory protections are restored. 45 | # 46 | 47 | import sys 48 | import argparse 49 | from flaredbg import flaredbg 50 | 51 | def hexint(x): 52 | return int(x, 16) 53 | 54 | class AddressParseAction(argparse.Action): 55 | def __call__(self, parser, namespace, values, option_string=None): 56 | setattr(namespace, self.dest, values) 57 | 58 | def main(): 59 | script_name = sys.argv[0].split('\\')[-1] 60 | parser = argparse.ArgumentParser(description='!py %s is used to create a memory breakpoint on memory region addresses' % script_name) 61 | parser.add_argument('-a', '--access-breakpoint', action="store_true", help="Use access violation breakpoint instead guard page") 62 | parser.add_argument('addr', nargs='+', type=hexint, action=AddressParseAction) 63 | args = parser.parse_args() 64 | 65 | dbg = flaredbg.DebugUtils() 66 | 67 | print " Running until memory breakpoint hit." 68 | if args.access_breakpoint: 69 | hit_addr = dbg.set_access_breakpoint(args.addr) 70 | else: 71 | hit_addr = dbg.set_mem_breakpoint(args.addr) 72 | print " Memory breakpoint hit!\n 0x%x" % (hit_addr) 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /plugins/injectfind.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2016 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | # 17 | # The injectfind plugin attempts to find injected code. 18 | # The plugin searches all memory regions and looks at the memory permissions 19 | # and the memory type. If the memory type is private and the protection is 20 | # executable, the plugin will print a disassembly listing and a hexdump of 21 | # the first several bytes of the beginning of the memory region. If the 22 | # memory region is mostly NULL bytes and more than a single page (0x1000 bytes), 23 | # the plugin will display the beginning of the second page. 24 | # 25 | # usage: !py injectfind 26 | # 27 | # Example output: 28 | # 0:018> .load pykd 29 | # 0:018> !py injectfind 30 | # ---------------------------------------------------------------- 31 | # Path: C:\WINDOWS\Explorer.EXE Pid: 632 Region: 0x1700000 - 0x1716fff Length: 0x17000 32 | # Hex dump: 33 | # 01700000 30 ae 80 7c 00 00 00 00-29 52 81 7c 00 00 00 00 0..|....)R.|.... 34 | # 01700010 49 73 57 6f 77 36 34 50-72 6f 63 65 73 73 00 cd IsWow64Process.. 35 | # 01700020 e4 80 7c 00 00 00 00 47-65 74 4d 6f 64 75 6c 65 ..|....GetModule 36 | # 01700030 48 61 6e 64 6c 65 57 00-31 b7 80 7c 00 00 00 00 HandleW.1..|.... 37 | # 01700040 47 65 74 4d 6f 64 75 6c-65 48 61 6e 64 6c 65 41 GetModuleHandleA 38 | # 01700050 00 7b 1d 80 7c 00 00 00-00 4c 6f 61 64 4c 69 62 .{..|....LoadLib 39 | # 01700060 72 61 72 79 41 00 c7 06-81 7c 00 00 00 00 43 72 raryA....|....Cr 40 | # 01700070 65 61 74 65 54 68 72 65-61 64 00 0f 29 83 7c 00 eateThread..).|. 41 | # 42 | # Disassembly: 43 | # 01700000 30ae807c0000 xor byte ptr [esi+7C80h],ch 44 | # 01700006 0000 add byte ptr [eax],al 45 | # 01700008 295281 sub dword ptr [edx-7Fh],edx 46 | # 0170000b 7c00 jl 0170000d 47 | # 0170000d 0000 add byte ptr [eax],al 48 | # 0170000f 004973 add byte ptr [ecx+73h],cl 49 | # 01700012 57 push edi 50 | # 01700013 6f outs dx,dword ptr [esi] 51 | # 52 | # ---------------------------------------------------------------- 53 | # Path: C:\WINDOWS\Explorer.EXE Pid: 632 Region: 0x1cd0000 - 0x1cd0fff Length: 0x1000 54 | # Hex dump: 55 | # 01cd0000 b8 30 00 00 00 e9 3b d1-c3 7a 00 00 00 00 00 00 .0....;..z...... 56 | # 01cd0010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 57 | # 01cd0020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 58 | # 01cd0030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 59 | # 01cd0040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 60 | # 01cd0050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 61 | # 01cd0060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 62 | # 01cd0070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 63 | # 64 | # Disassembly: 65 | # 01cd0000 b830000000 mov eax,30h 66 | # 01cd0005 e93bd1c37a jmp ntdll!NtCreateProcessEx+0x5 (7c90d145) 67 | # 01cd000a 0000 add byte ptr [eax],al 68 | # 01cd000c 0000 add byte ptr [eax],al 69 | # 01cd000e 0000 add byte ptr [eax],al 70 | # 01cd0010 0000 add byte ptr [eax],al 71 | # 01cd0012 0000 add byte ptr [eax],al 72 | # 01cd0014 0000 add byte ptr [eax],al 73 | # 74 | 75 | import pykd 76 | from flaredbg import flaredbg, utils 77 | 78 | 79 | def main(): 80 | """ 81 | injectfind searches process memory for potentially injected code 82 | """ 83 | 84 | process = flaredbg.get_process_obj() 85 | found = False 86 | 87 | for mbi in process.get_memory_map(): 88 | if mbi.is_executable() and mbi.is_private(): 89 | base_addr = mbi.BaseAddress 90 | size = mbi.RegionSize 91 | 92 | print '-' * 0x40 93 | print "Path: %s Pid: %s Region: 0x%x - 0x%x Length: 0x%x" % (process.get_image_name(), process.get_pid(), base_addr, (base_addr+size-1), size) 94 | 95 | db_res = pykd.dbgCommand('db %x' % base_addr) 96 | dis_res = pykd.dbgCommand('u %x' % base_addr) 97 | mem_bytes = process.read(base_addr, size) 98 | 99 | # Check for stripped header 100 | if mem_bytes[:0x1000].count('\0') > 0xfe0: 101 | if size > 0x2000 and mem_bytes[0x1000:0x2000].count('\0') < 0x200: 102 | print " !!! Possible stripped PE header at 0x%x\n Showing address: 0x%x\n" % (base_addr, base_addr+0x1000) 103 | db_res = pykd.dbgCommand('db %x' % (base_addr+0x1000)) 104 | dis_res = pykd.dbgCommand('u %x' % (base_addr+0x1000)) 105 | 106 | # Check for legit PE 107 | elif utils.is_legit_pe(mem_bytes[:0x1000]): 108 | print " Found legit PE at 0x%x\n" % (base_addr) 109 | dis_res = None 110 | 111 | if db_res: 112 | print "Hex dump:" 113 | print db_res 114 | if dis_res: 115 | print "Disassembly:" 116 | print dis_res 117 | print 118 | 119 | found = True 120 | 121 | if not found: 122 | print "Nothing found!" 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /flaredbg/utils.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | import PE 18 | import envi 19 | import flaredbg 20 | 21 | """ 22 | IDA script generation functions 23 | """ 24 | 25 | 26 | def escape(s): 27 | """ 28 | Escape single quotes/backslashes/new lines. 29 | 30 | Parameters: 31 | s : string to escape 32 | 33 | Returns: escapted string 34 | """ 35 | if s is None: 36 | s = '' 37 | s = str(s) 38 | s = str(repr(s)) 39 | s = str(s.replace('\\', '\\\\')) 40 | return s 41 | 42 | 43 | def generate_ida_patch(byte_diff): 44 | """ 45 | Generate idapython script to patch in byte changes. 46 | 47 | Parameters: 48 | byte_diff : dictionary of virtual addresses and hex strings of bytes to replace, created from the bgUtils.compare_memory() function 49 | 50 | Returns: IDA Python script to patch bytes 51 | """ 52 | ida_patch_str = 'import idc\n\n' 53 | ida_patch_str += 'def patch_hex_str(ea, hex_str):\n' 54 | ida_patch_str += ' for i, b in enumerate(hex_str.decode("hex")):\n' 55 | ida_patch_str += ' idc.PatchByte(ea+i, ord(b))\n\n' 56 | 57 | for va, bytes in sorted(byte_diff.iteritems()): 58 | byte_str = ''.join([c for c in bytes]) 59 | ida_patch_str += 'patch_hex_str(0x%x, "%s")\n' % (va, byte_str.encode('hex')) 60 | 61 | return str(ida_patch_str) 62 | 63 | 64 | def generate_ida_comments(cmt_list, rpt=False): 65 | """ 66 | Generate idapython script to make comments, optionally repeatable. 67 | 68 | Parameters: 69 | cmt_list : list that contains virtual addresses and comment strings 70 | 71 | Returns: IDA Python script to make comments 72 | """ 73 | ida_cmt_str = 'import idc\n\n' 74 | ida_cmt_str += 'def append_comment(ea, cmt):\n' 75 | ida_cmt_str += ' current_cmt = CommentEx(ea, %d)\n' % int(rpt) 76 | ida_cmt_str += ' if current_cmt:\n' 77 | ida_cmt_str += ' cmt = "%s\\n%s\\n" % (current_cmt, cmt)\n' 78 | ida_cmt_str += ' idc.%s(ea, cmt)\n\n' % (("MakeRptCmt" if rpt else "MakeComm")) 79 | 80 | for ea, cmt in cmt_list: 81 | ida_cmt_str += 'append_comment(0x%x, %s)\n' % (ea, str(escape(cmt))) 82 | ida_cmt_str += 'print hex(0x%x), %s\n' % (ea, str(escape(cmt))) 83 | 84 | return str(ida_cmt_str) 85 | 86 | 87 | def generate_ida_names(name_list): 88 | """ 89 | Generate idapython script to make names. 90 | 91 | Parameters: 92 | name_list : list that contains virutal addresses and names 93 | 94 | Returns: IDA Python script to make names 95 | """ 96 | ida_name_str = 'import idc\n\n' 97 | ida_name_str += 'def make_name(ea, name):\n' 98 | ida_name_str += ' ret = name\n' 99 | ida_name_str += ' if not MakeNameEx(ea, name, idc.SN_PUBLIC|SN_NOWARN):\n' 100 | ida_name_str += ' for i in range(0x100):\n' 101 | ida_name_str += ' non_collide_name = "%s_%d" % (name, i)\n' 102 | ida_name_str += ' if MakeNameEx(ea, non_collide_name, idc.SN_PUBLIC|SN_NOWARN):\n' 103 | ida_name_str += ' ret = non_collide_name\n' 104 | ida_name_str += ' break\n' 105 | ida_name_str += ' return ret\n\n' 106 | 107 | for ea, name in name_list: 108 | ida_name_str += 'make_name(0x%x, %s)\n' % (ea, str(escape(name))) 109 | ida_name_str += 'print hex(0x%x), %s\n' % (ea, str(escape(name))) 110 | 111 | return str(ida_name_str) 112 | 113 | 114 | """ 115 | PE functions 116 | """ 117 | 118 | 119 | def is_legit_pe(bytes): 120 | """ 121 | Load the memory region into a vivisect memory object and try loading the memory region as a PE "from memory". 122 | If it succeeds and contains valid sections, it's considered a valid PE. 123 | 124 | Parameters: 125 | bytes : byte string to test 126 | 127 | Returns: bool - True if legit pe, False if not 128 | """ 129 | try: 130 | new_pe = PE.peFromBytes(bytes) 131 | 132 | # ImageBase will not be zero and will be page aligned 133 | if new_pe.IMAGE_NT_HEADERS.OptionalHeader.ImageBase == 0 or new_pe.IMAGE_NT_HEADERS.OptionalHeader.ImageBase & 0xfff != 0: 134 | return False 135 | 136 | if new_pe.IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint > len(bytes): 137 | return False 138 | 139 | if new_pe.IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders < 0x80: 140 | return False 141 | 142 | if new_pe.IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders > len(bytes): 143 | return False 144 | 145 | # Section check 146 | # Start at 0x80, never seen a PE that has a VirtualAddress for the 147 | # first section below 0x80, usually > 0x400 148 | prva = 0x80 149 | for sect in new_pe.getSections(): 150 | if prva > sect.VirtualAddress: 151 | return False 152 | elif sect.VirtualAddress & 0xff != 0: 153 | return False 154 | prva = sect.VirtualAddress 155 | 156 | # Assuming that more than 20 sections in a PE is likely bogus 157 | if 0 >= new_pe.IMAGE_NT_HEADERS.FileHeader.NumberOfSections > 20: 158 | return False 159 | 160 | # Could do more checks, but leaving at these, hopefully it'll be enough to rule 161 | # out garbage, but still catch missing MZ or DOS text stubs 162 | 163 | except: 164 | return False 165 | 166 | return True 167 | 168 | 169 | def get_pe_obj(va): 170 | """ 171 | Gets a vivisect PE object from a virtual address. 172 | 173 | Parameters: 174 | va : virtual address 175 | 176 | Returns: vivisect PE object 177 | """ 178 | pu = flaredbg.ProcessUtils() 179 | va = pu.get_allocation_base(va) 180 | pbytes = pu.get_process_region_bytes(va) 181 | memobj = envi.memory.MemoryObject() 182 | memobj.addMemoryMap(va, envi.memory.MM_RWX, "", pbytes) 183 | pe = PE.peFromMemoryObject(memobj, va) 184 | 185 | return pe 186 | -------------------------------------------------------------------------------- /flaredbg/disargfinder.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | import envi 18 | 19 | def find_push_args(vw, dis, num_push_args): 20 | """ 21 | Try to find push instructions with immediate values without emulation, ignoring program flow. 22 | 23 | Parameters: 24 | dis : list of opcodes from vivisect 25 | num_push_args : number of push arguments to search for 26 | 27 | Returns: list of push values found from disassembly 28 | """ 29 | push_args = [] 30 | ignore_me = False 31 | # Using default value of 1, for reasons 32 | DEFAULT = 1 33 | for i, op in enumerate(reversed(dis)): 34 | if len(push_args) >= num_push_args: 35 | break 36 | if op.mnem == 'pop': 37 | ignore_me = True 38 | elif op.mnem == 'push': 39 | if not ignore_me: 40 | value = DEFAULT 41 | opnds = op.getOperands() 42 | if len(opnds) > 0: 43 | opnd = opnds[0] 44 | if opnd.isReg(): 45 | recursive_reg_name = opnd.repr(op) 46 | recursive_find = find_reg_args(vw, dis[i:], [recursive_reg_name]) 47 | value = DEFAULT 48 | if recursive_find: 49 | value = recursive_find[0][recursive_reg_name] 50 | elif opnd.isImmed(): 51 | value = opnd.getOperValue(op) 52 | push_args.append(value) 53 | else: 54 | ignore_me = False 55 | elif op.mnem == 'mov': 56 | opnds = op.getOperands() 57 | if opnds: 58 | opnd0 = opnds[0] 59 | if opnd0.isDeref() and hasattr(opnd0, 'reg'): 60 | # todo figure this out for 64-bit 61 | if opnd0.reg in (envi.archs.i386.REG_ESP, envi.archs.amd64.REG_RSP): 62 | if opnd0.disp in range(0, -num_push_args * vw.psize, -vw.psize): 63 | opnd1 = opnds[1] 64 | value = DEFAULT 65 | if opnd1.isImmed(): 66 | value = opnd1.getOperValue(op) 67 | if len(push_args) <= opnd0.disp / vw.psize: 68 | push_args.append(value) 69 | else: 70 | push_args[opnd0.disp / vw.psize] = value 71 | return push_args 72 | 73 | def find_reg_args(vw, dis, regs): 74 | """ 75 | Try to find immediate register values without emulation, ignoring program flow. 76 | 77 | Parameters: 78 | dis : list of opcodes from vivisect 79 | regs : list of register names 80 | 81 | Returns: list of register values found from disassembly 82 | """ 83 | lregs = list(regs) 84 | reg_args = [] 85 | find_push = False 86 | # Using default value of 1, for reasons 87 | DEFAULT = 1 88 | for i, op in enumerate(reversed(dis)): 89 | # Handle direct mov 90 | if op.mnem == 'mov': 91 | opnds = op.getOperands() 92 | if len(opnds) > 1: 93 | opnd0 = opnds[0] 94 | if opnd0.isReg(): 95 | for reg_name in lregs: 96 | if opnd0.repr(op) == reg_name: 97 | opnd1 = opnds[1] 98 | if opnd1.isImmed(): 99 | value = opnd1.getOperValue(op) 100 | elif opnd1.isReg(): 101 | recursive_reg_name = opnd1.repr(op) 102 | recursive_find = find_reg_args(vw, dis[i:], [recursive_reg_name]) 103 | value = DEFAULT 104 | if recursive_find: 105 | value = recursive_find[0][recursive_reg_name] 106 | else: 107 | # TODO: Handle regs being set by something 108 | # other than an immediate value 109 | value = DEFAULT 110 | reg_args.append({reg_name: value}) 111 | lregs.remove(reg_name) 112 | break 113 | elif op.mnem == 'lea': 114 | # Not sure how to handle this right now, maybe if one of the 115 | # regs wasn't found, just set it to DEFAULT? 116 | opnds = op.getOperands() 117 | opnd0 = opnds[0] 118 | if opnd0.isReg(): 119 | for reg_name in lregs: 120 | if opnd0.repr(op) == reg_name: 121 | opnd1 = opnds[1] 122 | if 'RegMemOper' in opnd1.__class__.__name__: 123 | value = DEFAULT 124 | lea_reg_name = opnd1._dis_regctx.getRegisterName(opnd1.reg) 125 | if hasattr(opnd1, 'disp'): 126 | recursive_find = find_reg_args(vw, dis[i:], [lea_reg_name]) 127 | if recursive_find: 128 | value = recursive_find[0][lea_reg_name] + opnd1.disp 129 | reg_args.append({reg_name: value}) 130 | lregs.remove(reg_name) 131 | break 132 | else: 133 | reg_args.append({reg_name: DEFAULT}) 134 | lregs.remove(reg_name) 135 | break 136 | # Handle zeroing xor 137 | elif op.mnem == 'xor': 138 | opnds = op.getOperands() 139 | reg_name = opnds[0].repr(op) 140 | if opnds[0].isReg() and reg_name in lregs: 141 | if opnds[1].isReg(): 142 | if opnds[0].reg == opnds[1].reg: 143 | reg_args.append({reg_name: 0}) 144 | lregs.remove(reg_name) 145 | # Handle push/pop 146 | elif op.mnem == 'pop': 147 | opnds = op.getOperands() 148 | if len(opnds) > 0: 149 | opnd0 = opnds[0] 150 | if opnd0.isReg(): 151 | for reg_name in lregs: 152 | if opnd0.repr(op) == reg_name: 153 | find_push = reg_name 154 | break 155 | elif op.mnem == 'push' and find_push: 156 | opnds = op.getOperands() 157 | if len(opnds) > 0: 158 | opnd0 = opnds[0] 159 | if opnd0.isImmed(): 160 | value = opnd0.getOperValue(op) 161 | elif opnd0.isReg(): 162 | recursive_reg_name = opnd0.repr(op) 163 | recursive_find = find_reg_args(vw, dis[i:], [recursive_reg_name]) 164 | value = DEFAULT 165 | if recursive_find: 166 | value = recursive_find[0][recursive_reg_name] 167 | else: 168 | # TODO: Handle this case 169 | value = DEFAULT 170 | if find_push in lregs: 171 | reg_args.append({find_push: value}) 172 | lregs.remove(find_push) 173 | find_push = False 174 | if len(lregs) == 0: 175 | break 176 | return reg_args 177 | 178 | def find_args(vw, fromva, num_push_args, regs=[]): 179 | """ 180 | Naively try to find arguments without emulation, using only the disassembly. 181 | 182 | Parameters: 183 | fromva : virtual address that contains the call 184 | num_push_args : number of push arguemnts 185 | regs : list of register names 186 | 187 | Returns: argument list found from disassembly 188 | """ 189 | arg_list = [] 190 | if vw is not None: 191 | fva = vw.getFunction(fromva) 192 | va = fva 193 | dis = [] 194 | while va < fromva: 195 | try: 196 | op = vw.parseOpcode(va) 197 | except envi.InvalidInstruction as e: 198 | print str( e ) 199 | break 200 | dis.append(op) 201 | va += op.size 202 | push_args = find_push_args(vw, dis, num_push_args) 203 | reg_args = find_reg_args(vw, dis, regs) 204 | 205 | args = reg_args + push_args 206 | arg_list.append(args) 207 | else: 208 | print " [-] Please generate a vivisect workspace first!" 209 | 210 | return arg_list -------------------------------------------------------------------------------- /flaredbg/vivargtracker.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | # Modified / standalone version of https://github.com/fireeye/flare-ida/blob/master/python/flare/argtracker.py 18 | # Can be used without IDA 19 | 20 | import os 21 | import sys 22 | import copy 23 | import struct 24 | import logging 25 | import binascii 26 | 27 | import vivisect 28 | import vivisect.impemu as viv_imp 29 | import vivisect.impemu.monitor as viv_imp_monitor 30 | from visgraph import pathcore as vg_path 31 | 32 | 33 | BADADDR = -1 34 | 35 | ######################################################################## 36 | # 37 | # 38 | ######################################################################## 39 | 40 | class RegMonitor(viv_imp_monitor.EmulationMonitor): 41 | ''' 42 | This tracks all register changes, even if it's not currently an interesting reg 43 | because we need to trace register changes backwards. 44 | ''' 45 | 46 | def __init__(self, regs): 47 | viv_imp_monitor.EmulationMonitor.__init__(self) 48 | self.logger = logging.getLogger('argracker.RegMonitor') 49 | self.regs = regs[:] 50 | self.reg_map = {} 51 | 52 | def prehook(self, emu, op, starteip): 53 | try: 54 | #self.logger.debug('prehook: 0x%08x', starteip) 55 | self.cachedRegs = emu.getRegisters() 56 | self.startEip = starteip 57 | except Exception, err: 58 | self.logger.exception('Error in prehook: %s', str(err)) 59 | 60 | def posthook(self, emu, op, endeip): 61 | try: 62 | #self.logger.debug('posthook: 0x%08x', endeip) 63 | curRegs = emu.getRegisters() 64 | curDict = {} 65 | for name, val in curRegs.items(): 66 | if (self.cachedRegs[name] != val): 67 | curDict[name] = val 68 | if len(curDict) != 0: 69 | self.reg_map[self.startEip] = curDict 70 | except Exception, err: 71 | self.logger.exception('Error in posthook: %s', str(err)) 72 | 73 | 74 | ######################################################################## 75 | # 76 | # 77 | ######################################################################## 78 | 79 | #maps a va to the vg_path node that contains it in an emu run 80 | def build_emu_va_map(node, **kwargs): 81 | res = kwargs.get('res') 82 | emu = kwargs.get('emu') 83 | logtype = kwargs.get('logtype') 84 | if (res is None) or (emu is None) or (logtype is None): 85 | return 86 | #for va in vg_path.getNodeProp(node, 'valist'): 87 | # res[va] = node 88 | #for pc, va, bytes in vg_path.getNodeProp(node, 'writelog'): 89 | for entry in vg_path.getNodeProp(node, logtype): 90 | pc, va, bytes = entry 91 | res[pc] = entry 92 | 93 | 94 | def formatWriteLogEntry(entry): 95 | pc, va, bytes = entry 96 | return '0x%08x: 0x%08x: %s' % (pc, va, binascii.hexlify(bytes)) 97 | 98 | def transformWriteLogEntry(entry, bigend=False): 99 | ''' 100 | Tranforms a writelog entry to a (pc, value) tuple 101 | ''' 102 | pc, va, bytes = entry 103 | blen = len(bytes) 104 | if blen == 1: 105 | return (pc, struct.unpack_from(' just want the first reg value 240 | regMods = self.tracker.regMon.reg_map[cVa] 241 | #self.tracker.logger.debug('regmon 0x%08x: examining %d items: %r', cVa, len(regMods), regMods) 242 | for reg in regMods: 243 | interesting1 = (reg in self.regs) and (reg not in self.resultArgs.keys()) 244 | interesting2 = (reg in self.tempMapping.keys()) 245 | if (not interesting1) and (not interesting2): 246 | #modified reg isn't interesting: either a function arg or a temp traced value 247 | #self.tracker.logger.debug('regmon 0x%08x: not interesting: %s', cVa, reg) 248 | continue 249 | #mnem = idc.GetMnem(cVa) 250 | op = self.tracker.vw.parseOpcode(cVa) 251 | mnem = op.mnem 252 | argName = reg 253 | if interesting1: 254 | self.regs.remove(reg) 255 | if interesting2: 256 | argName = self.tempMapping.pop(reg) 257 | if mnem.startswith('pop'): 258 | #add the current stack read address to the temporary tracking 259 | rlogEntry = tracker.va_read_map.get(cVa, None) 260 | if rlogEntry is None: 261 | raise RuntimeError('readlog entry does not exist for a pop') 262 | pc, readVa, bytes = rlogEntry 263 | #self.tracker.logger.debug('regmon 0x%08x tracing (pop): %s (%s): 0x%x', cVa, argName, reg, readVa) 264 | self.tempMapping[readVa] = argName 265 | elif mnem.startswith('mov'): 266 | opnds = op.getOperands() 267 | #if idc.GetOpType(cVa, 1) == idc.o_reg: 268 | if opnds[1].isReg(): 269 | #change to track this reg backwards 270 | #newReg = idc.GetOpnd(cVa, 1) 271 | newReg = repr(opnds[1]) 272 | #self.tracker.logger.debug('regmon 0x%08x tracing (mov): %s (%s)', cVa, argName, newReg) 273 | self.tempMapping[newReg] = argName 274 | else: 275 | #not a register, use the modified result otherwise? 276 | #self.tracker.logger.debug('regmon 0x%08x found (mov): %s (%s): 0x%x', cVa, argName, reg, regMods[reg]) 277 | self.saveResult(argName, cVa, regMods[reg]) 278 | else: 279 | #TODO: any other data movement instructions that should be traced back? 280 | #self.tracker.logger.debug('regmon 0x%08x found (default): %s (%s): 0x%x', cVa, argName, reg, regMods[reg]) 281 | self.saveResult(argName, cVa, regMods[reg]) 282 | 283 | def setStackArgLocs(self, baseEntry, num, regs): 284 | self.startSp = baseEntry[1] 285 | # desiredSp: the stack write addressses that correspond to the arguments we want 286 | self.stackArgLocs = [self.startSp + self.ptrsize*(1+i) for i in range(num)] 287 | 288 | def setDesiredState(self, baseEntry, num, regs): 289 | desiredState = [(i+1) for i in range(num)] 290 | desiredState.extend(regs) 291 | self.desiredState = sorted(desiredState) 292 | 293 | def isComplete(self): 294 | if len(self.desiredState) == len(self.resultArgs): 295 | if self.desiredState == sorted(self.resultArgs.keys()): 296 | return True 297 | else: 298 | raise RuntimeError('Matching len of resultArgs, but not equal!') 299 | return False 300 | 301 | # jayutils.py functions: 302 | 303 | ###################################################################### 304 | # visgraph traversal helpers 305 | ###################################################################### 306 | 307 | def path_dfs(node, func, **kwargs): 308 | todo = [node] 309 | while len(todo) != 0: 310 | #node is a tuple of (parent, child_list, prop_dict) 311 | cur = todo.pop(0) 312 | #insert children at start of queue 313 | blah = cur[1][:] 314 | blah.extend(todo) 315 | todo = blah 316 | func(cur, **kwargs) 317 | 318 | def path_bfs(node, func, **kwargs): 319 | todo = [node] 320 | while len(todo) != 0: 321 | #node is a tuple of (parent, child_list, prop_dict) 322 | cur = todo.pop(0) 323 | #append children to end of queue 324 | todo.extend(cur[1]) 325 | func(cur, **kwargs) 326 | 327 | ###################################################################### 328 | # vivisect 329 | ###################################################################### 330 | 331 | def loadWorkspace(filename, fast=False): 332 | logger = logging.getLogger('loadWorkspace') 333 | 334 | vw = vivisect.VivWorkspace() 335 | vivName = filename + '.viv' 336 | if os.path.exists(vivName): 337 | logger.info('Loading existing workspace %s', vivName) 338 | sys.stdout.flush() 339 | vw.loadWorkspace(vivName) 340 | else: 341 | logger.info('Loading file into vivisect: %s', filename) 342 | sys.stdout.flush() 343 | vw.loadFromFile(filename) 344 | if not fast: 345 | logger.info('Performing vivisect analysis now. This may take some time...') 346 | logger.info('#'*80) 347 | vw.analyze() 348 | logger.info('#'*80) 349 | logger.info('Analysis done. Continuing now') 350 | vw.saveWorkspace() 351 | logger.info('Caching vw workspace object in global variable now') 352 | return vw 353 | 354 | def getAllXrefsTo(vw, va): 355 | #manually parse the preceding instruction & look to see if it can fall through to us 356 | #make a copy of the xrefs!!! or badness will ensue 357 | init = vw.getXrefsTo(va)[:] 358 | prev = vw.getPrevLocation(va) 359 | if prev is None: 360 | return init 361 | lva, lsize, ltype, linfo = prev 362 | if ltype != vivisect.const.LOC_OP: 363 | return init 364 | try: 365 | op = vw.parseOpcode(lva) 366 | except Exception as err: 367 | print 'Weird error while doing getAllXrefsTo: %s' % str(err) 368 | return init 369 | brlist = op.getBranches() 370 | for tova,bflags in brlist: 371 | if tova == va: 372 | init.append( (lva, tova, vivisect.const.REF_CODE, bflags) ) 373 | return init 374 | 375 | ######################################################################## 376 | # 377 | # 378 | ######################################################################## 379 | 380 | class ArgTracker(object): 381 | 382 | def __init__(self, vw, maxIters=1000): 383 | self.logger = logging.getLogger('argracker.ArgTracker') 384 | self.logger.debug('Starting up here') 385 | self.vw = vw 386 | self.lastFunc = 0 387 | self.va_write_map = None 388 | self.ptrsize = self.vw.psize 389 | self.queue = [] 390 | self.maxIters = maxIters 391 | 392 | def printWriteLog(self, wlog): 393 | for ent in wlog: 394 | self.logger.debug(formatWriteLogEntry(ent)) 395 | 396 | def isCargsComplete(self, cargs, num, regs): 397 | return all([cargs.has_key(i+1) for i in range(num)]) and all([cargs.has_key(i) for i in regs]) 398 | 399 | 400 | def getPushArgs(self, va, num, regs=None): 401 | ''' 402 | num -> first arg is 1, 2nd is 2, ... 403 | 404 | Returns a list of dicts whose key is the arg number (starting at 1, 2.. num) 405 | Each dict for a stack argument is a write log tuple (pc, va bytes) 406 | Each dict for a registry is a tuple (pc, value) 407 | 408 | ''' 409 | if regs is None: 410 | regs = [] 411 | count = 0 412 | touched = [] 413 | 414 | #func = self.vw.getFunction(va) 415 | #if func is None: 416 | # self.logger.error('Could not get function start from vw 0x%08x -> has analysis been done???', va) 417 | # return [] 418 | #funcStart = idc.GetFunctionAttr(va, idc.FUNCATTR_START) 419 | funcStart = self.vw.getFunction(va) 420 | #if func != funcStart: 421 | # self.logger.error('IDA & vivisect disagree over function start. Needs to be addressed before process') 422 | # self.logger.error(' IDA: 0x%08x. vivisect: 0x%08x', funcStart, func) 423 | # return [] 424 | #map a every (?) va in a function to the pathnode it was found in 425 | if funcStart != self.lastFunc: 426 | emu = self.vw.getEmulator(True, True) 427 | self.logger.debug('Generating va_write_map for function 0x%08x', funcStart) 428 | self.regMon = RegMonitor(regs) 429 | emu.setEmulationMonitor(self.regMon) 430 | emu.runFunction(funcStart, maxhit=1, maxloop=1) 431 | #cache the last va_write_map for a given function 432 | self.va_write_map = {} 433 | self.va_read_map = {} 434 | self.lastFunc = funcStart 435 | path_bfs(emu.path, build_emu_va_map, res=self.va_write_map, emu=emu, logtype='writelog') 436 | path_bfs(emu.path, build_emu_va_map, res=self.va_read_map, emu=emu, logtype='readlog') 437 | else: 438 | self.logger.debug('Using cached va_write_map') 439 | #self.logger.debug('Len va_write_map: %d', len(self.va_write_map)) 440 | #for cVa, wlog in self.va_write_map.items(): 441 | # self.logger.debug('0x%08x: %s', cVa, formatWriteLogEntry(wlog)) 442 | 443 | baseEntry = self.va_write_map.get(va, None) 444 | if baseEntry is None: 445 | self.logger.error('Node does not have write log. Requires a call instruction (which writes to the stack) for this to work: 0x%08x', va) 446 | return [] 447 | self.startSp = baseEntry[1] 448 | return self.analyzeTracker(baseEntry, va, num, regs) 449 | 450 | def analyzeTracker(self, baseEntry, va, num, regs): 451 | self.logger.debug('analyzeTracker 0x%x, %d', va, num) 452 | #funcStart = idc.GetFunctionAttr(va, idc.FUNCATTR_START) 453 | funcStart = self.vw.getFunction(va) 454 | initState = TrackerState(self, baseEntry, num, regs) 455 | count = 0 456 | ret = [] 457 | touched = set() 458 | self.queue = [ (va, initState) ] 459 | while len(self.queue) != 0: 460 | if count > self.maxIters: 461 | self.logger.error('Max graph traveral iterations reached: (0x%08x) %d. Stopping early. Consider increasing ArgTracker maxIters (unless this is a bug)', va, count) 462 | break 463 | cVa, cState = self.queue.pop(0) 464 | touched.add(cVa) 465 | #self.logger.debug('Examining 0x%08x: %s', cVa, str(cState)) 466 | #self.logger.debug('Current tempMapping: 0x%08x %s', cVa, pprint.pformat(cState.tempMapping)) 467 | try: 468 | cState.processWriteLog(self, cVa) 469 | #self.logger.debug('writelog 0x%08x done', cVa) 470 | cState.processRegMon(self, cVa) 471 | #self.logger.debug('regmon 0x%08x done', cVa) 472 | except Exception, err: 473 | self.logger.exception('Error in process: %s', str(err)) 474 | return [] 475 | if cState.isComplete(): 476 | self.logger.debug('Yep, appending') 477 | ret.append(cState.resultArgs) 478 | else: 479 | if cVa == funcStart: 480 | #self.logger.debug('Skipping xref queueing: hit function start') 481 | pass 482 | else: 483 | #self.logger.debug('Not complete: queuing prev items') 484 | #for ref in idautils.CodeRefsTo(cVa, True): 485 | for ref, _, _, _ in getAllXrefsTo(self.vw, cVa): 486 | if ref in touched: 487 | #self.logger.debug('Skip queueing (touched) 0x%08x -> 0x%08x', cVa, ref) 488 | pass 489 | else: 490 | #self.logger.debug('Queueing 0x%08x -> 0x%08x', cVa, ref) 491 | self.queue.append( (ref, cState.copy()) ) 492 | count += 1 493 | return ret 494 | 495 | 496 | def main(): 497 | #jayutils.configLogger(None, logging.DEBUG) 498 | logging.basicConfig(level=logging.INFO) 499 | logger = logging.getLogger('') 500 | logger.debug('Starting up in main') 501 | #name = idc.AskStr('CreateThread', 'Enter function to find args for') 502 | name = raw_input('Enter function find args for [CreateThread]: ').strip() 503 | if name == '': 504 | name = 'CreateThread' 505 | #argNum = idc.AskLong(6) 506 | argNum = raw_input('Enter number of arguments [6]: ').strip() 507 | if argNum == '': 508 | argNum = 6 509 | else: 510 | argNum = int(argNum) 511 | 512 | #filePath = jayutils.getInputFilepath() 513 | filePath = sys.argv[1] 514 | if filePath is None: 515 | logger.info('No input file provided. Stopping') 516 | return 517 | vw = loadWorkspace(filePath) 518 | logger.debug('Loaded workspace') 519 | tracker = ArgTracker(vw) 520 | 521 | #import idautils 522 | #funcEa = idc.LocByName('CreateThread') 523 | funcEa = BADADDR 524 | for iva, _, _, impName in vw.getImports(): 525 | if impName.endswith(name): 526 | funcEa = iva 527 | break 528 | #if funcEa == idc.BADADDR: 529 | if funcEa == BADADDR: 530 | logger.info('CreateThread not found. Returning now') 531 | return 532 | #for xref in idautils.XrefsTo(funcEa): 533 | for fromva, _, _, _ in getAllXrefsTo(vw, funcEa): 534 | argsList = tracker.getPushArgs(fromva, 6) 535 | for argDict in argsList: 536 | print '-'*60 537 | pc, value = argDict[3] 538 | print '0x%08x: 0x%08x: 0x%08x' % (fromva, pc, value) 539 | 540 | if __name__ == '__main__': 541 | main() 542 | -------------------------------------------------------------------------------- /flaredbg/flaredbg.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright 2015 FireEye 3 | # 4 | # FireEye licenses this file to you under the Apache License, Version 5 | # 2.0 (the "License"); you may not use this file except in compliance with the 6 | # License. You may obtain a copy of the License at: 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | ######################################################################## 16 | 17 | # Python imports 18 | import os 19 | import sys 20 | import struct 21 | import tempfile 22 | 23 | # flaredbg imports 24 | import utils 25 | import disargfinder 26 | # Debugger imports 27 | import pykd 28 | import winappdbg 29 | # Vivisect imports 30 | import PE 31 | import envi 32 | 33 | PYKD2 = 'pykd_0_2_x' 34 | PYKD3 = 'pykd_0_3_x' 35 | 36 | 37 | def get_process_obj(pid=None): 38 | """ 39 | Convenience function to get a winappdbg process object from an active windbg session. 40 | 41 | Parameters: 42 | pid : (optional) process id 43 | 44 | Returns: winappdbg Process object 45 | """ 46 | if pid is None: 47 | pykd_version = get_pykd_version() 48 | 49 | if pykd_version == PYKD3: 50 | pid = pykd.getProcessSystemID() 51 | else: 52 | pid = pykd.getCurrentProcessId() 53 | 54 | return winappdbg.Process(pid) 55 | 56 | 57 | def get_pykd_version(): 58 | """ 59 | Gets the pykd version number 2 or 3. 60 | 61 | Returns: pykd version number 62 | """ 63 | version = pykd.version 64 | version_number = int(version.replace(',', '.').replace(' ', '').split('.')[1]) 65 | if version_number == 3: 66 | return PYKD3 67 | elif version_number == 2: 68 | return PYKD2 69 | return None 70 | 71 | 72 | """ 73 | Debug utils class 74 | """ 75 | 76 | 77 | class DebugUtils(): 78 | """ 79 | Utility class that contains functions for: 80 | Vivisect workspace creation 81 | Disassmebly 82 | Memory snapshots/diffing 83 | Memory read/write/alloc/free 84 | Register manipulations 85 | Stack manipulation 86 | Debugger execution 87 | Breakpoints 88 | Calling functions with arguments 89 | """ 90 | 91 | def __init__(self, pid=None, free_mem=True): 92 | """ 93 | Initialize some variables for the DebugUtils class. 94 | 95 | Parameters: 96 | pid : (optional) process id 97 | free_mem : Can be set to False when debugging and do do not want the memory freed 98 | """ 99 | self.alloc_mem_list = [] 100 | self.process = get_process_obj(pid) 101 | self.pid = self.process.get_pid() 102 | self.arch = self.process.get_arch() 103 | self.pykd_version = get_pykd_version() 104 | self.free_mem = free_mem 105 | 106 | if self.arch == winappdbg.win32.ARCH_I386: 107 | self.pointer_size = 4 108 | else: 109 | self.pointer_size = 8 110 | 111 | self.stack = None 112 | self.stack_size = 0 113 | self.init_stack() 114 | 115 | self.vw = None 116 | 117 | def __del__(self): 118 | """ 119 | Clean up all allocated memory. 120 | """ 121 | if self.free_mem and self.process is not None: 122 | for alloc_mem in self.alloc_mem_list: 123 | self.free(alloc_mem) 124 | 125 | """ 126 | Vivisect workspace functions 127 | """ 128 | 129 | def get_workspace_from_file(self, fp, reanalyze=False): 130 | """ 131 | For a file path return a workspace, it will create one if the extension 132 | is not .viv, otherwise it will load the existing one. Reanalyze will cause 133 | it to create and save a new one. 134 | 135 | Parameters: 136 | fp : file path 137 | reanalyze : (optional) reanalyze the file, else pull from cache if exists 138 | """ 139 | print " [+] Getting vivisect workspace." 140 | import vivisect # expensive import, so let's on demand load it 141 | self.vw = vivisect.VivWorkspace() 142 | self.vw.config.viv.parsers.pe.nx = True 143 | if fp.endswith('.viv'): 144 | self.vw.loadWorkspace(fp) 145 | if reanalyze: 146 | self.vw.saveWorkspace() 147 | else: 148 | self.vw.loadFromFile(fp) 149 | self.vw.analyze() 150 | self.vw.saveWorkspace() 151 | 152 | print " [+] vivisect workspace load complete." 153 | 154 | def get_workspace_from_addr(self, addr, entry_point=None, use_pe_load=True, reanalyze=False): 155 | """ 156 | Try to create a PE file given an address, then pass the created PE file to vivisect to create a workspace. 157 | 158 | Parameters: 159 | addr : any virtual address within a memory region 160 | entry_point : (optional) original entry point 161 | use_pe_load : (optional) attempt to save the memory region bytes to disk and load as PE 162 | reanalyze : (optional) reanalyze the vivisect workspace, use this if the workspace has become stale 163 | """ 164 | 165 | print " [+] Getting vivisect workspace." 166 | import vivisect # expensive import, so let's on-demand load it 167 | pu = ProcessUtils() 168 | va = pu.get_allocation_base(addr) 169 | bytes = pu.get_process_region_bytes(va) 170 | 171 | storage_name = '%d_%x_%x' % (self.process.get_pid(), va, len(bytes)) 172 | 173 | self.vw = vivisect.VivWorkspace() 174 | 175 | temp_dir = tempfile.gettempdir() 176 | storage_fname = '%s\\%s.viv' % (temp_dir, storage_name) 177 | 178 | # Don't reanalyze the workspace, try to grab a cached one even if stale 179 | if not reanalyze and os.path.exists(storage_fname): 180 | self.vw.loadWorkspace(storage_fname) 181 | # Reanalyze and create new workspace 182 | else: 183 | self.vw.setMeta('Architecture', self.arch) 184 | self.vw.setMeta('Platform', 'windows') 185 | self.vw.setMeta('Format', 'pe') 186 | self.vw.config.viv.parsers.pe.nx = True 187 | 188 | if utils.is_legit_pe(bytes) and use_pe_load: 189 | import vivisect.parsers.pe 190 | fname = '%s\\%s.mem' % (temp_dir, storage_name) 191 | open(fname, 'wb').write(bytes) 192 | f = file(fname, 'rb') 193 | peobj = PE.PE(f, inmem=True) 194 | peobj.filesize = len(bytes) 195 | vivisect.parsers.pe.loadPeIntoWorkspace(self.vw, peobj, fname) 196 | if entry_point: 197 | self.vw.addEntryPoint(entry_point) 198 | self.vw._snapInAnalysisModules() 199 | else: 200 | import vivisect.parsers.pe 201 | import envi.memory 202 | import vivisect.const 203 | defcall = vivisect.parsers.pe.defcalls.get(self.arch) 204 | self.vw.setMeta("DefaultCall", defcall) 205 | self.vw.addMemoryMap(va, envi.memory.MM_RWX, "", bytes) 206 | pe = None 207 | if utils.is_legit_pe(bytes): 208 | pe = utils.get_pe_obj(va) 209 | if not entry_point and pe: 210 | entry_point = pe.IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint + va 211 | if entry_point: 212 | self.vw.addEntryPoint(entry_point) 213 | self.vw.addExport(entry_point, vivisect.const.EXP_FUNCTION, '__entry', '') 214 | if pe: 215 | self.vw.addVaSet("Library Loads", 216 | (("Address", vivisect.const.VASET_ADDRESS), ("Library", vivisect.const.VASET_STRING))) 217 | self.vw.addVaSet('pe:ordinals', 218 | (('Address', vivisect.const.VASET_ADDRESS), ('Ordinal', vivisect.const.VASET_INTEGER))) 219 | # Add exports 220 | for rva, _, expname in pe.getExports(): 221 | self.vw.addExport( 222 | va + rva, vivisect.const.EXP_UNTYPED, expname, '') 223 | # Add imports 224 | for rva, lname, iname in pe.getImports(): 225 | if self.vw.probeMemory(rva + va, 4, envi.memory.MM_READ): 226 | self.vw.makeImport(rva + va, lname, iname) 227 | 228 | self.vw._snapInAnalysisModules() 229 | 230 | # save the analysis 231 | self.vw.setMeta("StorageModule", "vivisect.storage.basicfile") 232 | self.vw.setMeta("StorageName", storage_fname) 233 | 234 | self.vw.analyze() 235 | self.vw.saveWorkspace() 236 | print " [+] vivisect workspace load complete." 237 | 238 | """ 239 | Disassmebly 240 | """ 241 | 242 | def disasm(self, va): 243 | """ 244 | Use vivisect disassembler to disassemble at a specific virtual address, only supporting i386/amd64 245 | 246 | Parameters: 247 | va : virtual address to disassemble 248 | 249 | Returns: vivisect opcode 250 | """ 251 | if self.arch == winappdbg.win32.ARCH_I386: 252 | import envi.archs.i386 253 | d = envi.archs.i386.disasm.i386Disasm() 254 | else: 255 | import envi.archs.amd64 256 | d = envi.archs.amd64.disasm.Amd64Disasm() 257 | try: 258 | bytes = self.read_memory(va, 16) 259 | ret = d.disasm(bytes, 0, va) 260 | except AccessViolationException: 261 | ret = None 262 | return ret 263 | 264 | def wdbg_get_mnem(self, va): 265 | """ 266 | Use windbg disassembler to disassemble at a specific address and return only the mnem. Much faster than using 267 | self.disasm, but only gets the mnem. 268 | 269 | Args: 270 | va: virtual address to disassemble 271 | 272 | Returns: text opcode 273 | 274 | """ 275 | mnem = None 276 | d = pykd.disasm(va) 277 | inst = d.instruction() 278 | inst_elements = filter(None, inst.split(' ')) 279 | if len(inst_elements) > 2: 280 | mnem = inst_elements[2] 281 | return mnem 282 | 283 | """ 284 | Memory snapshots/diffing 285 | """ 286 | 287 | def snapshot_memory(self, start_va, end_va): 288 | """ 289 | Creates a memory snapshot. 290 | 291 | Parameters: 292 | start_va : starting virtual address 293 | end_va : ending virtual address 294 | 295 | Returns: dictionary containing start/end virtual address, length, and byte string 296 | """ 297 | length = end_va - start_va 298 | snapshot = {} 299 | if length > 0: 300 | snapshot['start_va'] = start_va 301 | snapshot['end_va'] = end_va 302 | snapshot['length'] = length 303 | snapshot['bytes'] = '' 304 | 305 | try: 306 | mem_bytes = self.read_memory(start_va, length) 307 | except AccessViolationException: 308 | print ' [-] Error reading bytes!' 309 | mem_bytes = '' 310 | 311 | snapshot['bytes'] = mem_bytes 312 | 313 | return snapshot 314 | 315 | def compare_memory(self, orig, new): 316 | """ 317 | Compare two memory snapshots, an original snapshot and a new snapshot. 318 | 319 | Parameters: 320 | orig : first snapshot 321 | new : second snapshot 322 | 323 | Returns: dictionary containing virtual addresses and modified byte string 324 | """ 325 | diff = {} 326 | if 'bytes' in orig and 'bytes' in new and 'length' in orig: 327 | if orig['length'] == len(orig['bytes']) == len(new['bytes']) > 0: 328 | if orig['start_va']: 329 | run_start = 0 330 | 331 | for i in range(len(orig['bytes'])): 332 | if new['bytes'][i] != orig['bytes'][i]: 333 | if run_start == 0: 334 | run_start = orig['start_va'] + i 335 | diff[run_start] = [] 336 | diff[run_start].append(new['bytes'][i]) 337 | else: 338 | run_start = 0 339 | return diff 340 | 341 | """ 342 | Memory functions 343 | Read 344 | Write 345 | Allocate 346 | Free 347 | """ 348 | 349 | def is_memory_readable(self, va, length): 350 | """ 351 | Wrapper for reable memory check. 352 | 353 | Parameters: 354 | va : virtual address to check 355 | length : length of memory to check 356 | 357 | Returns: bool - True if readable, False if not 358 | """ 359 | return self.process.is_buffer_readable(va, length) 360 | 361 | def is_memory_writeable(self, va, length): 362 | """ 363 | Wrapper for writeable memory check. 364 | 365 | Parameters: 366 | va : virtual address to check 367 | length : length of memory to check 368 | 369 | Returns: bool - True if writeable, False if not 370 | """ 371 | return self.process.is_buffer_writeable(va, length) 372 | 373 | def is_memory_executable(self, va, length): 374 | """ 375 | Wrapper for executable memory check. 376 | 377 | Parameters: 378 | va : virtual address to check 379 | length : length of memory to check 380 | 381 | Returns: bool - True if executable, False if not 382 | """ 383 | return self.process.is_buffer_executable(va, length) 384 | 385 | def read_memory(self, va, length): 386 | """ 387 | Memory reader wrapper. 388 | 389 | Parameters: 390 | va : virtual address to read 391 | length : length to read 392 | 393 | Returns: read bytes 394 | """ 395 | if va and self.is_memory_readable(va, length): 396 | bytes = self.process.read(va, length) 397 | else: 398 | raise AccessViolationException(va) 399 | return bytes 400 | 401 | def read_char(self, va): 402 | """ 403 | Read single byte. 404 | 405 | Parameters: 406 | va : virtual address of char 407 | 408 | Returns: char as integer 409 | """ 410 | return ord(self.read_memory(va, 1)) 411 | 412 | def read_byte(self, va): 413 | """ 414 | Read single byte. 415 | 416 | Parameters: 417 | va : virtual address of char 418 | 419 | Returns: char 420 | """ 421 | return self.read_char(va) 422 | 423 | def read_word(self, va): 424 | """ 425 | Read two byte word 426 | 427 | Parameters: 428 | va : virtual address of word 429 | 430 | Returns: word 431 | """ 432 | word = self.read_memory(va, 2) 433 | return struct.unpack(' 0: 1288 | if addr + i in export_addrs: 1289 | break 1290 | paddr = struct.pack(fmt, addr + i) 1291 | most_bytes = paddr[2:] 1292 | if most_bytes in fast_search: 1293 | fast_search[most_bytes].append((paddr[:2], addr)) 1294 | else: 1295 | fast_search[most_bytes] = [] 1296 | fast_search[most_bytes].append((paddr[:2], addr)) 1297 | 1298 | for search_bytes, small_bytes_list in fast_search.iteritems(): 1299 | current_offset = 0 1300 | found_offset = 0 1301 | while found_offset >= 0: 1302 | found_offset = mem_bytes[current_offset:].find(search_bytes) - 2 1303 | 1304 | if found_offset >= 0: 1305 | current_offset += found_offset 1306 | 1307 | current_small_bytes = mem_bytes[current_offset:current_offset + 2] 1308 | exp_addr = 0 1309 | for small_bytes, addr in small_bytes_list: 1310 | if small_bytes == current_small_bytes: 1311 | exp_addr = addr 1312 | break 1313 | if exp_addr: 1314 | found_addr = base_va + current_offset 1315 | 1316 | for dll, exp in module_exports.iteritems(): 1317 | if exp_addr in exp: 1318 | func_dll = dll 1319 | func_name = exp[exp_addr] 1320 | imports[found_addr] = {} 1321 | import_func = {} 1322 | import_func["func_name"] = func_name 1323 | import_func["func_dll"] = func_dll 1324 | 1325 | imports[found_addr] = import_func 1326 | current_offset += 3 1327 | 1328 | return imports 1329 | 1330 | def resolve_func_by_addr(self, va): 1331 | """ 1332 | Find exported function name by address. Search 8 bytes in to the function. 1333 | 1334 | Parameters: 1335 | va : virtual address of exported function 1336 | 1337 | Returns: function name or empty string if nothing found 1338 | """ 1339 | exports = self.get_exports() 1340 | func_name = '' 1341 | for dll, exp in exports.iteritems(): 1342 | for i in range(8): 1343 | if va - i in exp: 1344 | func_name = exp[va - i] 1345 | break 1346 | if func_name != '': 1347 | break 1348 | return func_name 1349 | 1350 | def resolve_addr_by_func_name(self, func_name): 1351 | """ 1352 | Reverse resolve, get the address given the function name. 1353 | 1354 | Parameters: 1355 | func_name : function name 1356 | 1357 | Returns: address for function name 1358 | """ 1359 | exports = self.get_exports() 1360 | addr = 0 1361 | for dll, exp in exports.iteritems(): 1362 | try: 1363 | addr = (addr for addr, name in exp.items() 1364 | if name == func_name).next() 1365 | except StopIteration: 1366 | continue 1367 | if addr != 0: 1368 | break 1369 | return addr 1370 | 1371 | def get_allocation_base(self, va): 1372 | """ 1373 | Try to find the base address given a virtual address. 1374 | 1375 | Parameters: 1376 | va : virtual address 1377 | 1378 | Returns: allocation base virtual address 1379 | """ 1380 | mbi = self.process.mquery(va) 1381 | return mbi.AllocationBase 1382 | 1383 | def find_contiguous_memory_size(self, va): 1384 | """ 1385 | Looking for contiguous memory regions. 1386 | 1387 | Parameters: 1388 | va : virtual address 1389 | 1390 | Returns: beginning virtual address and size 1391 | """ 1392 | begin_va = self.get_allocation_base(va) 1393 | va = begin_va 1394 | size = 0 1395 | if begin_va > 0: 1396 | while True: 1397 | mbi = self.process.mquery(va) 1398 | if not mbi.is_commited(): 1399 | break 1400 | if mbi.AllocationBase != begin_va: 1401 | break 1402 | size += mbi.RegionSize 1403 | va += mbi.RegionSize 1404 | return begin_va, size 1405 | 1406 | def get_process_region_bytes(self, va): 1407 | """ 1408 | Get the bytes from a memory region. 1409 | 1410 | Parameters: 1411 | va : virtual address 1412 | 1413 | Returns: memory region bytes 1414 | """ 1415 | begin_va = self.get_allocation_base(va) 1416 | va = begin_va 1417 | bytes = '' 1418 | 1419 | if begin_va > 0: 1420 | while True: 1421 | mbi = self.process.mquery(va) 1422 | if mbi.AllocationBase != begin_va: 1423 | break 1424 | elif mbi.is_commited(): 1425 | bytes += self.process.read(va, mbi.RegionSize) 1426 | elif mbi.is_reserved(): 1427 | bytes += '\0' * mbi.RegionSize 1428 | elif mbi.is_free(): 1429 | break 1430 | va += mbi.RegionSize 1431 | 1432 | return bytes 1433 | 1434 | def get_exports(self): 1435 | """ 1436 | Get's all module exports. 1437 | 1438 | Returns: module exports 1439 | """ 1440 | if not self.module_exports: 1441 | self.process.scan_modules() 1442 | self.module_exports = {} 1443 | for module in self.process.iter_modules(): 1444 | exports = self.get_module_exports(module) 1445 | name = module.get_name() 1446 | self.module_exports[name] = exports 1447 | else: 1448 | self.process.scan_modules() 1449 | for module in self.process.iter_modules(): 1450 | name = module.get_name() 1451 | if name not in self.module_exports: 1452 | exports = self.get_module_exports(module) 1453 | self.module_exports[name] = exports 1454 | return self.module_exports 1455 | 1456 | def get_module_exports(self, module): 1457 | """ 1458 | Walks the export table using a vivisect PE object. 1459 | 1460 | Parameters: 1461 | module : winappdbg module object 1462 | 1463 | Returns: all modules exports 1464 | """ 1465 | process = self.process 1466 | exports = {} 1467 | read_failed = False 1468 | 1469 | # Image base 1470 | base = module.get_base() 1471 | size = module.get_size() 1472 | 1473 | try: 1474 | dll_mem = self.process.read(base, size) 1475 | except: 1476 | read_failed = True 1477 | 1478 | if not read_failed: 1479 | memobj = envi.memory.MemoryObject() 1480 | memobj.addMemoryMap(base, envi.memory.MM_RWX, "", dll_mem) 1481 | pe = PE.peFromMemoryObject(memobj, base) 1482 | else: 1483 | pe = PE.peFromFileName(module.get_filename()) 1484 | 1485 | for rva, _, func_name in pe.getExports(): 1486 | exports[base + rva] = func_name 1487 | 1488 | 1489 | return exports 1490 | 1491 | 1492 | class ModuleUtils: 1493 | """ 1494 | Some module specific functions 1495 | """ 1496 | 1497 | def __init__(self, dbg, pi, ignore_base_addrs=[]): 1498 | """ 1499 | Initializes the ModuleUtils class. 1500 | 1501 | Parameters: 1502 | dbg : DebugUtils object 1503 | pi : ProcessUtils object 1504 | ignore_base_addrs : modules to ignore 1505 | """ 1506 | self._module_mbis = {} 1507 | self.dbg = dbg 1508 | self.pi = pi 1509 | self.process = self.dbg.process 1510 | if type(ignore_base_addrs) in (int, long): 1511 | ignore_base_addrs = [ignore_base_addrs] 1512 | self.ignore_base_addrs = ignore_base_addrs 1513 | 1514 | def get_module_mbis(self): 1515 | """ 1516 | Gets the executable sections of a module, but ignores a module within a given ignore_base_addrs. 1517 | 1518 | Returns: dictionary of loaded module mbis 1519 | """ 1520 | self.process.scan_modules() 1521 | if self.process.get_module_count() != len(self._module_mbis): 1522 | mem_map = self.process.get_memory_map() 1523 | for module in self.process.iter_modules(): 1524 | mod_beg = module.get_base() 1525 | if mod_beg not in self._module_mbis.keys(): 1526 | self._module_mbis[mod_beg] = [] 1527 | mod_end = mod_beg + module.get_size() 1528 | for mbi in mem_map: 1529 | if mod_beg <= mbi.BaseAddress < mod_end: 1530 | self._module_mbis[mod_beg].append( 1531 | (mbi.BaseAddress, mbi.BaseAddress + mbi.RegionSize, mbi.is_executable())) 1532 | return self._module_mbis 1533 | 1534 | def get_module_exec_sections(self, use_ignore_list=True): 1535 | """ 1536 | Get memory regions within a module marked executable. 1537 | 1538 | Parameters: 1539 | use_ignore_list : bool to use the ignore list 1540 | 1541 | Returns: executable mbis 1542 | """ 1543 | exec_sections = [] 1544 | # if not self._module_mbis: 1545 | self.get_module_mbis() 1546 | for mod_beg, mbis in self._module_mbis.iteritems(): 1547 | if use_ignore_list and mod_beg in self.ignore_base_addrs: 1548 | continue 1549 | for start, end, executable in mbis: 1550 | if executable: 1551 | exec_sections.append((start, end)) 1552 | return exec_sections 1553 | 1554 | def get_module_exec_section_bases(self, use_ignore_list=True): 1555 | """ 1556 | Get memory region base addresses within modules marked executable. 1557 | 1558 | Returns: the base addresses for each executable mbi 1559 | """ 1560 | exec_sections = self.get_module_exec_sections(use_ignore_list) 1561 | return [base[0] for base in exec_sections] 1562 | 1563 | def exec_module_func(self, va): 1564 | """ 1565 | Checks if the VA is in the range of a library function, if it is, execute the library function and return to the caller. 1566 | 1567 | Parameters: 1568 | va : virtual address 1569 | """ 1570 | lib_addrs = self.get_module_exec_sections() 1571 | for start, end in lib_addrs: 1572 | if start <= va < end: 1573 | self.dbg.step_out() 1574 | break 1575 | if 'LoadLibrary' in self.pi.resolve_func_by_addr(va): 1576 | self.get_module_mbis() 1577 | 1578 | def module_step_out(self): 1579 | """ 1580 | Steps out of current executable memory region. 1581 | """ 1582 | out = [] 1583 | pc = self.dbg.get_pc() 1584 | for mbi in self.process.get_memory_map(): 1585 | if not (mbi.BaseAddress <= pc < mbi.BaseAddress + mbi.RegionSize): 1586 | out.append(mbi.BaseAddress) 1587 | self.dbg.set_mem_breakpoint(out) 1588 | 1589 | 1590 | """ 1591 | Custom exceptions 1592 | """ 1593 | 1594 | 1595 | class AccessViolationException(Exception): 1596 | """ 1597 | Access violation exceptions 1598 | """ 1599 | 1600 | def __init__(self, va): 1601 | """ 1602 | Handles access violation exceptions. 1603 | 1604 | Parameters: 1605 | va : virtual address of access violation 1606 | """ 1607 | self.va = va 1608 | 1609 | def __str__(self): 1610 | return "Access Violation occurred at virtual address: 0x%x" % (self.va) 1611 | 1612 | 1613 | class SecondChanceException(Exception): 1614 | """ 1615 | Second chance exceptions 1616 | """ 1617 | pass 1618 | 1619 | 1620 | class ControlBreakException(Exception): 1621 | """ 1622 | Ctrl+Break received 1623 | """ 1624 | pass 1625 | 1626 | 1627 | """ 1628 | Exception handlers 1629 | """ 1630 | 1631 | 1632 | class BreakpointExceptionHandler(pykd.eventHandler): 1633 | """ 1634 | Set breakpoints and break when one is hit. 1635 | """ 1636 | 1637 | def __init__(self, addrs): 1638 | """ 1639 | Initializes the breakpoint exception handler. 1640 | 1641 | Parameters: 1642 | addrs : list of virtual addresses to set breakpoints 1643 | """ 1644 | pykd.eventHandler.__init__(self) 1645 | self.pykd_version = get_pykd_version() 1646 | self.bp_hit_addr = None 1647 | self.except_addr = None 1648 | self.ctrlbr = False 1649 | self.av = False 1650 | if type(addrs) in (int, long): 1651 | addrs = [addrs] 1652 | self.bp_ids = {} 1653 | self.set_breakpoints(addrs) 1654 | 1655 | def __del__(self): 1656 | self.remove_breakpoints() 1657 | 1658 | def set_breakpoints(self, addrs): 1659 | """ 1660 | Sets breakpoints. 1661 | 1662 | Parameters: 1663 | addrs : list of virtual addresses to set breakpoints 1664 | """ 1665 | for addr in list(set(addrs)): 1666 | bp = pykd.setBp(addr) 1667 | if self.pykd_version == PYKD3: 1668 | bp_id = bp.getId() 1669 | self.bp_ids[bp_id] = bp 1670 | else: 1671 | self.bp_ids[bp] = addr 1672 | 1673 | def remove_breakpoints(self): 1674 | """ 1675 | Removes all breakpoints that were set. 1676 | """ 1677 | for bp_id in self.bp_ids.keys(): 1678 | if self.pykd_version == PYKD3: 1679 | self.bp_ids[bp_id].remove() 1680 | else: 1681 | pykd.removeBp(bp_id) 1682 | del self.bp_ids[bp_id] 1683 | 1684 | def onBreakpoint(self, bp_id): 1685 | """ 1686 | Handles onBreakpoint event. 1687 | 1688 | Parameters: 1689 | bp_id : breakpoint id 1690 | 1691 | Returns: pykd Break event 1692 | """ 1693 | if bp_id in self.bp_ids.keys(): 1694 | if self.pykd_version == PYKD3: 1695 | self.bp_hit_addr = self.bp_ids[bp_id].getOffset() 1696 | else: 1697 | self.bp_hit_addr = self.bp_ids[bp_id] 1698 | self.remove_breakpoints() 1699 | 1700 | return pykd.eventResult.Break 1701 | 1702 | # Exception callback 1703 | def onException(self, exceptInfo): 1704 | """ 1705 | Handles exceptions. 1706 | 1707 | Parameters: 1708 | exceptInfo : pykd ExceptInfo object 1709 | 1710 | Returns: pykd event 1711 | """ 1712 | ret = pykd.eventResult.NoChange 1713 | except_code = 0 1714 | cexcept_addr = 0 1715 | first_chance = False 1716 | 1717 | if self.pykd_version == PYKD3: 1718 | except_code = exceptInfo.exceptionCode 1719 | cexcept_addr = exceptInfo.exceptionAddress 1720 | first_chance = exceptInfo.firstChance 1721 | else: 1722 | except_code = exceptInfo.ExceptionCode 1723 | cexcept_addr = exceptInfo.ExceptionAddress 1724 | first_chance = exceptInfo.FirstChance 1725 | 1726 | if except_code == winappdbg.win32.kernel32.STATUS_BREAKPOINT: 1727 | self.ctrlbr = True 1728 | self.remove_breakpoints() 1729 | ret = pykd.eventResult.Break 1730 | elif except_code == winappdbg.win32.kernel32.STATUS_ACCESS_VIOLATION: 1731 | self.av = True 1732 | self.except_addr = cexcept_addr 1733 | self.remove_breakpoints() 1734 | ret = pykd.eventResult.Break 1735 | 1736 | return ret 1737 | 1738 | 1739 | class PageGuardExceptionHandler(pykd.eventHandler): 1740 | """ 1741 | PAGE_GUARD exception handler for memory breakpoints 1742 | """ 1743 | 1744 | def __init__(self, process, addrs): 1745 | """ 1746 | Initializes the page guard exception handler. 1747 | 1748 | Parameters: 1749 | process : winappdbg process object 1750 | addrs : list of addresses to set page guard protection 1751 | """ 1752 | pykd.eventHandler.__init__(self) 1753 | self.pykd_version = get_pykd_version() 1754 | self.process = process 1755 | self.membps = [] 1756 | if type(addrs) in (int, long): 1757 | addrs = [addrs] 1758 | self.change_protect(addrs) 1759 | self.except_addr = None 1760 | self.bp_hit = False 1761 | self.ctrlbr = False 1762 | self.av = False 1763 | self.mem_access = False 1764 | self.second_chance = False 1765 | 1766 | def change_protect(self, addrs): 1767 | """ 1768 | Sets the PAGE_GUARD protection on all addrs. 1769 | 1770 | Parameters: 1771 | addrs : list of addresses to set the PAGE_GUARD protection 1772 | """ 1773 | for addr in addrs: 1774 | membp = {} 1775 | mbi = self.process.mquery(addr) 1776 | if mbi.is_executable() and not mbi.is_mapped(): 1777 | membp['size'] = mbi.RegionSize 1778 | membp['base_addr'] = mbi.BaseAddress 1779 | membp['end_addr'] = mbi.BaseAddress + mbi.RegionSize 1780 | membp['old_protect'] = self.process.mprotect(membp['base_addr'], membp['size'], 1781 | mbi.Protect | winappdbg.win32.PAGE_GUARD) 1782 | self.membps.append(membp) 1783 | 1784 | def reset_protect(self): 1785 | """ 1786 | Removes the PAGE_GUARD protection on all specified addrs. 1787 | """ 1788 | for membp in self.membps: 1789 | mbi = self.process.mquery(membp['base_addr']) 1790 | if not mbi.is_free(): 1791 | self.process.mprotect(membp['base_addr'], membp['size'], membp['old_protect']) 1792 | 1793 | # Exception callback 1794 | def onException(self, exceptInfo): 1795 | """ 1796 | Handles exceptions. 1797 | 1798 | Parameters: 1799 | exceptInfo : pykd ExceptInfo object 1800 | 1801 | Returns: pykd event 1802 | """ 1803 | ret = pykd.eventResult.NoChange 1804 | except_code = 0 1805 | cexcept_addr = 0 1806 | first_chance = False 1807 | self.mem_access = False 1808 | 1809 | if self.pykd_version == PYKD3: 1810 | except_code = exceptInfo.exceptionCode 1811 | cexcept_addr = exceptInfo.exceptionAddress 1812 | first_chance = exceptInfo.firstChance 1813 | else: 1814 | except_code = exceptInfo.ExceptionCode 1815 | cexcept_addr = exceptInfo.ExceptionAddress 1816 | first_chance = exceptInfo.FirstChance 1817 | 1818 | # Handle guard page exceptions and check if the execution address is in 1819 | # our range, so we don't deal with memory access breaks 1820 | if except_code == winappdbg.win32.kernel32.STATUS_GUARD_PAGE_VIOLATION: 1821 | ret = pykd.eventResult.Break 1822 | self.mem_access = True 1823 | self.except_addr = cexcept_addr 1824 | self.reset_protect() 1825 | for membp in self.membps: 1826 | if membp['base_addr'] <= cexcept_addr < membp['end_addr']: 1827 | self.bp_hit = True 1828 | self.mem_access = False 1829 | break 1830 | 1831 | # If Ctrl+Break was issued, reset protections and prepare to exit 1832 | elif except_code == winappdbg.win32.kernel32.STATUS_BREAKPOINT: 1833 | self.bp_hit = True 1834 | self.ctrlbr = True 1835 | self.reset_protect() 1836 | ret = pykd.eventResult.Break 1837 | # Not dealing with second chance exceptions 1838 | elif not first_chance: 1839 | self.bp_hit = True 1840 | self.second_chance = True 1841 | self.reset_protect() 1842 | ret = pykd.eventResult.Break 1843 | elif except_code == winappdbg.win32.kernel32.STATUS_ACCESS_VIOLATION: 1844 | self.bp_hit = True 1845 | self.av = True 1846 | self.except_addr = cexcept_addr 1847 | self.reset_protect() 1848 | ret = pykd.eventResult.Break 1849 | 1850 | return ret 1851 | 1852 | 1853 | class AccessViolationExceptionHandler(pykd.eventHandler): 1854 | """ 1855 | Access violation exception handler 1856 | """ 1857 | 1858 | def __init__(self, process=None, addrs=[]): 1859 | """ 1860 | Initializes access violation exception handler. 1861 | 1862 | Parameters: 1863 | process : (optional) winappdbg process object 1864 | addrs : list of virtual addresses 1865 | """ 1866 | pykd.eventHandler.__init__(self) 1867 | self.pykd_version = get_pykd_version() 1868 | self.process = process 1869 | self.membps = [] 1870 | if type(addrs) in (int, long): 1871 | addrs = [addrs] 1872 | self.change_protect(addrs) 1873 | self.except_addr = None 1874 | self.bp_hit = False 1875 | self.av = False 1876 | self.ctrlbr = False 1877 | self.second_chance = False 1878 | 1879 | def change_protect(self, addrs): 1880 | """ 1881 | Removes the executable protection on all addrs. 1882 | 1883 | Parameters: 1884 | addrs : list of virutal addresses 1885 | """ 1886 | for addr in addrs: 1887 | membp = {} 1888 | mbi = self.process.mquery(addr) 1889 | if mbi.is_executable(): 1890 | membp['size'] = mbi.RegionSize 1891 | membp['base_addr'] = mbi.BaseAddress 1892 | membp['end_addr'] = mbi.BaseAddress + mbi.RegionSize 1893 | membp['old_protect'] = self.process.mprotect(membp['base_addr'], membp['size'], 1894 | winappdbg.win32.PAGE_READWRITE) 1895 | self.membps.append(membp) 1896 | 1897 | def reset_protect(self): 1898 | """ 1899 | Resets the original protection on all specified addrs. 1900 | """ 1901 | for membp in self.membps: 1902 | mbi = self.process.mquery(membp['base_addr']) 1903 | if not mbi.is_free(): 1904 | self.process.mprotect(membp['base_addr'], membp['size'], membp['old_protect']) 1905 | 1906 | # Exception callback 1907 | def onException(self, exceptInfo): 1908 | """ 1909 | Handles exceptions. 1910 | 1911 | Parameters: 1912 | exceptInfo : pykd ExceptInfo object 1913 | 1914 | Returns: pykd event 1915 | """ 1916 | ret = pykd.eventResult.NoChange 1917 | except_code = 0 1918 | cexcept_addr = 0 1919 | first_chance = False 1920 | 1921 | if self.pykd_version == PYKD3: 1922 | except_code = exceptInfo.exceptionCode 1923 | cexcept_addr = exceptInfo.exceptionAddress 1924 | first_chance = exceptInfo.firstChance 1925 | else: 1926 | except_code = exceptInfo.ExceptionCode 1927 | cexcept_addr = exceptInfo.ExceptionAddress 1928 | first_chance = exceptInfo.FirstChance 1929 | 1930 | # Handle acces violation errors 1931 | if except_code == winappdbg.win32.kernel32.STATUS_ACCESS_VIOLATION: 1932 | ret = pykd.eventResult.Break 1933 | self.reset_protect() 1934 | self.except_addr = cexcept_addr 1935 | for membp in self.membps: 1936 | if membp['base_addr'] <= cexcept_addr < membp['end_addr']: 1937 | self.bp_hit = True 1938 | break 1939 | if not self.bp_hit: 1940 | self.av = True 1941 | 1942 | # If Ctrl+Break was issued, reset protections and prepare to exit 1943 | elif except_code == winappdbg.win32.kernel32.STATUS_BREAKPOINT: 1944 | self.bp_hit = True 1945 | self.ctrlbr = True 1946 | self.reset_protect() 1947 | ret = pykd.eventResult.Break 1948 | # Not dealing with second chance exceptions 1949 | elif not first_chance: 1950 | self.bp_hit = True 1951 | self.second_chance = True 1952 | self.reset_protect() 1953 | ret = pykd.eventResult.Break 1954 | 1955 | return ret --------------------------------------------------------------------------------