#!/usr/bin/env python
##WingHeader v1
###############################################################################
## File : REpdb.py
## Description: pyREtic extensions to the standard Python debugger (pdb)
## : focussing on use for reverse engineering bytecode/vuln hunting
## Created_On : Tue Aug 3 20:24:32 2010
## Created_By : Rich Smith
## Modified_On: Mon Dec 6 12:12:03 2010
## Modified_By: Rich Smith
## License : GPLv3 (Docs/LICENSE.txt)
##
## (c) Copyright 2010, Rich Smith all rights reserved.
###############################################################################
__author__ = "Rich Smith"
__version__= "0.5"
##Use libs we control in preference to the packed runtime's
import sys
import os.path
##Allows easy relative writes to the location this dir later
MODULE_LOCATION = os.path.dirname(__file__)
sys.path = [os.path.join(MODULE_LOCATION, "Projects", "default", "libs")] + sys.path
##Imports
import os
import pdb
import time
import struct
import atexit
import shutil
import urllib
import tarfile
import subprocess
##Third party imports for extra functionality in the debugger context
try:
from ThirdParty import pycallgraph
CAN_CALLGRAPH = True
except:
CAN_CALLGRAPH = False
#TODO - CONFIG FILE WITH ALL THIS STUFF IN & make OS specific
RUNTIME_LOCATION = {"2.4" : "/usr/bin/python2.4",
"2.5" : "/usr/bin/python2.5",
"2.6" : "/usr/bin/python2.6",
"2.7" : "/usr/bin/python2.7"
}
##pyREtic modules
import pyREtic
[docs]class RePdb(pdb.Pdb):
"""
Extended pdb debugger with functionality useful when reverse engineering
Python .pyc/.pyo files without the .py source
"""
def __init__(self, completekey='tab', stdin=None, stdout=None):
##initiate Pdb
pdb.Pdb.__init__(self, completekey, stdin, stdout)
global MODULE_LOCATION
##Locations of reference/obfuscated .pyb's
self.ref_pyb = None
self.obf_pyb = None
self.runtime_version = "default"
self.pyretic = pyREtic.pyREtic()
##Get default project name from pyREtic
self.projectdir = self.pyretic.get_projectroot()
self.ref_pyb_generated = False
self.callgraph_started = False
self.cg_exclude = ['pycallgraph.*']
self.do_module_restore = False
self.runtime_remapped = False
self.remap_complete()
self.prompt = '(REpdb:%s) '%(self.pyretic.get_project())
print "\n%s\nREpdb is part of the pyREtic toolkit. %s 2010\n%s\n"%("="*53, __author__, "="*53)
def __reload_modules(self):
"""
This function reloads all the modules in the libs dir of the project that
has been switched to + some other modules that require it.
*Don't mess with this as it can introduce subtle bugs that are annoying to debug*
"""
##Ordered reload
must_reload = ["opcode", "dis", "opcodes"]
for mr in must_reload:
try:
exec("%s = reload(%s)"%(mr, mr))
#print "[+] Reloaded '%s'"%(mr)
except Exception, err:
#print "[-] Problem reloading '%s' : %s"%(mr, err)
pass
##Unordered reload
lib_modules = os.listdir(self.pyretic.get_project_mod_dir())
for m in lib_modules:
if m not in must_reload and m != "__init__.py":
mod_name, ext = os.path.splitext(m)
if ext != ".py":
#print "[-] none .py '%s' in project directory, skipping"%(m)
continue
try:
exec("%s = reload(%s)"%(mod_name, mod_name))
#395print "[+] Reloaded '%s'"%(mod_name)
except Exception, err:
#print "[-] Problem reloading '%s' : %s"%(mod_name, err)
pass
[docs] def do_show(self, args):
"""
Print out all the current settings for this REpdb project
Usage: show
"""
self.do_is_remapped()
print """
Current project name: %s
Current project directory: %s
Project runtime set as: %s
Decompiler: %s
Python paths: %s
Runtime opcode remapped: %s
Reference modules: %s
Obfuscated modules: %s
Remap done: %s
Callgraph started: %s
Current runtime reported as: %s
"""%(self.pyretic.get_project(), self.pyretic.get_dump_dir(), self.runtime_version,
self.pyretic.decompiler, RUNTIME_LOCATION, self.runtime_remapped,
os.path.join(self.pyretic.get_projectdir(), "pybs", "ref_pyb"),
os.path.join(self.pyretic.get_projectdir(), "pybs", "obj_pyb"),
self.ref_pyb_generated, self.callgraph_started,
sys.version.replace("\n", ""))
[docs] def remap_complete(self):
"""
For the current project has a re-mapped opcode table been generated, set var accordingly ?
"""
try:
os.stat(os.path.join(self.pyretic.get_project_mod_dir(), "opcodes_remap.py"))
self.ref_pyb_generated = True
except OSError:
self.ref_pyb_generated = False
[docs] def do_is_remapped(self, args = None):
"""
Determine if the current runtime has remapped its opcode table
Usage: is_remapped
"""
##Code to compile
test_py = "print 'Am I remapped?'"
##The bytecode that should be produced if the runtime is NOT remapped
expected_bc = "\x64\x00\x00\x47\x48\x64\x01\x00\x53"
try:
current_bc = compile(test_py, "<pyREtic test>", "exec")
except Exception, err:
print "[-] Problem compiling bytecode: %s"%(err)
print "[?] Runtime may have been modified to mess with the compile() function ?"
self.runtime_remapped = True
if expected_bc != current_bc:
print "[!] Runtime appears to be opcode remapped"
self.runtime_remapped = True
else:
print "[=] Runtime prodcued expected bytecode it doesn't seem to be remapped"
self.runtime_remapped = False
[docs] def do_set_version(self, version):
"""
Set the version number of the runtime we are running in to a specific value
Usage: set_version 2.5.4
"""
if not version:
print "[-] No version given to set"
self.runtime_version = version
#TODO - set correct libs for project ?
print "[+] Python version set as: %s"%(version)
def _autodetect_version(self, always_prompt = False):
"""
Interrogate the runtime 3 different ways to try and get an idea of what
the running python's version is
Prompts user for their choice
returns the version as a string
"""
choices = []
##Get the version according to the builtin sys module
##Derived from PY_VERSION defined in patchlevel.h
sys_mod_ver = sys.version.split()[0]
##Derived from PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION defined in patchlevel.h
sys_mod_ver_info = "%s.%s.%s"%(sys.version_info[0],sys.version_info[1],sys.version_info[2])
print "[=] sys.version reports version: %s"%(sys_mod_ver)
print "[=] sys.version_info reports version: %s"%(sys_mod_ver_info)
choices.append(sys_mod_ver_info)
##First compare - has to be an 'in' compare as sys_mod_ver may not have a .0 micro version
if not sys_mod_ver in sys_mod_ver_info:
print "[-] sys.version does not match sys.version_info"
choices.append(sys_mod_ver)
##Get the magic number from a pyc
magic_dict = self._get_magic()
if magic_dict.has_key(None):
##A problem was encountered
print "[-] Problem when getting pyc magic"
print "[-] %s"%(magic_dict[None])
else:
print "[=] pyc magic number reports version: %s [magic: %s]"%(magic_dict.values()[0], magic_dict.keys()[0][0])
##Now compare sys.version to the magic byte version
if not magic_dict.values()[0] in sys_mod_ver_info:
print "[!] pyc magic does not match sys.version_info - *indication of obfuscation/opcode remapping*"
closest_ver = self.closest(magic_dict.keys()[0][0], self.known_magic.keys())
print "[=] pyc magic value: %s - closest value of a known runtime: %s [magic: %s]"%(magic_dict.keys()[0][0],self.known_magic[closest_ver],closest_ver)
choices.append(self.known_magic[closest_ver])
##If there were not discrepencies and always_prompt not set return version
if len(choices) == 1 and not always_prompt:
print "[+] Python version detected as: %s. Setting..."%(choices[0])
return choices[0]
##Else promptt user to choose
print "[=] Please chose which runtime version to set:"
for c in choices:
print "\t [%s] %s"%(choices.index(c), c)
print "\t [%s] Enter own version value to use"%(len(choices))
user_choice = raw_input("Enter number of version to use: ")
if user_choice == str(len(choices)):
own_ver = raw_input("Please enter version to set (e.g. 2.5.4): ")
return own_ver
elif not user_choice.isdigit() or int(user_choice) > len(choices)-1 or int(user_choice) < 0:
print "[-] Invalid choice entered: %s"%(user_choice)
return None
else:
return choices[int(user_choice)]
[docs] def do_detect_version(self, args):
"""
Try to determine the version of the running Python (the runtime we are injected into)
If there is a mismatch between the different ways we can do this you will be prompted
to choose for yourself
Usage: detect_version
"""
p_ver = self._autodetect_version()
if not p_ver:
return
else:
self.do_set_version(p_ver)
[docs] def closest(self, target, collection):
"""
For a list of integers and a given value find the list element it's closest to
"""
return min((abs(target - i), i) for i in collection)[1]
def _get_magic(self):
"""
Get the 'magic number' from the pyc file
"""
## Walk the sys.modules table until we get a file location of a loaded module
path = None
for m in sys.modules.values():
if not m:
continue
try:
path = m.__file__
if os.path.splitext(path)[-1] in [".pyc", ".pyo"]:
break
except:
continue
if not path:
return {None : "No pyc module could be found"}
##Read the first 2 magic bytes
try:
fo = open(path, "rb")
magic = struct.unpack( "<H", fo.read(2))
fo.close()
except Exception, err:
return {None : "Error accessing '%s' : %s"%(path, err)}
##Compare to table of values derived from import.c
self.known_magic = {20121:"1.5", #or 1.5.1 or 1.5.2
50428:"1.6",
50823:"2.0", #or 2.0.1
60202:"2.1", #or 2.1.1, 2.1.2
60717:"2.2",
62011:"2.3",
62021:"2.3",
62041:"2.4",
62051:"2.4",
62061:"2.4",
62071:"2.5",
62081:"2.5",
62091:"2.5",
62092:"2.5",
62101:"2.5",
62111:"2.5",
62121:"2.5",
62131:"2.5",
62151:"2.6",
62161:"2.6",
62171:"2.7",
62181:"2.7",
62191:"2.7",
62201:"2.7",
62211:"2.7"}
try:
pyc_ver = self.known_magic[magic]
except KeyError:
##Unknown magic value - sign of obfuscation / remapping
pyc_ver = "unknown"
return {magic : pyc_ver}
[docs] def do_download_runtime(self, version):
"""
Download the specified Python runtime sourcecode from python.org and decompress it.
It is saved to the 'Downloaded_Runtimes' subdir and shared between all projects
Usage: download_runtime 2.5.4
"""
if not version:
print "[-] No Python version specified, cannot download"
return
elif self.py_ver_downloaded(version):
print "[+] That version has already been downloaded, no need to redownload"
return
target = r"http://www.python.org/ftp/python/%s/Python-%s.tar.bz2"%(version, version)
local = os.path.join(MODULE_LOCATION, "Downloaded_Runtimes","Python-%s.tar.bz2"%(version))
print "[=] Downloading from %s ....."%(target)
try:
fn, msg = urllib.urlretrieve(target, local)
print "[+] Download complete - saved to %s"%(local)
except Exception, err:
print "[-] Problem downloading %s : %s"%(target, err)
return
print "[=] Decompressing...."
try:
tar = tarfile.open(local, "r:bz2")
tar.extractall(path = os.path.join(MODULE_LOCATION, "Downloaded_Runtimes"))
tar.close()
print "[+] Complete"
except:
print "[-] Error decompressing - Check that the version given is a valid runtime at python.org"
[docs] def py_ver_downloaded(self, ver):
"""
Has the specifiedversion of Python already been downloaded ?
"""
try:
os.stat(os.path.join(MODULE_LOCATION, "Downloaded_Runtimes","Python-%s"%(ver)))
return True
except:
return False
[docs] def set_py(self, loc, ver):
"""
Set the location of the python runtime for the specified version
Called from the other do_set_py* functions
"""
global RUNTIME_LOCATION
RUNTIME_LOCATION[ver] = loc
print "[=] Reference Python %s location set to: %s"%(ver, loc)
[docs] def do_set_py24(self, loc):
"""
Reset the location of the standard Python 2.4 runtime used to generate
reference bytecode
Usage: set_py24 /usr/local/bin/python2.4
"""
self.set_py(loc, "2.4")
[docs] def do_set_py25(self, loc):
"""
Reset the location of the standard Python 2.5 runtime used to generate
reference bytecode
Usage: set_py25 /usr/local/bin/python2.5
"""
self.set_py(loc, "2.5")
[docs] def do_set_py26(self, loc):
"""
Reset the location of the standard Python 2.6 runtime used to generate
reference bytecode
Usage: set_py26 /usr/local/bin/python2.6
"""
self.set_py(loc, "2.6")
[docs] def do_set_py27(self, loc):
"""
Reset the location of the standard Python 2.7 runtime used to generate
reference bytecode
Usage: set_py27 /usr/local/bin/python2.7
"""
self.set_py(loc, "2.7")
[docs] def do_set_project(self, name):
"""
Create a new project or switch to an existing one
Usage: set_project <project name>
"""
#TODO - description field
if not name:
print "[-] No project name supplied"
return
elif self.does_project_exist(name):
##Project already exists so just switch to it
self.switch_project(name)
return
print "[=] Please select the Python runtime version to associate wth this project"
print "[=] Automatic version detection "
p_ver = self._autodetect_version()
##Has that version of the Python runtime already been downloaded?
if not self.py_ver_downloaded(p_ver):
print "[=] Python %s has not already been downloaded to the pyREtic cache,"%p_ver
print " if you choose not to download the the standard runtime there"
print " may be difficulties in performing some operations due to module"
print " version mismatches."
choice = raw_input("Do you want to download Python %s now? "%p_ver )
if choice.lower() in ["y", "yes"]:
self.do_download_runtime(p_ver)
else:
print "[-] Not downloading Python"
print "[!] Please copy the correct files for the Python runtime to the %s projects libs directory"
print "[=] Creating new project: '%s'"%(name)
ret = self.pyretic.new_project(name, p_ver)
if ret == False:
print "[-] Problem during project creation, source code output still going to: %s"%(self.pyretic.get_dump_dir())
elif ret == True:
print "[+] Project created. Source code output now going to : %s"%(self.pyretic.get_dump_dir())
self.prompt = '(REpdb:%s) '%(self.pyretic.get_project())
self.do_set_version(p_ver)
self.__reload_modules()
[docs] def switch_project(self, name):
"""
Switch to another project descriptor
"""
if not name:
print "[-] No project name supplied"
return
elif not self.does_project_exist(name):
print "[-] Project does not already exist, cannot switch to it"
return
print "[=] Switching to project '%s'"%(name)
self.pyretic.switch_project(name)
print "[+] Success, sourcecode output now going to %s"%(self.pyretic.get_dump_dir())
self.prompt = '(REpdb:%s) '%(self.pyretic.get_project())
##Read project meta data -
#TODO break into own function as we hold more meta data
try:
f = open(os.path.join(self.pyretic.get_projectdir(), "meta"), "r")
data = f.read()
f.close()
ver = data.split(":")
self.do_set_version(ver[1])
except Exception, err:
print "[-] Problem reading project meta data : %s"%(err)
self.remap_complete()
self.__reload_modules()
[docs] def does_project_exist(self, name):
"""
Determine whether a project of the given name already exists
Return boolean
"""
name = self.pyretic.normalise_path(name)
try:
print os.path.join(self.pyretic.get_projectroot(), name)
os.stat(os.path.join(self.pyretic.get_projectroot(), name))
return True
except:
return False
[docs] def do_setprojectroot(self, location):
"""
Leave the project name the same but change the root directory location
on the filesystem. The currently set project is used as the project to relocate
Usage: set_project_root /tmp/pyretic_dump
"""
if not location:
print "[-] No project root location supplied"
return
print "[=] Setting project root to '%s'"%(location)
if not self.pyretic.set_projectroot(location):
print "[-] Problem during project change, source code output still going to: %s"%(self.pyretic.get_dump_dir())
else:
print "[=] Source code output now going to : %s"%(self.pyretic.get_dump_dir())
self.remap_complete()
[docs] def do_fs_um_decompile(self, path = None):
"""
Decompile obfuscated bytecode by traversing the filesystem from a given
start point and using the current runtimes marshal module to unmarshal
the bytecode of each .pyc found.
Note: If the current obfuscated runtime does not have the marshal module
available then this decompilation technique cannot be used.
usage: fs_um_decompile <path to obfuscated pyc's>
example: fs_um_decompile /tmp/foo.app/Contents/Resources/runtime/site_packages/
"""
if not path:
print "[-] No path to begin decompilation from specified"
return
self.pyretic.fs_unmarshal(path)
[docs] def do_fs_mem_decompile(self, path = None):
"""
Decompile obfuscated bytecode by traversing the filesystem from a given
start point but do NOT rely on the presence of the marshal module to be
able to get thebytecode from the pyc file found. Instead each pyc found
is imported and each of its objects are interogated for their bytecode.
These bytecode objects are what is decompiled.
Note: If the current obfuscated runtime does not have the marshal module
available but you do have access to the filesystem where the obfuscated
pyc's reside then this is the technique to use.
usage: fs_mem_decompile <path to obfuscated pyc's>
example: fs_mem_decompile /tmp/foo.app/Contents/Resources/runtime/site_packages/
"""
if not path:
print "[-] No path to begin decompilation from specified"
return
self.pyretic.fs_objwalk(path)
[docs] def do_pure_mem_decompile(self, obj = None):
"""
Decompile to source code purely from instantiated objects. This assumes
no availability of the marshal module or even access to the filesystem
where the pyc files reside, this decompiles directly from the currently
executing runtimes namespace.
The specified object is decompiled and the objects it contains are traversed
and decompiled until no more remain.
[ Currently available objects can be seen by typing dir() at the REpdb prompt]
usage: pure_mem_decompile <name of object to decompile>
example: pure_mem_decompile AnObjectsName
"""
if not obj:
print "[-] No object to begin decompilation from specified"
return
##We need to access the context of the frame we started the trace from
##NOT our frame we are executing in here - self.curframe holds this from
##bdp
#print "Current executing frame context: ",sys._getframe()
#print "Frame from where the debugger was called: ",self.curframe
locals = self.curframe.f_locals
globals = self.curframe.f_globals
try:
##We are passed a string, eval it to an object using the calling frames
## globals and locals
e_obj = eval(obj, globals, locals)
except Exception, err:
print dir()
print "[-] Problem accessing specified object: %s"%(err)
return
self.pyretic.mem_objwalk( e_obj )
[docs] def do_get_version(self, args):
"""
Get the reported version of the currently executing Python runtime
Note: This may not be accurate, a runtime can be made to report any version.
Use only as an indicator when choosing a reference runtime to use.
usage: get_version
"""
print "[=] Report version of current runtime:\n\t%s\n"%(sys.version)
[docs] def do_auto_remap(self, remapped_pycs):
"""
Make some best guesses on how to remap the opcodes from the currently
running obfuscated runtime.
NOTE: If there is an exiting auto_remap project it will be overwritten
Usage: auto_remap <path to remapped pycs>
"""
if not remapped_pycs:
print "[-] No target obfuscated byte code supplied to remap. Aborting"
return
self.do_is_remapped()
if not self.runtime_remapped:
resp = raw_input( "[!] It doesn't seem like the runtime we are in has remapped it's opcodes, do you want to continue to remap anyway ? (yes/no) ")
if resp.lower() in ["n","no"]:
print "[=] Aborting remap"
return
##Remove an existing auto_remap project
try:
shutil.rmtree(os.path.join(self.pyretic.prev_project_root, "auto-remap"))
except:
##Probably didn't exist
pass
##New project called auto remap - this will prompt for py runtime version choice
self.do_set_project("auto-remap")
##Generate reference bytecode
self.do_gen_ref()
##Generate obfuscated bytecode with supplied pyc target
self.do_gen_obf(remapped_pycs)
##Perform remap
self.autowrite = True
self.do_remap()
##Swap in the newly generated opcodes
self.do_swap_opcodes()
print "[+] Automatic remapping of opcodes complete. Please restart to use the new opcode tables in decompilation"
[docs] def do_gen_ref(self, reference_modules = None):
"""
Generate reference bytecode from the .py's at the path specified
using the Python runtime version already set. If not path is specified
the relevant Python runtime will be used if it has already been downloaded
with download_runtime.
The generated bytecode will be used to diff against the obfuscated
bytecode to deduce a modified opcode map.
The more commonality between the reference and obfuscated bytecode there
the higher the number of opcodes that will be able to be remapped.
Usage: gen_ref [to use the downloaded runtime of the current project
version as the reference source]
gen_ref <path to directory of reference python source code>
Example: gen_ref /tmp/python2.5.4/Lib
"""
try:
downloaded_rt = os.path.join(MODULE_LOCATION, "Downloaded_Runtimes",
"Python-%s"%(self.runtime_version), "Lib")
os.stat(downloaded_rt)
source_modules = downloaded_rt
except:
source_modules = None
if not reference_modules and not source_modules:
print "[-] No path given from which to generate reference bytecode and no runtime downloaded for stdlib use"
return
elif reference_modules:
source_modules = reference_modules
##Force a recompile so we know we are in a clean state
self.do_recompile(downloaded_rt)
source_modules = self.pyretic.normalise_path(source_modules)
print "[=] Generating bytecode from .py's at: %s"%(source_modules)
##Where the bytecode output will go
self.ref_pyb = os.path.join(self.pyretic.get_projectdir(), "pybs")
##We need to run in a standrd runtime (not the current one)to get reference
##bytecode so we call out to the external Python runtime that is set
if self.runtime_version == "default":
version_to_gen = "2.5"
else:
for maj_ver in RUNTIME_LOCATION.keys():
if maj_ver in self.runtime_version:
version_to_gen = maj_ver
break
else:
print "[-] Unknown runtime location for version specified: %s"%(self.runtime_version)
return
##-S needed to supress any site specific import hooks that may be in the
## obfuscated package we are runnign from that will get in the way
command = [RUNTIME_LOCATION[version_to_gen], "-S",
os.path.join(MODULE_LOCATION, "OpcodeRemapping", "OpcodeRemap.py"),
version_to_gen, self.ref_pyb, source_modules]
print "[=] Using runtime located at %s to create reference bytecode"%(RUNTIME_LOCATION[version_to_gen])
## Reset environment of PYTHON* variables to avoid interference
try:
old_home = os.environ["PYTHONHOME"]
del os.environ["PYTHONHOME"]
except KeyError:
old_home = None
try:
old_path = os.environ["PYTHONPATH"]
del os.environ["PYTHONPATH"]
except KeyError:
old_path = None
##Spawn process to generate bytecodes
try:
subprocess.call(command)
self.ref_pyb_generated = True
except:
print "[-] Error trying to execute: %s"%(command)
##Reset env
if old_home:
os.environ["PYTHONHOME"] = old_home
if old_path:
os.environ["PYTHONPATH"] = old_path
print "[+] Reference bytecode generated"
[docs] def do_gen_obf(self, obfuscated_modules = None):
"""
Generate obfuscated Python bytecode for the modules at the path
specified using the current runtime we are running from.
The generated bytecode will be used to diff against the
reference bytecode to deduce a modified opcode map. In general you
should point this at the directory containing the obfuscated
stdlib .pyc's for the obfuscated runtime
The more commonality between the reference and obfuscated bytecode there
the higher the number of opcodes that will be able to be remapped.
Usage: gen_obf <path to directory of obfusctaed python .pyc's>
Example: gen_obf /tmp/foo.app/Contents/Resources/runtime/site_packages/
"""
if not obfuscated_modules:
print "[-] No path given from which to generate obfuscated bytecode"
return
##Make sure we have everything current
if "OpcodeRemap" not in sys.modules.keys():
from OpcodeRemapping import OpcodeRemap
else:
OpcodeRemap = reload(OpcodeRemap)
self.obf_pyb = os.path.join(self.pyretic.get_projectdir(), "pybs")
obfuscated_modules = self.pyretic.normalise_path(obfuscated_modules)
print "[=] Generating bytecode from .py's at: %s"%(obfuscated_modules)
##Call into OpcodeRemap
if self.runtime_version == "default":
version_to_gen = "2.5"
else:
version_to_gen = self.runtime_version
OpcodeRemap.gen_obf(self.obf_pyb, obfuscated_modules, version_to_gen)
print "[+] Obfuscated bytecode generated"
[docs] def do_remap(self, dirs = None):
"""
From the two sets of .pyb's produced by gen_r2x and gen_o2x do the compares
to work out the new opcode map. From this new opcode map create new files
opcode.py (for the running stdlib) and opcodes.py (for UnPYC)
Note: the .pyb's must already have been generated from the gen_xxx calls
Usage: remap
"""
if not dirs:
try:
os.stat(os.path.join(self.pyretic.get_projectdir(), "pybs","obf_pyb"))
os.stat(os.path.join(self.pyretic.get_projectdir(), "pybs","ref_pyb"))
except OSError:
print "[-] No .pyb directories could be found and non specified"
return
##Try setting to where pyb's would reside if they had already been gen'd
ref_dir = os.path.join(self.pyretic.get_projectdir(), "pybs","ref_pyb")
obf_dir = os.path.join(self.pyretic.get_projectdir(), "pybs","obf_pyb")
else:
##Split supplied sirs string to ref and obf
try:
ref_dir, obf_dir = dirs.split(" ")
except:
print "[-] Reference or obfuscated .pyb sets not produced or specified"
return
##Make sure we have everything current
if "OpcodeRemap" not in sys.modules.keys():
from OpcodeRemapping import OpcodeRemap
else:
OpcodeRemap = reload(OpcodeRemap)
##Location where the opcode/opcodes.py will be dumped - with project
output_dir= self.pyretic.get_project_mod_dir
##Call into OpcodeRemap
try:
OpcodeRemap.remap(ref_dir, obf_dir, self.pyretic.get_project_mod_dir())
except OpcodeRemap.OpcodeRemapError, err:
print "[-] Problem with remap: %s"%(err)
[docs] def do_swap_opcodes(self, args = None):
"""
Swap the remapped opcodes.py module for the original module in the UnPYC
directory. Until this is done UnPYC will not be able to decompile
correctly as it will be using the wrong opcode map. The opcodes.py file
that will be used is the one that is located at the PROJECT_DIR/libs
Ssage: swap_modules
"""
files = ["opcode", "opcodes"]
for f in files:
try:
remap_name = os.path.join(self.pyretic.get_project_mod_dir(), "%s_remap.py"%f)
os.stat(remap_name)
except OSError:
print "[-] Remapped %s_remap.py file cannot be found at: %s\n\t Has it been generated yet?"%(f, remap_name)
continue
##Archive current opcodes.py module
archive_name = os.path.join(self.pyretic.get_project_mod_dir(), "%s_orig.py"%f)
curr_name = os.path.join(self.pyretic.get_project_mod_dir(), "%s.py"%f)
try:
shutil.copyfile(curr_name, archive_name)
print "[+] Original %s.py archived to %s"%(f,archive_name)
##Copy over
shutil.copyfile(remap_name, curr_name)
self.do_module_restore = False
#TODO force reload
##live.reload()
except Exception, err:
print "[-] Error copying a file: %s"%(err)
print "[+] New opcode maps copied"
print "[!!] *MUST restart REpdb for changes to take effect*"
## ##Reload stdlib modules
## self.__reload_modules()
##
## if "OpcodeRemap" not in sys.modules.keys():
## print "IMPORT"
## from OpcodeRemapping import OpcodeRemap
## OpcodeRemap.opcode.__file__
## else:
## print "RELOAD"
## OpcodeRemap = reload(OpcodeRemap)
## OpcodeRemap.opcode.__file__
[docs] def do_restore_opcodes(self, args):
"""
Restore the original opcode.py and opcodes.py module that was archived by swap_opcodes
Usage: restore_opcodes
"""
files_to_move = ["opcode", "opcodes"]
for f in files_to_move:
try:
os.stat(os.path.join(self.pyretic.get_project_mod_dir(), "%s_orig.py"%(f)))
except:
print "[-] Archived file %s_orig.py cannot be found, check if has remapping occurred?"%(f)
continue
try:
shutil.copyfile(os.path.join(self.pyretic.get_project_mod_dir(), "%s_orig.py"%(f)),
os.path.join(self.pyretic.get_project_mod_dir(), "%s.py"%(f)))
print "[+] Restored original %s.py"%(f)
except Exception, err:
print "[-] Error copying file: %s"%(err)
#print "[=] *MUST* restart REpdb for changes to take effect"
#TODO force reload
##live.reload()
self.__reload_modules()
[docs] def do_recompile(self, path):
"""
For the path specified do a recursive recompilation of all .py's found
using the current runtimes compiler (if available)
Usage: recompile <path to modules>
Example: recompile /tmp/python_254/Libs
"""
##From the projects libs
import compileall
if not path:
print "[-] No path to begin recompilation from specified"
return
path = self.pyretic.normalise_path(path)
try:
compileall.compile_dir(path, force=1)
print "[+] Recompilation complete"
except Exception, err:
print "[-] Unable to recompile: %s"
[docs] def trace_dispatch(self, frame, event, arg):
"""
Overide the bdb trace_dispatch method so as we can add in more
trace hooks for other functionality such as call graphing
"""
#print "\nCalling Frame : %s\nDebugger Frame: %s\n"%(frame, self.debugger_frame )
self.debugger_frame = sys._getframe()
self.calling_frame = frame
if self.callgraph_started:
pycallgraph.tracer(frame, event, arg)
##Call through to the original trace_dispatch to continue the
##debugger activity
return pdb.bdb.Bdb.trace_dispatch(self, frame, event, arg)
##Extra none decompile related functionality
[docs] def do_set_callgraph_exclude(self, excludes):
"""
Set an exclusion filter for the callgraph functionality, this defines
modules/functions that are not included in the callgraph tracing
Argument is a comma seperated list of filter expresions
Usage: set_callgraph_exclude foo,bar*,blat
"""
self.cg_exclude += excludes.replace(" ","").split(",")
print "[=] Set callgraph excludes to: %s"%(self.cg_exclude)
[docs] def do_start_callgraph(self, name):
"""
Initialise a callgraph trace using pycallgraph
After this is set use the pdb commands 'n', 'c' etc to step through
the application being debugged and generate the callgraph
The callgraph can be stopped & written at anytime by 'stop_callgraph',
if the debugging exits the callgraph is automatically stopped and written
Usage: start_callgraph <name of callgraph if you want non-default name>
"""
##Both pdb and pycallgraph both want to set the tracer so we have to chain them
## sys.settrace applies globally to all frames
if not CAN_CALLGRAPH:
print "[-] pycallgraph module, or graphviz unavailable"
return
##If no filename for the callgraph is given just use epoch to get one
if not name:
self.cg_name = "callgraph_%s"%(str(time.time()).split(".")[0])
else:
self.cg_name = name
##Try to cleanup graph a bit
filter_func = pycallgraph.GlobbingFilter(exclude = self.cg_exclude, max_depth = 4)
locals = self.calling_frame.f_locals
globals = self.calling_frame.f_globals
##Call the callgraph funtion from the context f_backframe
exec("from ThirdParty import pycallgraph", globals,locals)
exec("pycallgraph.start_trace()", globals, locals)
self.callgraph_started = True
print "[+] Call graph initialised"
[docs] def do_stop_callgraph(self, foo):
"""
Stop & print callgraph
Usage: stop_callgraph
"""
if not self.callgraph_started:
print "[-] Call graph not started yet. Use 'start_callgraph' to start"
return
graph_path = os.path.join(self.pyretic.get_projectdir(),
"%s.jpg"%self.cg_name.strip(os.sep))
print "[+] Outputting callgraph to: %s\n This may take a while..."%(graph_path)
##Put in the calling frames context
locals = self.calling_frame.f_locals
globals = self.calling_frame.f_globals
try:
exec("pycallgraph.make_dot_graph('%s',format='jpg', tool='dot')"%graph_path, globals, locals)
print "[+] Graph generation complete"
except Exception, err:
print "[-] Problem generating callgraph: %s"%err
print "[?] Is graphviz installed on your system? (http://www.graphviz.org/Download..php)"
self.callgraph_started = False
[docs] def do_obj_mirror(self, args):
"""
For the supplied object, all of it's methods/attributes etc are mirrored
in the calling objects namespace
This is a dirty way of acting as an object proxy meaning we can be injected
in place of another object and be sure we won't break the larger app
If no frame is specified then the frame from which the debugger was called is used
If "debugger" is given as the frame the debugger frame is used
Usage: obj_mirror <instantiated object to mirror>
"""
import inspect
if not args:
print "[-] No object supplied to mirror"
return
arg_list = args.split(" ")
s_obj_to_mirror = arg_list[0]
if len(arg_list) > 1:
frame = arg_list[1]
else:
frame = None
if not frame:
##Use context of calling frame
frame_context = self.curframe
elif frame == "debugger":
##Use context of the frame the debugger is executing in
frame_context = self.debugger_frame
else:
##None frame object supplied ... bail
print "[-] None frame object supplied - object type was %s"%(type(frame))
return
locals = frame_context.f_locals
globals = frame_context.f_globals
print "[=] Mirroring %s in the context of %s"%(s_obj_to_mirror, frame_context)
try:
obj_to_mirror = eval(s_obj_to_mirror, globals, locals)
except:
print "[-] Unknown object specified, cannot mirror"
return
for x in dir(obj_to_mirror):
skip_list = ["__init__", "__builtins__", "__doc__", "__name__"]
if inspect.isbuiltin(x) or x in skip_list:
print "[-] skipping %s"%(x)
continue
print "[+] %s -> %s.%s"%(x, obj_to_mirror.__name__, x)
exec("%s = %s.%s"%(x, obj_to_mirror.__name__, x),
globals, locals)
[docs] def cleanup(self, args):
"""
Cleanup function, currently just stops the callgraph if it
has been started
"""
#print "[+] atexit cleanup function entered"
if self.callgraph_started:
self.do_stop_callgraph(None)
if self.do_module_restore:
self.do_restore_opcodes(None)
# Simplified interface - for similar uasage to pdb.py
def run(statement, globals=None, locals=None):
RePdb().run(statement, globals, locals)
def runeval(expression, globals=None, locals=None):
return RePdb().runeval(expression, globals, locals)
def runctx(statement, globals, locals):
# B/W compatibility
run(statement, globals, locals)
def runcall(*args, **kwds):
return RePdb().runcall(*args, **kwds)
def set_trace(frame = None):
REPDBInstance = RePdb()
atexit.register( REPDBInstance.cleanup, REPDBInstance)
REPDBInstance.set_trace(sys._getframe().f_back)
# Post-Mortem interface - for similar uasage to pdb.py
def post_mortem(t):
p = RePdb()
p.reset()
while t.tb_next is not None:
t = t.tb_next
p.interaction(t.tb_frame, t)
def pm():
post_mortem(sys.last_traceback)
if __name__ == "__main__":
set_trace()