shadowbrokers-exploits/windows/fuzzbunch/daveplugin.py
2017-04-14 11:45:07 +02:00

528 lines
20 KiB
Python

import hashlib, os, threading
import xml.parsers.expat as expat
import exception, util, truantchild
import edfmeta, edfexecution
#from plugin import Plugin
from edfplugin import EDFPlugin
from pytrch import TrchError
from redirection import LocalRedirection, RemoteRedirection
__all__ = ["DAVEPlugin"]
class DAVEPlugin(EDFPlugin):
def __init__(self, files, io):
# DAVE plugins are *currently* supported only on Win32 (that's a
# restriction based on delivery mechanisms, not the DAVE spec itself)
import sys
if sys.platform != "win32":
raise EnvironmentError("DAVEPlugins are supported only on Windows for this version of Fuzzbunch!")
try:
EDFPlugin.__init__(self, files, io)
self.metaconfig = files[2]
self.initTouches()
self.initConsoleMode()
self.initRedirection()
except TrchError:
# There was an error parsing the plug-in XML
raise
except IndexError:
# We didn't get the right number of files
raise
self.procs = []
self.package_arches = edfmeta.parse_forward(self.metaconfig)
if not self.package_arches:
raise EnvironmentError("A DAVEPlugin is missing required 'package' information in its .fb file!")
def getMetaHash(self):
return "%s %s %s" % (hashlib.sha1(open(self.metaconfig, 'rb').read()).hexdigest(),
os.lstat(self.metaconfig).st_size,
os.path.basename(self.metaconfig))
def canDeploy(self):
return True
def killPlugin(self):
"""Helper function for forceful termination of the plugin executable,
primarily used for testing
"""
for p in self.procs:
p.kill()
self.procs = []
def write_interpreted_xml_file(self, inConfFile, globalvars={}):
"""Rewrite the inconfig, substituting variables"""
tmpFile = open(inConfFile, "w");
# Note: Truantchild has been used to this point to store parameters, so
# the inconfig here represents all of the prompted data
configdata = self.getMarshalledInConfig()
configlines = configdata.split("\n")
newlines = []
for line in configlines:
newlines.append(util.variable_replace(line, globalvars))
newconfig = "\n".join(newlines)
tmpFile.write(newconfig)
tmpFile.close()
return inConfFile
"""
Plugin validation routine
"""
def validate(self, dirs, globalvars={}):
baseDir, logDir = dirs
timestamp = util.formattime()
exeBaseName = os.path.basename(self.executable)
logName = "%s-%s.log" % (exeBaseName, timestamp)
logFile = os.path.join(logDir, logName)
try:
os.remove(logFile)
except:
pass
inConfName = "%s-%s-InConfig.validate.xml" % (exeBaseName, timestamp)
inConfFile = os.path.join(logDir, inConfName)
self.write_interpreted_xml_file(inConfFile, globalvars=globalvars)
if edfexecution.validate_plugin(self.executable, inConfFile, self.io) == 0:
return True
else:
return False
def marshal_params(self, logDir, archOs, output_filename=None, globalvars={}):
import sys, subprocess, platform
# Find/compute various paths and filename components we'll need later
storageDir = globalvars['FbStorage']
timestamp = util.formattime()
exeBaseName = os.path.basename(self.executable)
# Get our own packaging options (for reference)
arch_map = self.package_arches
# Figure out which piece to use for marshaling
host_archOs = "%s-%s" % (platform.machine(), platform.system())
proxy = arch_map[host_archOs][0]
core = arch_map[archOs][1]
# Non supported!
if proxy is None:
return (None, None)
# Get files/paths set up for marshaling
if output_filename is None:
output_filename = os.path.join(logDir, "%s-%s-Marshal.bin" % (exeBaseName, timestamp))
output_path = os.path.dirname(output_filename)
try:
os.makedirs(output_path)
except os.error:
assert os.path.isdir(output_path), "Output path '%s' could not be found/created!" % output_path
xml_config_name = os.path.join(logDir, "%s-%s-InConfig.marshal.xml" % (exeBaseName, timestamp))
self.write_interpreted_xml_file(xml_config_name, globalvars=globalvars)
# Fire off the DANE config utility to actually create the package. Note that this is strictly
# Win32[/64] for now...
# (This stuff should be abtracted away form cross-platformness and to avoid hard-coded paths.)
proxy_dll = os.path.join(os.path.dirname(self.executable), proxy)
assert os.path.isfile(proxy_dll), "Required file '%s' doesn't exist!" % proxy_dll
self.io.print_msg("\tUsing '%s' to handle parameter marshaling" % proxy_dll)
self.io.print_msg("\tMarshaling the contents of '%s'" % xml_config_name)
config_exe = os.path.join(storageDir, 'dvmarshal.exe')
assert os.path.isfile(config_exe), "Required program '%s' doesn't exist!" % config_exe
subprocess.check_call([config_exe, proxy_dll, xml_config_name, output_filename])
core_dll = os.path.join(os.path.dirname(self.executable), core)
return (core_dll, output_filename)
def build_package(self, logDir, archOs, listenPort=None, output_filename=None, globalvars={}):
import sys, subprocess, platform
# Find/compute various paths and filename components we'll need later
storageDir = globalvars['FbStorage']
timestamp = util.formattime()
exeBaseName = os.path.basename(self.executable)
# Get our own packaging options (for reference)
arch_map = self.package_arches
# Figure out which architecture/OS to use for each piece
host_archOs = "%s-%s" % (platform.machine(), platform.system())
proxy = arch_map[host_archOs][0]
core = arch_map[archOs][1]
if (proxy is None) or (core is None):
# Not supported!
return None
if output_filename is None:
output_filename = os.path.join(logDir, "%s-%s-Package.dll" % (exeBaseName, timestamp))
output_path = os.path.dirname(output_filename)
try:
os.makedirs(output_path)
except os.error:
assert os.path.isdir(output_path), "Output path '%s' could not be found/created!" % output_path
xml_config_name = os.path.join(logDir, "%s-%s-InConfig.package.xml" % (exeBaseName, timestamp))
self.write_interpreted_xml_file(xml_config_name, globalvars=globalvars)
# Fire off the DANE config utility to actually create the package. Note that this is strictly
# Win32[/64] for now...
# (This stuff should be abtracted away for cross-platformness and to avoid hard-coded paths.)
baseArch = archOs.split('-')[0]
dane_dll = os.path.join(storageDir, 'dane_%s.dll' % baseArch)
assert os.path.isfile(dane_dll), "Required file '%s' doesn't exist!" % dane_dll
self.io.print_msg("\tUsing '%s' as the output template" % dane_dll)
proxy_dll = os.path.join(os.path.dirname(self.executable), proxy)
assert os.path.isfile(proxy_dll), "Required file '%s' doesn't exist!" % proxy_dll
self.io.print_msg("\tUsing '%s' to handle parameter marshaling" % proxy_dll)
core_dll = os.path.join(os.path.dirname(self.executable), core)
assert os.path.isfile(core_dll), "Required file '%s' doesn't exist!" % core_dll
self.io.print_msg("\tUsing '%s' as the input payload" % core_dll)
config_exe = os.path.join(storageDir, 'danecfg.exe')
assert os.path.isfile(config_exe), "Required program '%s' doesn't exist!" % config_exe
subprocess.check_call([config_exe, dane_dll, proxy_dll, core_dll, xml_config_name, output_filename])
if listenPort:
# Pack in the listen-port as a particular binary resource
import ctypes, struct
RT_RCDATA = 10
ID_PORTNUM = 101
LANG_ID = 0x0000
BeginUpdateResource = ctypes.windll.kernel32.BeginUpdateResourceA
UpdateResource = ctypes.windll.kernel32.UpdateResourceA
EndUpdateResource = ctypes.windll.kernel32.EndUpdateResourceA
rblob = struct.pack("<H", listenPort)
handle = BeginUpdateResource(output_filename, False)
if handle is None:
raise ctypes.WinError()
if not UpdateResource(handle, RT_RCDATA, ID_PORTNUM, LANG_ID, rblob, len(rblob)):
raise ctypes.WinError()
if not EndUpdateResource(handle, False):
raise ctypes.WinError()
return output_filename
"""
Plugin execution routine
"""
def execute(self, session, consolemode, interactive, scripted, globalvars={}, runMode='', archOs='x86-Windows', listenPort=0):
self.lastsession = session
baseDir, logDir = session.get_dirs()
waitmode, newconsole = self.get_runflags(consolemode, interactive, scripted)
timestamp = util.formattime()
# Save history
session.history = self.getParameters()
# Prompt for run mode
if runMode in ("DANE", "DAVE"):
# TODO: prompt operator to verify remote callback tunnel exists for localhost comms
if runMode == "DANE":
# Build package
packagePath = self.build_package(logDir, archOs, listenPort, globalvars=globalvars)
# Print package info
self.io.print_success("DANE Package: %s" % packagePath)
# elif runMode == "DAVE":
# # Marshal params (and get core module name)
# modulePath, inputPath = self.marshal_params(logDir, archOs, globalvars=globalvars)
# # Build up DAVE commandline
# dvcmds = 'daringveteran -module "%s" -input "%s" -run %s' % (
# modulePath,
# inputPath,
# 'interactive' if (listenPort) else 'batch')
# if listenPort:
# dvcmds += " -homeport %d" % listenPort
# # Print core module and marshaled data info
# self.io.print_success('DAVE Pastable:\n\t' + dvcmds)
else:
raise NotImplementedError("No such option '%s'; what happened??" % (runMode))
# Set "DaveProxyPort" hidden parameter in current config
if listenPort:
#self.set("DaveProxyPort", str(listenPort))
self.io.print_msg("Proxy listening on localhost:%d" % listenPort)
else:
# Bail right now--gracefully...
return newconsole, None
elif runMode == 'FB':
# Make sure the proxy port is zeroed
self.set("DaveProxyPort", "0")
else:
raise NotImplementedError("No such option '%s'; what happened??" % (runMode))
exeBaseName = os.path.basename(self.executable)
logName = "%s-%s.log" % (exeBaseName, timestamp)
logFile = os.path.join(logDir, logName)
try:
os.remove(logFile)
except:
pass
# Touch the logfile
tmpFile = open(logFile, "w")
tmpFile.close()
# Create InConfig and write to the logdir
inConfName = "%s-%s-InConfig.xml" % (exeBaseName, timestamp)
inConfFile = os.path.join(logDir, inConfName)
self.write_interpreted_xml_file(inConfFile, globalvars=globalvars)
# Create the pipe that we will use for the --OutConfig parameter
pipeName = edfexecution.generate_pipename()
pipe = edfexecution.create_pipe(pipeName)
cwd = os.getcwd()
os.chdir(logDir)
try:
#
# This is the sneaky bit for the output. We call launch_plugin,
# which does two things. First, it passes stdin,stdout, and stderr
# to the call to subprocess.Popen so that output is duplicated to
# the console. Second, it passes --OutConfig a pipe so that, when we
# later call write_outconfig, this contains only the data we want
#
proc = edfexecution.launch_plugin(self.executable, inConfFile,
pipeName, logFile, self.io, newconsole)
self.procs.append(proc)
except KeyboardInterrupt:
self.io.print_error("Stopping plugin")
try:
self.procs.remove(proc)
proc.kill()
except:
pass
# Create the output param for the contract
session.contract = [util.oParam("Status", "Failed", "String", "Scalar"),
util.oParam("ReturnCode", "User Abort", "String", "Scalar")]
session.mark_fail()
raise exception.CmdErr, "Canceled by User"
os.chdir(cwd)
try:
# Wait for the spawned process to connect to our named pipe
pipe = edfexecution.connect_pipe(pipe, pipeName)
except edfexecution.PipeError, err:
self.io.print_error(str(err))
pipe = None
if pipe == None:
try:
# Try to kill the process
self.procs.remove(proc)
proc.kill()
except:
pass
try:
# See if the process has terminated
proc.poll()
except:
pass
# Create the output param for the contract
session.contract = [util.oParam("Status", "Failed", "String", "Scalar"),
util.oParam("ReturnCode", str(proc.returncode), "String", "Scalar")]
session.mark_fail()
raise exception.CmdErr, "Error Connecting Pipe to Plugin"
# Create OutConfig file name
outConfName = "%s-%s-OutConfig.xml" % (exeBaseName, timestamp)
outConfFile = os.path.join(logDir, outConfName)
if waitmode:
# Wait for the plugin to finish. So, we don't have overlapping
# output or problems waiting for the outconfig
self.get_outconfig(pipe, session, outConfFile, proc)
else:
# Creates a new thread to read output config from the executed plugin.
pluginThread = threading.Thread(None,
self.get_outconfig,
None,
(pipe, session, outConfFile, proc))
pluginThread.start()
return newconsole, logFile
"""
Output XML handling
"""
def get_outconfig(self, pipe, session, outConfName, proc):
try:
# Read from the named pipe we passed to launch_plugin and
# write to file
edfexecution.write_outconfig(outConfName, pipe)
except KeyboardInterrupt:
self.io.print_error("Canceled by User")
self.io.print_error("Stopping plugin")
try:
proc.kill()
except:
pass
finally:
# XXX - Remove_pipe too?
edfexecution.close_pipe(pipe)
try:
#name = self.getName()
params = self.read_outxml(outConfName)
params.append(util.oParam("Status", "Success", "String", "Scalar"))
if len(params) > 1:
session.mark_ready()
else:
session.mark_used()
except (ValueError, TrchError, AttributeError) as e:
# We get here if the following conditions happen:
# Argument errors to any calls
# Plug-in failed and didn't actually write an outconfig
# Output created an invalid XML configuration
#self.io.print_warning("Output XML error: It's either empty or does not parse: %s" % (e))
self.io.print_warning("Plugin failed")
proc.wait()
params = [util.oParam("Status", "Failed", "String", "Scalar"),
util.oParam("ReturnCode", str(proc.returncode), "String", "Scalar")]
session.mark_fail()
session.contract = params
def read_outxml(self, filename):
try:
return truantchild.Config((filename, self.executable))._outputParams.getParameterListExt()
except:
pass
"""
Session info description generation
"""
def getSessionDescription(self):
descr = [""]
if self.redirection:
redir = self.getRedirection()
descr = []
for l in redir['local']:
try:
descr.append( self.get(l.listenaddr) )
except exception.CmdErr:
# The tunnel is invalid because the parameter is not in scope. Ignore the tunnel
pass
return " ".join(descr)
"""
Rendezvous
"""
def doRendezvous(self, value):
self._trch_addrendezvousparam(value)
"""
Run Mode
"""
def initConsoleMode(self):
if self.metaconfig:
try:
mode = edfmeta.parse_consolemode(self.metaconfig)
self.consolemode = util.convert_consolemode(mode)
except expat.ExpatError:
# Pass here because we printed something while parsing the
# invalid file, but we want execution to continue
pass
else:
EDFPlugin.initConsoleMode(self)
def get_runflags(self, mode, interactive, scripted):
"""
Return the following boolean flags:
newconsole - the plugin should execute in a new console
waitmode - Execution should wait for the plugin to finish
executing
"""
if interactive:
if not mode:
# Use the plugin's settings
mode = self.getConsoleMode()
else:
# Non-interactive mode must reuse the console
mode = util.CONSOLE_REUSE
if mode == util.CONSOLE_REUSE:
# Reusing implies no new console
newconsole = False
else:
newconsole = True
waitmode = True
# If non-scripted, interactive sessions that launch new consoles don't
# need to wait for plugins to finish. All other permutations do.
if interactive and not scripted and newconsole:
waitmode = False
return (waitmode,newconsole)
"""
Redirection
"""
def initRedirection(self):
redir = None
try:
redir = edfmeta.parse_redirection(self.xmlInConfig)
except expat.ExpatError:
self.io.print_error("Error parsing redirection information")
# XXX - What should we do if we fail to parse redirection?
counter = 1
for r in redir['remote']:
if 'name' not in r:
r['name'] = 'remote-tunnel-%d' % counter
counter += 1
counter = 1
for r in redir['local']:
if 'name' not in r:
r['name'] = 'local-tunnel-%d' % counter
counter += 1
self.redirection = {
'remote' : [RemoteRedirection(**r) for r in redir['remote']],
'local' : [LocalRedirection(**l) for l in redir['local']]
}
"""
Touches
"""
def initTouches(self):
if self.metaconfig:
try:
self.touchlist = edfmeta.parse_touchlist(self.metaconfig)
except exception.PluginMetaErr:
import os.path
(p,f) = os.path.split(self.metaconfig)
n = f.split('-')[0]
self.io.pre_input(None)
self.io.print_warning("Error in %s meta (.fb) file - touches not loaded " % (str(n)))
self.io.post_input()
EDFPlugin.initTouches(self)
except IndexError:
EDFPlugin.initTouches(self)
else:
EDFPlugin.initTouches(self)