diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 87cd24b..5e30ae4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +Version 1.4.11 +-------------- +* TCP Custom Response and UDP Custom Response features +* Removed stray `fcntl` import from `test.py` that prevented Windows testing in + some cases + Version 1.4.10 -------------- * Fix format string errors due to line length limit diff --git a/docs/CustomResponse.md b/docs/CustomResponse.md index 4e04b34..d74d5d7 100644 --- a/docs/CustomResponse.md +++ b/docs/CustomResponse.md @@ -1,47 +1,79 @@ -# HTTP Listener Custom Responses +# Custom Response Configuration -The `HTTPListener` can accept a setting named `Custom` that enables -customizable responses beyond what can be achieved by emplacing files in the -web root (e.g. under `defaultFiles/`). +The Custom Response feature enables customization of responses either +statically or as user-supplied Python script code. -The `HTTPListener` `Custom` setting indicates the name of an HTTP custom -response configuration file in the same location and format as the active -FakeNet-NG configuration file. An example HTTP custom response configuration -file is supplied in `fakenet/configs/sample_http_custom.ini`. +This enables convenient C2 server development for malware analysis purposes +beyond what can be achieved by emplacing files in the service root (e.g. under +`defaultFiles/` in the default configuration) and without having to modify +default listener source code that is currently only available in the source +release of FakeNet-NG. -The sections of the HTTP custom response configuration file can define a series -of named rules regarding how to match requests and what to return. +Currently the `RawListener` (both TCP and UDP) and the `HTTPListener` accept a +setting named `Custom` that allows the user to specify Custom Response +behavior. -Valid matching specifications are: -* `MatchHosts`: a comma-separated list of hostnames that will match against +The `Custom` setting indicates the name of a custom response configuration file +in the same location and format as the active FakeNet-NG configuration file. +Example custom response configuration files are supplied under +`fakenet/configs/`. + +Each section of the custom response configuration file must specify which +listener(s) to configure. Valid listener configuration specifications are: +* `ListenerType`: The type of listener to affect: + * `HTTP` for any `HTTPListener` + * `TCP` for any TCP `RawListener` + * `UDP` for any UDP `RawListener` +* `InstanceName`: The name of the listener instance to be configured, + as found in the section name within the FakeNet-NG configuration file. + +If both the `ListenerType` and `InstanceName` listener specifications are +present in a single section, they will be evaluated disjunctively (logical or). + +Further details are documented below for HTTP and raw Listeners subsequently. + +## HTTP Listener Custom Responses + +Aside from which listener instances to configure, the HTTP custom response +configuration section must specify: +* Which requests to match +* What response to return. + +Valid HTTP request matching specifications are: +* `HttpHosts`: a comma-separated list of hostnames that will match against host headers. -* `MatchURIs`: a comma-separated list of URIs that will match against request +* `HttpURIs`: a comma-separated list of URIs that will match against request URIs. -If both matching specifications are present in a single section, they will be -evaluated conjunctively (logical and). +If the `HttpHosts` specification includes a colon-delimited port number, it +will only match if the host header includes the same colon-delimited port +number. + +If both the `HttpHosts` and `HttpURIs` matching specifications are present in +a single section, they will be evaluated conjunctively (logical and). -Valid response specifications are: -* `RawFile`: Returns the raw contents of the specified file located under +Valid HTTP custom response specifications are: +* `HttpRawFile`: Returns the raw contents of the specified file located under the web root, with the exception of date replacement. -* `StaticString`: Wraps the specified string with server headers and a 200 OK - response code, replacing `\r\n` tokens with actual CRLFs and performing date - replacement as necessary. - * `ContentType`: Optionally, you accompany the `StaticString` setting with - an HTTP `Content-Type` header value to send. It is an error to specify - this setting with any other kind of response specification. -* `Dynamic`: Loads the specified Python file located under the web root - and invokes its `HandleRequest` function as described below. +* `HttpStaticString`: Wraps the specified string with server headers and a 200 + OK response code, replacing `\r\n` tokens with actual CRLFs and performing + date replacement as necessary. + * `ContentType`: Optionally, you accompany the `HttpStaticString` setting + with an HTTP `Content-Type` header value to send. It is an error to + specify this setting with any other kind of response specification. +* `HttpDynamic`: Loads the specified Python file located under the web root + and invokes its `HandleHttp` function as described below. -Date replacement applies to both `RawFile` and `StaticString`, and replaces any -occurrences of `` with a server-formatted date. +Date replacement applies to both `HttpRawFile` and `HttpStaticString`, and +replaces any occurrences of `` in the specified text with a +server-formatted date. -## Implementing a Dynamic Response Handler +### Implementing the HttpDynamic Response Handler -The `HandleRequest` method must conform to the following prototype: +The `HandleHttp` function must conform to the following prototype: ``` -def HandleRequest(req, method, post_data=None): +def HandleHttp(req, method, post_data=None): """Handle an HTTP request. Parameters @@ -57,5 +89,77 @@ def HandleRequest(req, method, post_data=None): pass ``` -An example dynamic response handler is supplied in -`defaultFiles/HTTPCustomProviderExample.py` +An example HTTP dynamic response handler is supplied in +`configs/CustomProviderExample.py` + +## Raw Listener Custom Responses + +Raw Listener (TCP and UDP) Custom Responses implement no request filtering and +implement only response specifications. + +### TCP Listener Custom Responses +Valid TCP custom response specifications are: +* `TcpRawFile`: Returns the raw contents of the specified file located under + the configuration root. +* `TcpStaticString`: Sends the specified string as-is. +* `TcpStaticBase64`: Base64 decodes the specified Base64-encoded data and sends + the result as-is. +* `TcpDynamic`: Loads the specified Python file located under the web root + and invokes its `HandleTcp` function as described below. + +#### Implementing the TcpDynamic Response Handler + +The `HandleTcp` function must conform to the following prototype: + +``` +def HandleTcp(sock): + """Handle a TCP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + """ + pass +``` + +The socket object's `recv` function is hooked so that any time it is called, a +hex dump will be emitted in the FakeNet-NG log output on behalf of the user. + +### UDP Listener Custom Responses +Valid UDP custom response specifications have the same meanings as their TCP +analogs documented above: +* `UdpRawFile` +* `UdpStaticString` +* `UdpStaticBase64` +* `UdpDynamic`: invokes `HandleUdp` + +#### Implementing the UdpDynamic Response Handler + +The `HandleUdp` function's prototype differs significantly from its TCP analog +due to the differences in protocols. With UDP, the data has already been +received before it is time to call any user-implemented callback. Furthermore, +the remote address of the peer is needed to be able to send data via UDP. + +``` +def HandleUdp(sock, data, addr): + """Handle a UDP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + data : str + The data received + addr : tuple + The host and port of the remote peer + """ + pass +``` + +To send data, the programmer must supply not only the buffer to send, but also +the remote address to transmit to, as provided in the `addr` argument: + +``` + sock.sendto(buf, addr) +``` diff --git a/docs/developing.md b/docs/developing.md index 1bec90e..c8b8f5e 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -233,7 +233,10 @@ fakenet1.4.3\ +-- configs\ |   +-- CustomProviderExample.py |   +-- default.ini - |   +-- sample_http_custom.ini + | +-- CustomProviderExample.py + |   +-- sample_custom_response.ini + | +-- sample_raw_response.txt + | +-- sample_raw_tcp_response.txt | +-- defaultFiles\ | +-- FakeNet.gif @@ -244,9 +247,7 @@ fakenet1.4.3\ | +-- FakeNet.pdf | +-- FakeNet.png | +-- FakeNet.txt - | +-- HTTPCustomProviderExample.py | +-- ncsi.txt - | +-- sample_raw_response.txt | +-- listeners\    +-- ssl_utils diff --git a/docs/srs.md b/docs/srs.md index ad5f7ff..5d58b58 100644 --- a/docs/srs.md +++ b/docs/srs.md @@ -458,9 +458,9 @@ string, the HTTP Custom Response feature must replace occurrences of `` in the ### TCP and UDP Listener Custom Response Configuration -The TCP and UDP listeners should accommodate Custom Response configuration -constrained to an operator-defined regular expression of octets and permit -responses to be returned according to three configuration specifications: +The TCP and UDP listeners must accommodate Custom Response configuration +and permit responses to be returned according to three configuration +specifications: * The raw contents of a configured binary file * The contents of a statically configured string * The delegation of control to a Python script file diff --git a/fakenet/configs/CustomProviderExample.py b/fakenet/configs/CustomProviderExample.py new file mode 100644 index 0000000..ae928cd --- /dev/null +++ b/fakenet/configs/CustomProviderExample.py @@ -0,0 +1,72 @@ +import socket + +# To read about customizing HTTP responses, see docs/CustomResponse.md +def HandleRequest(req, method, post_data=None): + """Sample dynamic HTTP response handler. + + Parameters + ---------- + req : BaseHTTPServer.BaseHTTPRequestHandler + The BaseHTTPRequestHandler that recevied the request + method: str + The HTTP method, either 'HEAD', 'GET', 'POST' as of this writing + post_data: str + The HTTP post data received by calling `rfile.read()` against the + BaseHTTPRequestHandler that received the request. + """ + response = 'Ahoy\r\n' + + if method == 'GET': + req.send_response(200) + req.send_header('Content-Length', len(response)) + req.end_headers() + req.wfile.write(response) + + elif method == 'POST': + req.send_response(200) + req.send_header('Content-Length', len(response)) + req.end_headers() + req.wfile.write(response) + + elif method == 'HEAD': + req.send_response(200) + req.end_headers() + + +def HandleTcp(sock): + """Handle a TCP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + """ + while True: + try: + data = None + data = sock.recv(1024) + except socket.timeout: + pass + + if not data: + break + + resp = raw_input('\nEnter a response for the TCP client: ') + sock.sendall(resp) + + +def HandleUdp(sock, data, addr): + """Handle a UDP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + data : str + The data received + addr : tuple + The host and port of the remote peer + """ + if data: + resp = raw_input('\nEnter a response for the UDP client: ') + sock.sendto(resp, addr) diff --git a/fakenet/configs/default.ini b/fakenet/configs/default.ini index 495b5cb..f851df9 100644 --- a/fakenet/configs/default.ini +++ b/fakenet/configs/default.ini @@ -232,6 +232,8 @@ Listener: RawListener UseSSL: No Timeout: 10 Hidden: False +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini [RawUDPListener] Enabled: True @@ -241,6 +243,8 @@ Listener: RawListener UseSSL: No Timeout: 10 Hidden: False +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini [FilteredListener] Enabled: False @@ -276,8 +280,8 @@ Timeout: 10 DumpHTTPPosts: Yes DumpHTTPPostsFilePrefix: http Hidden: False -# To read about customizing HTTP responses, see docs/CustomResponse.md -# Custom: sample_http_custom.ini +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini [HTTPListener443] Enabled: True diff --git a/fakenet/configs/sample_custom_response.ini b/fakenet/configs/sample_custom_response.ini new file mode 100644 index 0000000..1424b3f --- /dev/null +++ b/fakenet/configs/sample_custom_response.ini @@ -0,0 +1,25 @@ +# To read about customizing HTTP responses, see docs/CustomResponse.md +[Example0] +InstanceName: HTTPListener80 +HttpURIs: /test.txt +HttpStaticString: Wraps this with normal FakeNet HTTP headers ()\r\n + +[Example1] +InstanceName: HTTPListener80 +ListenerType: HTTP +HttpHosts: some.random.c2.com, other.c2.com +HttpRawFile: sample_raw_response.txt + +[Example2] +ListenerType: HTTP +HttpHosts: both_host.com +HttpURIs: and_uri.txt +HttpDynamic: CustomProviderExample.py + +[ExampleTCP] +InstanceName: RawTCPListener +TcpDynamic: CustomProviderExample.py + +[ExampleUDP] +InstanceName: RawUDPListener +UdpDynamic: CustomProviderExample.py diff --git a/fakenet/configs/sample_http_custom.ini b/fakenet/configs/sample_http_custom.ini deleted file mode 100644 index 4ee57d8..0000000 --- a/fakenet/configs/sample_http_custom.ini +++ /dev/null @@ -1,13 +0,0 @@ -# To read about customizing HTTP responses, see docs/CustomResponse.md -[Example0] -MatchURIs: /test.txt -StaticString: Wraps this with normal FakeNet HTTP headers ()\r\n - -[Example1] -MatchHosts: some.random.c2.com, other.c2.com -RawFile: sample_raw_response.txt - -[Example2] -MatchHosts: both_host.com -MatchURIs: and_uri.txt -Dynamic: HTTPCustomProviderExample.py diff --git a/fakenet/defaultFiles/sample_raw_response.txt b/fakenet/configs/sample_raw_response.txt similarity index 100% rename from fakenet/defaultFiles/sample_raw_response.txt rename to fakenet/configs/sample_raw_response.txt diff --git a/fakenet/defaultFiles/HTTPCustomProviderExample.py b/fakenet/defaultFiles/HTTPCustomProviderExample.py deleted file mode 100644 index 85ea79e..0000000 --- a/fakenet/defaultFiles/HTTPCustomProviderExample.py +++ /dev/null @@ -1,31 +0,0 @@ -# To read about customizing HTTP responses, see docs/CustomResponse.md -def HandleRequest(req, method, post_data=None): - """Sample dynamic HTTP response handler. - - Parameters - ---------- - req : BaseHTTPServer.BaseHTTPRequestHandler - The BaseHTTPRequestHandler that recevied the request - method: str - The HTTP method, either 'HEAD', 'GET', 'POST' as of this writing - post_data: str - The HTTP post data received by calling `rfile.read()` against the - BaseHTTPRequestHandler that received the request. - """ - response = 'Ahoy\r\n' - - if method == 'GET': - req.send_response(200) - req.send_header('Content-Length', len(response)) - req.end_headers() - req.wfile.write(response) - - elif method == 'POST': - req.send_response(200) - req.send_header('Content-Length', len(response)) - req.end_headers() - req.wfile.write(response) - - elif method == 'HEAD': - req.send_response(200) - req.end_headers() diff --git a/fakenet/fakenet.py b/fakenet/fakenet.py index 9c285b5..5e64cd0 100644 --- a/fakenet/fakenet.py +++ b/fakenet/fakenet.py @@ -331,7 +331,7 @@ def main(): | | / ____ \| . \| |____| |\ | |____ | | | |\ | |__| | |_|/_/ \_\_|\_\______|_| \_|______| |_| |_| \_|\_____| - Version 1.4.10 + Version 1.4.11 _____________________________________________________________ Developed by FLARE Team _____________________________________________________________ diff --git a/fakenet/listeners/HTTPListener.py b/fakenet/listeners/HTTPListener.py index bbc0a43..16f472c 100644 --- a/fakenet/listeners/HTTPListener.py +++ b/fakenet/listeners/HTTPListener.py @@ -46,11 +46,11 @@ def qualify_file_path(filename, fallbackdir): class CustomResponse(object): - def __init__(self, name, conf, webroot): + def __init__(self, name, conf, configroot): self.name = name - match_specs = {'matchuris', 'matchhosts'} - response_specs = {'rawfile', 'staticstring', 'dynamic'} + match_specs = {'httpuris', 'httphosts'} + response_specs = {'httprawfile', 'httpstaticstring', 'httpdynamic'} if not match_specs.intersection(conf): raise ValueError('Custom HTTP config section %s lacks ' @@ -61,33 +61,49 @@ def __init__(self, name, conf, webroot): raise ValueError('Custom HTTP config section %s has %d of %s' % (name, nr_responses, '/'.join(response_specs))) - if ('contenttype' in conf) and ('staticstring' not in conf): + if ('contenttype' in conf) and ('httpstaticstring' not in conf): raise ValueError('Custom HTTP config section %s has ContentType ' - 'which is only usable with StaticString' % (name)) + 'which is only usable with ' + 'HttpStaticString' % (name)) - self.uris = conf.get('matchuris', {}) + self.uris = conf.get('httpuris', {}) if self.uris: self.uris = {u.strip() for u in self.uris.split(',')} - self.hosts = conf.get('matchhosts', {}) + self.hosts = conf.get('httphosts', {}) if self.hosts: self.hosts = {h.strip().lower() for h in self.hosts.split(',')} - self.raw_file = qualify_file_path(conf.get('rawfile'), webroot) + self.raw_file = qualify_file_path(conf.get('httprawfile'), configroot) if self.raw_file: self.raw_file = open(self.raw_file, 'rb').read() - self.pymod = qualify_file_path(conf.get('dynamic'), webroot) - if self.pymod: - self.pymod = imp.load_source('cr_' + self.name, self.pymod) - - self.static_string = conf.get('staticstring') + self.handler = None + pymod_path = qualify_file_path(conf.get('httpdynamic'), configroot) + if pymod_path: + pymod = imp.load_source('cr_' + self.name, pymod_path) + funcname = 'HandleHttp' + funcname_legacy = 'HandleRequest' + if hasattr(pymod, funcname): + self.handler = getattr(pymod, funcname) + elif hasattr(pymod, funcname_legacy): + self.handler = getattr(pymod, funcname_legacy) + else: + raise ValueError('Loaded %s module %s has no function %s' % + ('httpdynamic', conf.get('httpdynamic'), + funcname)) + + self.static_string = conf.get('httpstaticstring') if self.static_string is not None: self.static_string = self.static_string.replace('\\r\\n', '\r\n') self.content_type = conf.get('ContentType') def checkMatch(self, host, uri): hostmatch = (host.strip().lower() in self.hosts) + if (not hostmatch) and (':' in host): + host = host[:host.find(':')] + hostmatch = (host.strip().lower() in self.hosts) + urimatch = False for match_uri in self.uris: @@ -106,8 +122,8 @@ def respond(self, req, meth, postdata=None): if self.raw_file: up_to_date = self.raw_file.replace('', current_time) req.wfile.write(up_to_date) - elif self.pymod: - self.pymod.HandleRequest(req, meth, postdata) + elif self.handler: + self.handler(req, meth, postdata) elif self.static_string is not None: up_to_date = self.static_string.replace('', current_time) req.send_response(200) @@ -121,7 +137,7 @@ def respond(self, req, meth, postdata=None): class HTTPListener(object): def taste(self, data, dport): - + request_methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'CONNECT', 'PATCH'] @@ -154,7 +170,6 @@ def __init__( self.name = name self.local_ip = config.get('ipaddr') self.server = None - self.name = 'HTTP' self.port = self.config.get('port', 80) self.logger.debug('Initialized with config:') @@ -195,15 +210,29 @@ def start(self): self.server.custom_responses = [] custom = self.config.get('custom') + def checkSetting(d, name, value): + if name not in d: + return False + return d[name].lower() == value.lower() + if custom: - custom = qualify_file_path(custom, self.config.get('configdir')) + configdir = self.config.get('configdir') + custom = qualify_file_path(custom, configdir) customconf = ConfigParser() customconf.read(custom) for section in customconf.sections(): entries = dict(customconf.items(section)) - cr = CustomResponse(section, entries, self.webroot_path) - self.server.custom_responses.append(cr) + + if (('instancename' not in entries) and + ('listenertype' not in entries)): + msg = 'Custom Response lacks ListenerType or InstanceName' + raise RuntimeError(msg) + + if (checkSetting(entries, 'instancename', self.name) or + checkSetting(entries, 'listenertype', 'HTTP')): + cr = CustomResponse(section, entries, configdir) + self.server.custom_responses.append(cr) self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.daemon = True @@ -305,7 +334,7 @@ def do_POST(self): http_f.close() else: - self.server.logger.error('Failed to write HTTP POST headers and data to %s.', http_filename) + self.server.logger.error('Failed to write HTTP POST headers and data to %s.', http_filename) # Prepare response if not self.doCustomResponse('GET', post_body): @@ -356,7 +385,7 @@ def get_response(self, path): except Exception, e: self.server.logger.error('Failed to open response file: %s', response_filename) response_type = 'text/html' - else: + else: response = f.read() f.close() @@ -397,7 +426,7 @@ def main(): """ logging.basicConfig(format='%(asctime)s [%(name)15s] %(message)s', datefmt='%m/%d/%y %I:%M:%S %p', level=logging.DEBUG) - + config = {'port': '8443', 'usessl': 'Yes', 'webroot': 'fakenet/defaultFiles' } listener = HTTPListener(config) diff --git a/fakenet/listeners/RawListener.py b/fakenet/listeners/RawListener.py index 79b4031..1dfd72a 100644 --- a/fakenet/listeners/RawListener.py +++ b/fakenet/listeners/RawListener.py @@ -1,7 +1,10 @@ import logging +from ConfigParser import ConfigParser import os import sys +import imp +import base64 import threading import SocketServer @@ -14,6 +17,73 @@ INDENT = ' ' +def qualify_file_path(filename, fallbackdir): + path = filename + if path: + if not os.path.exists(path): + path = os.path.join(fallbackdir, filename) + if not os.path.exists(path): + raise RuntimeError('Cannot find %s' % (filename)) + + return path + + +class RawCustomResponse(object): + def __init__(self, proto, name, conf, configroot): + self.name = name + self.static = None + self.handler = None + + spec_file = '%srawfile' % (proto.lower()) + spec_str = '%sstaticstring' % (proto.lower()) + spec_b64 = '%sstaticbase64' % (proto.lower()) + spec_dyn = '%sdynamic' % (proto.lower()) + + response_specs = { + spec_file, + spec_str, + spec_b64, + spec_dyn, + } + + nr_responses = len(response_specs.intersection(conf)) + if nr_responses != 1: + raise ValueError('Custom %s config section %s has %d of %s' % + (proto.upper(), name, nr_responses, + '/'.join(response_specs))) + + self.static = conf.get(spec_str) + + if self.static is not None: + self.static = self.static.rstrip('\r\n') + + if not self.static is not None: + b64_text = conf.get(spec_b64) + if b64_text: + self.static = base64.b64decode(b64_text) + + if not self.static is not None: + file_path = conf.get(spec_file) + if file_path: + raw_file = qualify_file_path(file_path, configroot) + self.static = open(raw_file, 'rb').read() + + pymodpath = qualify_file_path(conf.get(spec_dyn), configroot) + if pymodpath: + pymod = imp.load_source('cr_raw_' + self.name, pymodpath) + funcname = 'Handle%s' % (proto.capitalize()) + if not hasattr(pymod, funcname): + raise ValueError('Loaded %s module %s has no function %s' % + (spec_dyn, conf.get(spec_dyn), funcname)) + self.handler = getattr(pymod, funcname) + + def respondUdp(self, sock, data, addr): + if self.static: + sock.sendto(self.static, addr) + elif self.handler: + self.handler(sock, data, addr) + + class RawListener(object): def taste(self, data, dport): @@ -32,7 +102,6 @@ def __init__(self, self.name = name self.local_ip = config.get('ipaddr') self.server = None - self.name = 'Raw' self.port = self.config.get('port', 1337) self.logger.debug('Starting...') @@ -44,13 +113,13 @@ def __init__(self, def start(self): # Start listener - if self.config.get('protocol') != None: - - if self.config['protocol'].lower() == 'tcp': + proto = self.config.get('protocol') + if proto is not None: + if proto.lower() == 'tcp': self.logger.debug('Starting TCP ...') self.server = ThreadedTCPServer((self.local_ip, int(self.config['port'])), ThreadedTCPRequestHandler) - elif self.config['protocol'].lower() == 'udp': + elif proto.lower() == 'udp': self.logger.debug('Starting UDP ...') self.server = ThreadedUDPServer((self.local_ip, int(self.config['port'])), ThreadedUDPRequestHandler) @@ -80,7 +149,40 @@ def start(self): sys.exit(1) self.server.socket = ssl.wrap_socket(self.server.socket, keyfile=keyfile_path, certfile=certfile_path, server_side=True, ciphers='RSA') - + + self.server.custom_response = None + custom = self.config.get('custom') + + def checkSetting(d, name, value): + if name not in d: + return False + return d[name].lower() == value.lower() + + if custom: + configdir = self.config.get('configdir') + custom = qualify_file_path(custom, configdir) + customconf = ConfigParser() + customconf.read(custom) + + for section in customconf.sections(): + entries = dict(customconf.items(section)) + + if (('instancename' not in entries) and + ('listenertype' not in entries)): + msg = 'Custom Response lacks ListenerType or InstanceName' + raise RuntimeError(msg) + + if (checkSetting(entries, 'instancename', self.name) or + checkSetting(entries, 'listenertype', proto)): + + if self.server.custom_response: + msg = ('Only one %s Custom Response can be configured ' + 'at a time' % (proto)) + raise(msg) + + self.server.custom_response = ( + RawCustomResponse(proto, section, entries, configdir)) + self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.daemon = True self.server_thread.start() @@ -94,53 +196,76 @@ def stop(self): class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): def handle(self): + # Hook to ensure that all `recv` calls transparently emit a hex dump + # in the log output, even if they occur within a user-implemented + # custom handler + def do_hexdump(data): + for line in hexdump_table(data): + self.server.logger.info(INDENT + line) - # Timeout connection to prevent hanging - self.request.settimeout(int(self.server.config.get('timeout', 5))) + orig_recv = self.request.recv - try: - - while True: + def hook_recv(self, bufsize, flags=0): + data = orig_recv(bufsize, flags) + if data: + do_hexdump(data) + return data - data = self.request.recv(1024) + bound_meth = hook_recv.__get__(self.request, self.request.__class__) + setattr(self.request, 'recv', bound_meth) - if not data: - break + # Timeout connection to prevent hanging + self.request.settimeout(int(self.server.config.get('timeout', 5))) + + cr = self.server.custom_response - for line in hexdump_table(data): - self.server.logger.info(INDENT + line) + # Allow user-scripted responses to handle all control flow (e.g. + # looping, exception handling, etc.) + if cr and cr.handler: + cr.handler(self.request) + else: + try: + + while True: + data = self.request.recv(1024) + if not data: + break - self.request.sendall(data) + if cr and cr.static: + self.request.sendall(cr.static) + else: + self.request.sendall(data) - except socket.timeout: - self.server.logger.warning('Connection timeout') + except socket.timeout: + self.server.logger.warning('Connection timeout') - except socket.error as msg: - self.server.logger.error('Error: %s', msg.strerror or msg) + except socket.error as msg: + self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: - self.server.logger.error('Error: %s', e) + except Exception, e: + self.server.logger.error('Error: %s', e) class ThreadedUDPRequestHandler(SocketServer.BaseRequestHandler): def handle(self): + (data,sock) = self.request - try: - (data,socket) = self.request - - if not data: - return - + if data: for line in hexdump_table(data): self.server.logger.info(INDENT + line) - socket.sendto(data, self.client_address) + cr = self.server.custom_response + if cr: + cr.respondUdp(sock, data, self.client_address) + elif data: + try: + sock.sendto(data, self.client_address) - except socket.error as msg: - self.server.logger.error('Error: %s', msg.strerror or msg) + except socket.error as msg: + self.server.logger.error('Error: %s', msg.strerror or msg) - except Exception, e: - self.server.logger.error('Error: %s', e) + except Exception, e: + self.server.logger.error('Error: %s', e) class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): # Avoid [Errno 98] Address already in use due to TIME_WAIT status on TCP diff --git a/setup.py b/setup.py index 73f13d4..8f4d2df 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( name='FakeNet NG', - version='1.4.10', + version='1.4.11', description="", long_description="", author="FireEye FLARE Team with credit to Peter Kacherginsky as the original developer", diff --git a/test/CustomProviderExample.py b/test/CustomProviderExample.py new file mode 100644 index 0000000..6fa3195 --- /dev/null +++ b/test/CustomProviderExample.py @@ -0,0 +1,72 @@ +import socket + +# To read about customizing HTTP responses, see docs/CustomResponse.md +def HandleRequest(req, method, post_data=None): + """Sample dynamic HTTP response handler. + + Parameters + ---------- + req : BaseHTTPServer.BaseHTTPRequestHandler + The BaseHTTPRequestHandler that recevied the request + method: str + The HTTP method, either 'HEAD', 'GET', 'POST' as of this writing + post_data: str + The HTTP post data received by calling `rfile.read()` against the + BaseHTTPRequestHandler that received the request. + """ + response = 'Ahoy\r\n' + + if method == 'GET': + req.send_response(200) + req.send_header('Content-Length', len(response)) + req.end_headers() + req.wfile.write(response) + + elif method == 'POST': + req.send_response(200) + req.send_header('Content-Length', len(response)) + req.end_headers() + req.wfile.write(response) + + elif method == 'HEAD': + req.send_response(200) + req.end_headers() + + +def HandleTcp(sock): + """Handle a TCP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + """ + while True: + try: + data = None + data = sock.recv(1024) + except socket.timeout: + pass + + if not data: + break + + resp = ''.join([chr(ord(c)+1) for c in data]) + sock.sendall(resp) + + +def HandleUdp(sock, data, addr): + """Handle a UDP buffer. + + Parameters + ---------- + sock : socket + The connected socket with which to recv and send data + data : str + The data received + addr : tuple + The host and port of the remote peer + """ + if data: + resp = ''.join([chr(ord(c)+1) for c in data]) + sock.sendto(resp, addr) diff --git a/test/HTTPCustomProviderExample.py b/test/HTTPCustomProviderExample.py deleted file mode 100644 index 4d6f5fa..0000000 --- a/test/HTTPCustomProviderExample.py +++ /dev/null @@ -1,30 +0,0 @@ -def HandleRequest(req, method, post_data=None): - """Sample dynamic HTTP response handler. - - Parameters - ---------- - req : BaseHTTPServer.BaseHTTPRequestHandler - The BaseHTTPRequestHandler that recevied the request - method: str - The HTTP method, either 'HEAD', 'GET', 'POST' as of this writing - post_data: str - The HTTP post data received by calling `rfile.read()` against the - BaseHTTPRequestHandler that received the request. - """ - response = 'Ahoy\r\n' - - if method == 'GET': - req.send_response(200) - req.send_header('Content-Length', len(response)) - req.end_headers() - req.wfile.write(response) - - elif method == 'POST': - req.send_response(200) - req.send_header('Content-Length', len(response)) - req.end_headers() - req.wfile.write(response) - - elif method == 'HEAD': - req.send_response(200) - req.end_headers() diff --git a/test/custom_responses.ini b/test/custom_responses.ini new file mode 100644 index 0000000..ebcff50 --- /dev/null +++ b/test/custom_responses.ini @@ -0,0 +1,41 @@ +[Example0] +InstanceName: HTTPListener80 +HttpURIs: /test.txt +HttpStaticString: Wraps this with normal FakeNet HTTP headers ()\r\n + +[Example1] +InstanceName: HTTPListener80 +ListenerType: HTTP +HttpHosts: some.random.c2.com, other.c2.com:81 +HttpRawFile: sample_raw_response.txt + +[Example2] +ListenerType: HTTP +HttpHosts: both_host.com +HttpURIs: and_uri.txt +HttpDynamic: CustomProviderExample.py + +[ExampleTCPBase64] +InstanceName: TCPStaticBase64_1000 +TcpStaticBase64: D0wKUg4= + +[ExampleUDPBase64] +InstanceName: UDPStaticBase64_1000 +UdpStaticBase64: D0wKUg4= + +[ExampleTCPStaticString] +InstanceName: TCPStaticString1001 +TcpStaticString: static string TCP response + +[ExampleTCPStaticFile] +InstanceName: TCPStaticFile1002 +TcpRawFile: sample_raw_tcp_response.txt + +[ExampleTCPDynamic] +InstanceName: TCPDynamic1003 +TcpDynamic: CustomProviderExample.py + +[ExampleUDPDynamic] +InstanceName: UDPDynamic1003 +UdpDynamic: CustomProviderExample.py + diff --git a/test/fakenet_http.ini b/test/fakenet_http.ini deleted file mode 100644 index e6af372..0000000 --- a/test/fakenet_http.ini +++ /dev/null @@ -1,12 +0,0 @@ -[Example0] -MatchURIs: /test.txt -StaticString: Wraps this with normal FakeNet HTTP headers ()\r\n - -[Example1] -MatchHosts: some.random.c2.com, other.c2.com -RawFile: sample_raw_response.txt - -[Example2] -MatchHosts: both_host.com -MatchURIs: and_uri.txt -Dynamic: HTTPCustomProviderExample.py diff --git a/test/sample_raw_tcp_response.txt b/test/sample_raw_tcp_response.txt new file mode 100644 index 0000000..57de537 --- /dev/null +++ b/test/sample_raw_tcp_response.txt @@ -0,0 +1 @@ +sample TCP raw file response \ No newline at end of file diff --git a/test/template.ini b/test/template.ini index 6bdc6ef..3b1fc84 100644 --- a/test/template.ini +++ b/test/template.ini @@ -29,7 +29,7 @@ NetworkMode: SingleHost # DebugLevel (Linux only as of this writing): specify fine-grained debug print # flags to enable. Enabling all logging when verbose mode is selected results # in an unacceptable overhead cost, hence this setting. -DebugLevel: NFQUEUE,IPTALBS,NONLOC,GENPKTV,PCAP +DebugLevel: NFQUEUE,IPTABLES,NONLOC,GENPKTV,PCAP # Restrict which interfaces on which Fakenet-NG will intercept and handle # packets. Specify (only) one interface and Fakenet-NG will ignore all other @@ -211,6 +211,66 @@ UseSSL: No Timeout: 10 Hidden: False +[UDPStaticBase64_1000] +Enabled: True +Port: 1000 +Protocol: UDP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + +[TCPStaticBase64_1000] +Enabled: True +Port: 1000 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + +[TCPStaticString1001] +Enabled: True +Port: 1001 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + +[TCPStaticFile1002] +Enabled: True +Port: 1002 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + +[TCPDynamic1003] +Enabled: True +Port: 1003 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + +[UDPDynamic1003] +Enabled: True +Port: 1003 +Protocol: UDP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +Custom: custom_responses.ini + [HiddenRawTcpListener] Enabled: True Port: 12345 @@ -263,8 +323,20 @@ Timeout: 10 DumpHTTPPosts: Yes DumpHTTPPostsFilePrefix: http Hidden: False -Custom: fakenet_http.ini +Custom: custom_responses.ini +[HTTPListener81] +Enabled: True +Port: 81 +Protocol: TCP +Listener: HTTPListener +UseSSL: No +Webroot: defaultFiles/ +Timeout: 10 +DumpHTTPPosts: Yes +DumpHTTPPostsFilePrefix: http +Hidden: False +Custom: custom_responses.ini [HTTPListener8080_ProcessBlack] Enabled: True diff --git a/test/test.py b/test/test.py index b6b173e..56fe330 100644 --- a/test/test.py +++ b/test/test.py @@ -1,920 +1,959 @@ -import os -import re -import sys -import time -import ctypes -import signal -import socket -import pyping -import ftplib -import poplib -import shutil -import hashlib -import smtplib -import logging -import binascii -import platform -import requests -import netifaces -import subprocess -import irc.client -import ConfigParser -from collections import OrderedDict - -logger = logging.getLogger('FakeNetTests') -logging.basicConfig(format='%(message)s', level=logging.INFO) - -def is_admin(): - result = False - try: - result = os.getuid() == 0 - except AttributeError: - result = ctypes.windll.shell32.IsUserAnAdmin() != 0 - return result - -def execute_detached(execute_cmd, winders=False): - DETACHED_PROCESS = 0x00000008 - cflags = DETACHED_PROCESS if winders else 0 - cfds = False if winders else True - shl = False if winders else True - - def ign_sigint(): - # Prevent KeyboardInterrupt in FakeNet-NG's console from - # terminating child processes - signal.signal(signal.SIGINT, signal.SIG_IGN) - - - preexec = None if winders else ign_sigint - - try: - pid = subprocess.Popen(execute_cmd, creationflags=cflags, - shell=shl, - close_fds = cfds, - preexec_fn = preexec).pid - except Exception, e: - logger.info('Error: Failed to execute command: %s', execute_cmd) - logger.info(' %s', e) - return None - else: - return pid - -def get_ips(ipvers): - """Return IP addresses bound to local interfaces including loopbacks. - - Parameters - ---------- - ipvers : list - IP versions desired (4, 6, or both); ensures the netifaces semantics - (e.g. netiface.AF_INET) are localized to this function. - """ - specs = [] - results = [] - - for ver in ipvers: - if ver == 4: - specs.append(netifaces.AF_INET) - elif ver == 6: - specs.append(netifaces.AF_INET6) - else: - raise ValueError('get_ips only supports IP versions 4 and 6') - - for iface in netifaces.interfaces(): - for spec in specs: - addrs = netifaces.ifaddresses(iface) - # If an interface only has an IPv4 or IPv6 address, then 6 or 4 - # respectively will be absent from the keys in the interface - # addresses dictionary. - if spec in addrs: - for link in addrs[spec]: - if 'addr' in link: - results.append(link['addr']) - - return results - -def get_external_ip(): - addrs = get_ips([4]) - for addr in addrs: - if not addr.startswith('127.'): - return addr - -class IrcTester(object): - def __init__(self, hostname, port=6667): - self.hostname = hostname - self.port = port - - self.nick = 'dr_evil' - self.join_chan = '#whatevs' - self.clouseau = 'inspector_clouseau' - self.safehouse = "I'm looking for a safe house." - self.pub_chan = '#evil_bartenders' - self.black_market = 'Black Market' - - def _irc_evt_handler(self, srv, evt): - """Check for each case and set the corresponding success flag.""" - if evt.type == 'join': - if evt.target.startswith(self.join_chan): - self.join_ok = True - elif evt.type == 'welcome': - if evt.arguments[0].startswith('Welcome to IRC'): - self.welcome_ok = True - elif evt.type == 'privmsg': - if (evt.arguments[0].startswith(self.safehouse) and - evt.source.startswith(self.clouseau)): - self.privmsg_ok = True - elif evt.type == 'pubmsg': - if (evt.arguments[0].startswith(self.black_market) and - evt.target == self.pub_chan): - self.pubmsg_ok = True - - def _irc_script(self, srv): - """Callback manages individual test cases for IRC.""" - # Clear success flags - self.welcome_ok = False - self.join_ok = False - self.privmsg_ok = False - self.pubmsg_ok = False - - # This handler should set the success flags in success cases - srv.add_global_handler('join', self._irc_evt_handler) - srv.add_global_handler('welcome', self._irc_evt_handler) - srv.add_global_handler('privmsg', self._irc_evt_handler) - srv.add_global_handler('pubmsg', self._irc_evt_handler) - - # Issue all commands, indirectly invoking the event handler for each - # flag - - srv.join(self.join_chan) - srv.process_data() - - srv.privmsg(self.pub_chan, self.black_market) - srv.process_data() - - srv.privmsg(self.clouseau, self.safehouse) - srv.process_data() - - srv.quit() - srv.process_data() - - if not self.welcome_ok: - raise FakeNetTestException('Welcome test failed') - - if not self.join_ok: - raise FakeNetTestException('Join test failed') - - if not self.privmsg_ok: - raise FakeNetTestException('privmsg test failed') - - if not self.pubmsg_ok: - raise FakeNetTestException('pubmsg test failed') - - return all([ - self.welcome_ok, - self.join_ok, - self.privmsg_ok, - self.pubmsg_ok - ]) - - def _run_irc_script(self, nm, callback): - """Connect to server and give control to callback.""" - r = irc.client.Reactor() - srv = r.server() - srv.connect(self.hostname, self.port, self.nick) - retval = callback(srv) - srv.close() - return retval - - def test_irc(self): - return self._run_irc_script('testnm', self._irc_script) - -class FakeNetTestException(Exception): - """A recognizable exception type indicating a known failure state based on - test criteria. HTTP test uses this, others may in the future, too. - """ - pass - -class FakeNetTester(object): - """Controller for FakeNet-NG that runs test cases""" - - def __init__(self, settings): - self.settings = settings - self.pid_fakenet = None - - def _setStopFlag(self): - with open(self.settings.stopflag, 'w') as f: - f.write('1') - - def _clearStopFlag(self): - if os.path.exists(self.settings.stopflag): - os.remove(self.settings.stopflag) - - def _confirmFakenetStopped(self): - return not os.path.exists(self.settings.stopflag) - - def _waitFakenetStopped(self, timeoutsec=None): - retval = False - - while True: - if self._confirmFakenetStopped(): - retval = True - break - time.sleep(1) - - if timeoutsec is not None: - timeoutsec -= 1 - if timeoutsec <= 0: - break - - return retval - - def _checkPid(self, pid): - retval = False - if self.settings.windows: - PROCESS_TERMINATE = 1 - p = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) - retval = p != 0; - if p: - ctypes.windll.kernel32.CloseHandle(p) - else: - # https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python - try: - os.kill(pid, 0) - except OSError: - pass - else: - retval = True - - return retval - - def _kill(self, pid): - if self.settings.windows: - PROCESS_TERMINATE = 1 - # Note, this will get a handle even after the process terminates, - # in which case TerminateProcess will simply return FALSE. - p = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) - if p: - ok = ctypes.windll.kernel32.TerminateProcess(p, 1) - ctypes.windll.kernel32.CloseHandle(p) - else: - os.kill(pid, signal.SIGKILL) - - def stopFakenetAndWait(self, timeoutsec=None, kill=False): - if not self.pid_fakenet: - raise RuntimeError('FakeNet-NG not running, nothing to stop') - - self._setStopFlag() - stopped_responsive = self._waitFakenetStopped(timeoutsec) - - if not stopped_responsive: - self._clearStopFlag() - - if kill and self._checkPid(self.pid_fakenet): - self._kill(self.pid_fakenet) - - self.pid_fakenet = None - - return stopped_responsive - - def executeFakenet(self): - if self.pid_fakenet: - raise RuntimeError('FakeNet-NG already running, PID %d' % - (self.pid_fakenet)) - - os.chdir(self.settings.fndir) - - max_del_attempts = 3 - if os.path.exists(self.settings.logpath): - for i in range(1, max_del_attempts + 1): - try: - os.remove(self.settings.logpath) - except WindowsError: # i.e. log file locked by another process - logger.warning('Failed to delete %s, attempt %d' % - (self.settings.logpath, i)) - if i == max_del_attempts: - logger.error('Final attempt, re-raising exception') - raise - else: - logger.warning('Retrying in %d seconds...' % (i)) - time.sleep(i) - else: - break - - cmd = self.settings.genFakenetCmd() - logger.info('About to run %s' % (cmd)) - self.pid_fakenet = execute_detached(cmd, self.settings.windows) - if self.pid_fakenet: - logger.info('FakeNet started with PID %s' % (str(self.pid_fakenet))) - - return (self.pid_fakenet is not None) - - def delConfig(self): - if os.path.exists(self.settings.configpath): - os.remove(self.settings.configpath) - if os.path.exists(self.settings.configpath_http): - os.remove(self.settings.configpath_http) - - def doTests(self, match_spec): - self.testGeneral(match_spec) - self.testNoRedirect(match_spec) - self.testBlacklistProcess(match_spec) - self.testWhitelistProcess(match_spec) - - def _printStatus(self, desc, passed): - status = 'Passed' if passed else 'FAILED' - punc = '[ + ]' if passed else '[!!!]' - logger.info('%s %s: %s' % (punc, status, desc)) - - def _tryTest(self, desc, callback, args, expected): - retval = None - try: - retval = callback(*args) - except Exception as e: - logger.info('Test %s: Uncaught exception of type %s: %s' % - (desc, str(type(e)), str(e))) - - passed = (retval == expected) - - return passed - - def _filterMatchingTests(self, tests, matchspec): - """Remove tests that match negative specifications (regexes preceded by - a minus sign) or do not match positive specifications (regexes not - preceded by a minus sign). - - Modifies the contents of the tests dictionary. - """ - negatives = [] - positives = [] - - if len(matchspec): - # If the user specifies a minus sign before a regular expression, - # match negatively (exclude any matching tests) - for spec in matchspec: - if spec.startswith('-'): - negatives.append(spec[1:]) - else: - positives.append(spec) - - # Iterating over tests first, match specifications second to - # preserve the order of the selected tests. Less efficient to - # compile every regex several times, but less confusing. - for testname, test in tests.items(): - - # First determine if it is to be excluded, in which case, - # remove it and do not evaluate further match specifications. - exclude = False - for spec in negatives: - if bool(re.search(spec, testname)): - exclude = True - if exclude: - tests.pop(testname) - continue - - # If the user ONLY specified negative match specifications, - # then admit all tests - if not len(positives): - continue - - # Otherwise, only admit if it matches a positive spec - include = False - for spec in positives: - if bool(re.search(spec, testname)): - include = True - break - if not include: - tests.pop(testname) - - return - - def _testGeneric(self, label, config, tests, matchspec=[]): - self._filterMatchingTests(tests, matchspec) - if not len(tests): - logger.info('No matching tests') - return False - - # If doing a multi-host test, then toggle the network mode - if not self.settings.singlehost: - config.multiHostMode() - - self.writeConfig(config) - - if self.settings.singlehost: - if not self.executeFakenet(): - self.delConfig() - return False - - sec = self.settings.sleep_after_start - logger.info('Sleeping %d seconds before commencing' % (sec)) - time.sleep(sec) - else: - logger.info('Waiting for you to transition the remote FakeNet-NG') - logger.info('system to run the %s test suite' % (label)) - logger.info('(Copy this config: %s)' % (self.settings.configpath)) - logger.info('(And this: %s)' % (self.settings.configpath_http)) - logger.info('') - while True: - logger.info('Type \'ok\' to continue, or \'exit\' to stop') - try: - ok = raw_input() - except EOFError: - ok = 'exit' - - if ok.lower() in ['exit', 'quit', 'stop', 'n', 'no']: - sys.exit(0) - elif ok.lower() in ['ok', 'okay', 'go', 'y', 'yes']: - break - - logger.info('-' * 79) - logger.info('Testing') - logger.info('-' * 79) - - # Do each test - for desc, (callback, args, expected) in tests.iteritems(): - logger.debug('Testing: %s' % (desc)) - passed = self._tryTest(desc, callback, args, expected) - - # Retry in case of transient error e.g. timeout - if not passed: - logger.debug('Retrying: %s' % (desc)) - passed = self._tryTest(desc, callback, args, expected) - - self._printStatus(desc, passed) - - time.sleep(0.5) - - logger.info('-' * 79) - logger.info('Tests complete') - logger.info('-' * 79) - - if self.settings.singlehost: - sec = self.settings.sleep_before_stop - logger.info('Sleeping %d seconds before transitioning' % (sec)) - time.sleep(sec) - - logger.info('Stopping FakeNet-NG and waiting for it to complete') - responsive = self.stopFakenetAndWait(15, True) - - if responsive: - logger.info('FakeNet-NG is stopped') - else: - logger.info('FakeNet-NG was no longer running or was stopped forcibly') - - time.sleep(1) - - self.delConfig() - - def _test_sk(self, proto, host, port, timeout=5): - """Test socket-oriented""" - retval = False - s = socket.socket(socket.AF_INET, proto) - s.settimeout(timeout) - - try: - s.connect((host, port)) - - teststring = 'Testing FakeNet-NG' - remaining = len(teststring) - - while remaining: - sent = s.send(teststring) - if sent == 0: - raise IOError('Failed to send all bytes') - remaining -= sent - - recvd = '' - remaining = len(teststring) - - while remaining: - chunk = s.recv(remaining) - if chunk == '': - raise IOError('Failed to receive all bytes') - remaining -= len(chunk) - recvd += chunk - - retval = (recvd == teststring) - - except socket.error as e: - logger.error('Socket error: %s (%s %s:%d)' % - (str(e), proto, host, port)) - except Exception as e: - logger.error('Non-socket Exception received: %s' % (str(e))) - - return retval - - def _test_icmp(self, host): - r = pyping.ping(host, count=1) - return (r.ret_code == 0) - - def _test_ns(self, hostname, expected): - return (expected == socket.gethostbyname(hostname)) - - def _test_smtp_ssl(self, sender, recipient, msg, hostname, port=None, timeout=5): - smtpserver = smtplib.SMTP_SSL(hostname, port, 'fake.net', None, None, timeout) - server.sendmail(sender, recipient, msg) - smtpserver.quit() - - def _test_smtp(self, sender, recipient, msg, hostname, port=None, timeout=5): - smtpserver = smtplib.SMTP(hostname, port, 'fake.net', timeout) - smtpserver.sendmail(sender, recipient, msg) - smtpserver.quit() - - return True - - def _test_pop(self, hostname, port=None, timeout=5): - pop3server = poplib.POP3(hostname, port, timeout) - pop3server.user('popuser') - pop3server.pass_('password') - msg = pop3server.retr(1) - - response = msg[0] - lines = msg[1] - octets = msg[2] - - if not response.startswith('+OK'): - msg = 'POP3 response does not start with "+OK"' - logger.error(msg) - return False - - if not 'Alice' in ''.join(lines): - msg = 'POP3 message did not contain expected string' - raise FakeNetTestException(msg) - return False - - return True - - def _util_irc(self, nm, hostname, port, nick, callback): - r = irc.client.Reactor() - srv = r.server() - srv.connect(hostname, port, nick) - retval = callback(srv) - srv.close() - return retval - - def _test_irc(self, hostname, port=6667): - irc_tester = IrcTester(hostname, port) - return irc_tester.test_irc() - - def _test_http(self, hostname, port=None, scheme=None, uri=None, - teststring=None): - """Test HTTP Listener""" - retval = False - - scheme = scheme if scheme else 'http' - uri = uri.lstrip('/') if uri else 'asdf.html' - teststring = teststring if teststring else 'H T T P L I S T E N E R' - - if port: - url = '%s://%s:%d/%s' % (scheme, hostname, port, uri) - else: - url = '%s://%s/%s' % (scheme, hostname, uri) - - try: - r = requests.get(url, timeout=3) - - if r.status_code != 200: - raise FakeNetTestException('Status code %d' % (r.status_code)) - - if teststring not in r.text: - raise FakeNetTestException('Test string not in response') - - retval = True - - except requests.exceptions.Timeout as e: - pass - - except FakeNetTestException as e: - pass - - return retval - - def _test_ftp(self, hostname, port=None): - """Note that the FakeNet-NG Proxy listener won't know what to do with - this client if you point it at some random port, because the client - listens silently for the server 220 welcome message which doesn't give - the Proxy listener anything to work with to decide where to forward it. - """ - fullbuf = '' - - m = hashlib.md5() - - def update_hash(buf): - m.update(buf) - - f = ftplib.FTP() - f.connect(hostname, port) - f.login() - f.set_pasv(False) - f.retrbinary('RETR FakeNet.gif', update_hash) - f.quit() - - digest = m.digest() - expected = binascii.unhexlify('a6b78c4791dc8110dec6c55f8a756395') - - return (digest == expected) - - def testNoRedirect(self, matchspec=[]): - config = self.makeConfig(singlehostmode=True, proxied=False, redirectall=False) - - domain_dne = self.settings.domain_dne - ext_ip = self.settings.ext_ip - arbitrary = self.settings.arbitrary - localhost = self.settings.localhost - - tcp = socket.SOCK_STREAM - udp = socket.SOCK_DGRAM - - t = OrderedDict() # The tests - - t['RedirectAllTraffic disabled external IP @ bound'] = (self._test_sk, (tcp, ext_ip, 1337), True) - t['RedirectAllTraffic disabled external IP @ unbound'] = (self._test_sk, (tcp, ext_ip, 9999), False) - - t['RedirectAllTraffic disabled arbitrary host @ bound'] = (self._test_sk, (tcp, arbitrary, 1337), False) - t['RedirectAllTraffic disabled arbitrary host @ unbound'] = (self._test_sk, (tcp, arbitrary, 9999), False) - - t['RedirectAllTraffic disabled named host @ bound'] = (self._test_sk, (tcp, domain_dne, 1337), False) - t['RedirectAllTraffic disabled named host @ unbound'] = (self._test_sk, (tcp, domain_dne, 9999), False) - - if self.settings.singlehost: - t['RedirectAllTraffic disabled localhost @ bound'] = (self._test_sk, (tcp, localhost, 1337), True) - t['RedirectAllTraffic disabled localhost @ unbound'] = (self._test_sk, (tcp, localhost, 9999), False) - - return self._testGeneric('No Redirect', config, t, matchspec) - - def testBlacklistProcess(self, matchspec=[]): - config = self.makeConfig() - config.blacklistProcess(self.settings.pythonname) - - arbitrary = self.settings.arbitrary - - tcp = socket.SOCK_STREAM - udp = socket.SOCK_DGRAM - - t = OrderedDict() # The tests - - if self.settings.singlehost: - t['Global blacklisted process test'] = (self._test_sk, (tcp, arbitrary, 9999), False) - - return self._testGeneric('Global process blacklist', config, t, matchspec) - - def testWhitelistProcess(self, matchspec=[]): - config = self.makeConfig() - config.whitelistProcess(self.settings.pythonname) - - arbitrary = self.settings.arbitrary - - tcp = socket.SOCK_STREAM - udp = socket.SOCK_DGRAM - - t = OrderedDict() # The tests - - if self.settings.singlehost: - t['Global whitelisted process test'] = (self._test_sk, (tcp, arbitrary, 9999), True) - - return self._testGeneric('Global process whitelist', config, t, matchspec) - - def testGeneral(self, matchspec=[]): - config = self.makeConfig() - - domain_dne = self.settings.domain_dne - ext_ip = self.settings.ext_ip - arbitrary = self.settings.arbitrary - blacklistedhost = self.settings.blacklistedhost - blacklistedtcp = self.settings.blacklistedtcp - blacklistedudp = self.settings.blacklistedudp - localhost = self.settings.localhost - dns_expected = self.settings.dns_expected - hidden_tcp = self.settings.hidden_tcp - no_service = self.settings.no_service - - sender = self.settings.sender - recipient = self.settings.recipient - smtpmsg = self.settings.smtpmsg - - tcp = socket.SOCK_STREAM - udp = socket.SOCK_DGRAM - - t = OrderedDict() # The tests - - t['TCP external IP @ bound'] = (self._test_sk, (tcp, ext_ip, 1337), True) - t['TCP external IP @ unbound'] = (self._test_sk, (tcp, ext_ip, 9999), True) - t['TCP arbitrary @ bound'] = (self._test_sk, (tcp, arbitrary, 1337), True) - t['TCP arbitrary @ unbound'] = (self._test_sk, (tcp, arbitrary, 9999), True) - t['TCP domainname @ bound'] = (self._test_sk, (tcp, domain_dne, 1337), True) - t['TCP domainname @ unbound'] = (self._test_sk, (tcp, domain_dne, 9999), True) - if self.settings.singlehost: - t['TCP localhost @ bound'] = (self._test_sk, (tcp, localhost, 1337), True) - t['TCP localhost @ unbound'] = (self._test_sk, (tcp, localhost, 9999), False) - - t['UDP external IP @ bound'] = (self._test_sk, (udp, ext_ip, 1337), True) - t['UDP external IP @ unbound'] = (self._test_sk, (udp, ext_ip, 9999), True) - t['UDP arbitrary @ bound'] = (self._test_sk, (udp, arbitrary, 1337), True) - t['UDP arbitrary @ unbound'] = (self._test_sk, (udp, arbitrary, 9999), True) - t['UDP domainname @ bound'] = (self._test_sk, (udp, domain_dne, 1337), True) - t['UDP domainname @ unbound'] = (self._test_sk, (udp, domain_dne, 9999), True) - if self.settings.singlehost: - t['UDP localhost @ bound'] = (self._test_sk, (udp, localhost, 1337), True) - t['UDP localhost @ unbound'] = (self._test_sk, (udp, localhost, 9999), False) - - t['ICMP external IP'] = (self._test_icmp, (ext_ip,), True) - t['ICMP arbitrary host'] = (self._test_icmp, (arbitrary,), True) - t['ICMP domainname'] = (self._test_icmp, (domain_dne,), True) - - t['DNS listener test'] = (self._test_ns, (domain_dne, dns_expected), True) - t['HTTP listener test'] = (self._test_http, (arbitrary,), True) - t['HTTP custom test by URI'] = (self._test_http, (arbitrary, None, None, '/test.txt', 'Wraps this'), True) - t['HTTP custom test by hostname'] = (self._test_http, ('other.c2.com', None, None, None, 'success'), True) - t['HTTP custom test by both URI and hostname'] = (self._test_http, ('both_host.com', None, None, '/and_uri.txt', 'Ahoy'), True) - t['HTTP custom test by both URI and hostname negative'] = (self._test_http, ('both_host.com', None, None, '/not_uri.txt', 'Ahoy'), False) - t['FTP listener test'] = (self._test_ftp, (arbitrary,), True) - t['POP3 listener test'] = (self._test_pop, (arbitrary, 110), True) - t['SMTP listener test'] = (self._test_smtp, (sender, recipient, smtpmsg, arbitrary), True) - - # Does not work, SSL error - t['SMTP SSL listener test'] = (self._test_smtp_ssl, (sender, recipient, smtpmsg, arbitrary), True) - - # Works on Linux, not on Windows - t['IRC listener test'] = (self._test_irc, (arbitrary,), True) - - t['Proxy listener HTTP test'] = (self._test_http, (arbitrary, no_service), True) - t['Proxy listener HTTP hidden test'] = (self._test_http, (arbitrary, hidden_tcp), True) - - t['TCP blacklisted host @ unbound'] = (self._test_sk, (tcp, blacklistedhost, 9999), False) - t['TCP arbitrary @ blacklisted unbound'] = (self._test_sk, (tcp, arbitrary, blacklistedtcp), False) - t['UDP arbitrary @ blacklisted unbound'] = (self._test_sk, (udp, arbitrary, blacklistedudp), False) - - if self.settings.singlehost: - t['Listener process blacklist'] = (self._test_http, (arbitrary, self.settings.listener_proc_black), False) - t['Listener process whitelist'] = (self._test_http, (arbitrary, self.settings.listener_proc_white), True) - t['Listener host blacklist'] = (self._test_http, (arbitrary, self.settings.listener_host_black), True) - t['Listener host whitelist'] = (self._test_http, (arbitrary, self.settings.listener_host_black), True) - - return self._testGeneric('General', config, t, matchspec) - - def makeConfig(self, singlehostmode=True, proxied=True, redirectall=True): - template = self.settings.configtemplate - return FakeNetConfig(template, singlehostmode, proxied, redirectall) - - def writeConfig(self, config): - logger.info('Writing config to %s' % (self.settings.configpath)) - config.write(self.settings.configpath) - for filename in self.settings.ancillary_files: - path = os.path.join(self.settings.startingpath, filename) - dest = os.path.join(self.settings.ancillary_files_dest, filename) - shutil.copyfile(path, dest) - -class FakeNetConfig: - """Convenience class to read/modify/rewrite a configuration template.""" - - def __init__(self, path, singlehostmode=True, proxied=True, redirectall=True): - self.rawconfig = ConfigParser.RawConfigParser() - self.rawconfig.read(path) - - if singlehostmode: - self.singleHostMode() - else: - self.multiHostMode() - - if not proxied: self.noProxy() - - self.setRedirectAll(redirectall) - - def blacklistProcess(self, process): self.rawconfig.set('Diverter', 'ProcessBlacklist', process) - def whitelistProcess(self, process): self.rawconfig.set('Diverter', 'ProcessWhitelist', process) - - def setRedirectAll(self, enabled): - if enabled: - self.rawconfig.set('Diverter', 'RedirectAllTraffic', 'Yes') - else: - self.rawconfig.set('Diverter', 'RedirectAllTraffic', 'No') - - def singleHostMode(self): self.rawconfig.set('Diverter', 'NetworkMode', 'SingleHost') - def multiHostMode(self): self.rawconfig.set('Diverter', 'NetworkMode', 'MultiHost') - - def noProxy(self): - self.rawconfig.remove_section('ProxyTCPListener') - self.rawconfig.remove_section('ProxyUDPListener') - self.rawconfig.set('Diverter', 'DefaultTCPListener', 'RawTCPListener') - self.rawconfig.set('Diverter', 'DefaultUDPListener', 'RawUDPListener') - - def write(self, path): - with open(path, 'w') as f: - return self.rawconfig.write(f) - -class FakeNetTestSettings: - """Test constants/literals, some of which may vary per OS, etc.""" - - def __init__(self, startingpath, singlehost=True): - # Where am I? Who are you? - self.platform_name = platform.system() - self.windows = (self.platform_name == 'Windows') - self.linux = (self.platform_name.lower().startswith('linux')) - - # Test parameters - self.singlehost = singlehost - self.startingpath = startingpath - self.configtemplate = os.path.join(startingpath, 'template.ini') - - self.ancillary_files_dest = self.genPath('%TEMP%', '/tmp/') - self.ancillary_files = [ - 'fakenet_http.ini', - 'HTTPCustomProviderExample.py', - 'sample_raw_response.txt', - ] - - # Paths - self.configpath = self.genPath('%TEMP%\\fakenet.ini', '/tmp/fakenet.ini') - self.configpath_http = self.genPath('%TEMP%\\fakenet_http.ini', '/tmp/fakenet_http.ini') - self.stopflag = self.genPath('%TEMP%\\stop_fakenet', '/tmp/stop_fakenet') - self.logpath = self.genPath('%TEMP%\\fakenet.log', '/tmp/fakenet.log') - self.fakenet = self.genPath('fakenet', 'python fakenet.py') - self.fndir = self.genPath('.', '$HOME/files/src/flare-fakenet-ng/fakenet') - - # For process blacklisting - self.pythonname = os.path.basename(sys.executable) - - # Various - self.ext_ip = get_external_ip() - self.arbitrary = '8.8.8.8' - self.blacklistedhost = '6.6.6.6' - self.blacklistedtcp = 139 - self.blacklistedudp = 67 - self.hidden_tcp = 12345 - self.no_service = 10 - self.listener_proc_black = 8080 # HTTP listener with process blacklist - self.listener_proc_white = 8081 # HTTP listener with process whitelists - self.listener_host_black = 8082 # HTTP listener with host blacklist - self.listener_host_white = 8083 # HTTP listener with host whitelists - self.localhost = '127.0.0.1' - self.dns_expected = '192.0.2.123' - self.domain_dne = 'does-not-exist-amirite.fireeye.com' - self.sender = 'from-fakenet@example.org' - self.recipient = 'to-fakenet@example.org' - self.smtpmsg = 'FakeNet-NG SMTP test email' - - # Behaviors - self.sleep_after_start = 4 - self.sleep_before_stop = 1 - - def genPath(self, winpath, unixypath): - if self.windows: - return os.path.expandvars(winpath) - else: - return os.path.expandvars(unixypath) - - def genFakenetCmd(self): - return ('%s -f %s -n -l %s -c %s' % - (self.fakenet, self.stopflag, self.logpath, self.configpath)) - -def is_ip(s): - pat = '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' - return bool(re.match(pat, s)) - -def main(): - if not is_admin(): - logger.error('Not an admin, exiting...') - sys.exit(1) - - if len(sys.argv) < 2: - logger.error('Usage: test.py [matchspec1 [matchspec2 [...] ] ]') - logger.error('') - logger.error('Valid where:') - logger.error(' here') - logger.error(' Any dot-decimal IP address') - logger.error('') - logger.error('Each match specification is a regular expression that') - logger.error('will be compared against test names, and any matches') - logger.error('will be included. Because regular expression negative') - logger.error('matching is complicated to use, you can just prefix') - logger.error('a match specification with a minus sign to indicate') - logger.error('that you would like to include only tests that do NOT') - logger.error('match the expression.') - sys.exit(1) - - # Validate where - where = sys.argv[1] - - singlehost = (where.lower() == 'here') - - if not singlehost and not is_ip(where): - logger.error('Invalid where: %s' % (where)) - sys.exit(1) - - # Will execute only tests matching *match_spec if specified - match_spec = sys.argv[2:] - - if len(match_spec): - logger.info('Only running tests that match the following ' + - 'specifications:') - for spec in match_spec: - logger.info(' %s' % (spec)) - - # Doit - startingpath = os.getcwd() - settings = FakeNetTestSettings(startingpath, singlehost) - if not singlehost: # was an IP, so record it - settings.ext_ip = where - tester = FakeNetTester(settings) - logger.info('Running with privileges on %s' % (settings.platform_name)) - tester.doTests(match_spec) - -if __name__ == '__main__': - main() +import os +import re +import sys +import time +import errno +import ctypes +import signal +import socket +import pyping +import ftplib +import poplib +import shutil +import hashlib +import smtplib +import logging +import zipfile +import binascii +import platform +import requests +import netifaces +import subprocess +import irc.client +import ConfigParser +from collections import OrderedDict + +logger = logging.getLogger('FakeNetTests') +logging.basicConfig(format='%(message)s', level=logging.INFO) + +def is_admin(): + result = False + try: + result = os.getuid() == 0 + except AttributeError: + result = ctypes.windll.shell32.IsUserAnAdmin() != 0 + return result + +def execute_detached(execute_cmd, winders=False): + DETACHED_PROCESS = 0x00000008 + cflags = DETACHED_PROCESS if winders else 0 + cfds = False if winders else True + shl = False if winders else True + + def ign_sigint(): + # Prevent KeyboardInterrupt in FakeNet-NG's console from + # terminating child processes + signal.signal(signal.SIGINT, signal.SIG_IGN) + + + preexec = None if winders else ign_sigint + + try: + pid = subprocess.Popen(execute_cmd, creationflags=cflags, + shell=shl, + close_fds = cfds, + preexec_fn = preexec).pid + except Exception, e: + logger.info('Error: Failed to execute command: %s', execute_cmd) + logger.info(' %s', e) + return None + else: + return pid + +def get_ips(ipvers): + """Return IP addresses bound to local interfaces including loopbacks. + + Parameters + ---------- + ipvers : list + IP versions desired (4, 6, or both); ensures the netifaces semantics + (e.g. netiface.AF_INET) are localized to this function. + """ + specs = [] + results = [] + + for ver in ipvers: + if ver == 4: + specs.append(netifaces.AF_INET) + elif ver == 6: + specs.append(netifaces.AF_INET6) + else: + raise ValueError('get_ips only supports IP versions 4 and 6') + + for iface in netifaces.interfaces(): + for spec in specs: + addrs = netifaces.ifaddresses(iface) + # If an interface only has an IPv4 or IPv6 address, then 6 or 4 + # respectively will be absent from the keys in the interface + # addresses dictionary. + if spec in addrs: + for link in addrs[spec]: + if 'addr' in link: + results.append(link['addr']) + + return results + +def get_external_ip(): + addrs = get_ips([4]) + for addr in addrs: + if not addr.startswith('127.'): + return addr + +class IrcTester(object): + def __init__(self, hostname, port=6667): + self.hostname = hostname + self.port = port + + self.nick = 'dr_evil' + self.join_chan = '#whatevs' + self.clouseau = 'inspector_clouseau' + self.safehouse = "I'm looking for a safe house." + self.pub_chan = '#evil_bartenders' + self.black_market = 'Black Market' + + def _irc_evt_handler(self, srv, evt): + """Check for each case and set the corresponding success flag.""" + if evt.type == 'join': + if evt.target.startswith(self.join_chan): + self.join_ok = True + elif evt.type == 'welcome': + if evt.arguments[0].startswith('Welcome to IRC'): + self.welcome_ok = True + elif evt.type == 'privmsg': + if (evt.arguments[0].startswith(self.safehouse) and + evt.source.startswith(self.clouseau)): + self.privmsg_ok = True + elif evt.type == 'pubmsg': + if (evt.arguments[0].startswith(self.black_market) and + evt.target == self.pub_chan): + self.pubmsg_ok = True + + def _irc_script(self, srv): + """Callback manages individual test cases for IRC.""" + # Clear success flags + self.welcome_ok = False + self.join_ok = False + self.privmsg_ok = False + self.pubmsg_ok = False + + # This handler should set the success flags in success cases + srv.add_global_handler('join', self._irc_evt_handler) + srv.add_global_handler('welcome', self._irc_evt_handler) + srv.add_global_handler('privmsg', self._irc_evt_handler) + srv.add_global_handler('pubmsg', self._irc_evt_handler) + + # Issue all commands, indirectly invoking the event handler for each + # flag + + srv.join(self.join_chan) + srv.process_data() + + srv.privmsg(self.pub_chan, self.black_market) + srv.process_data() + + srv.privmsg(self.clouseau, self.safehouse) + srv.process_data() + + srv.quit() + srv.process_data() + + if not self.welcome_ok: + raise FakeNetTestException('Welcome test failed') + + if not self.join_ok: + raise FakeNetTestException('Join test failed') + + if not self.privmsg_ok: + raise FakeNetTestException('privmsg test failed') + + if not self.pubmsg_ok: + raise FakeNetTestException('pubmsg test failed') + + return all([ + self.welcome_ok, + self.join_ok, + self.privmsg_ok, + self.pubmsg_ok + ]) + + def _run_irc_script(self, nm, callback): + """Connect to server and give control to callback.""" + r = irc.client.Reactor() + srv = r.server() + srv.connect(self.hostname, self.port, self.nick) + retval = callback(srv) + srv.close() + return retval + + def test_irc(self): + return self._run_irc_script('testnm', self._irc_script) + +class FakeNetTestException(Exception): + """A recognizable exception type indicating a known failure state based on + test criteria. HTTP test uses this, others may in the future, too. + """ + pass + +class FakeNetTester(object): + """Controller for FakeNet-NG that runs test cases""" + + def __init__(self, settings): + self.settings = settings + self.pid_fakenet = None + + def _setStopFlag(self): + with open(self.settings.stopflag, 'w') as f: + f.write('1') + + def _clearStopFlag(self): + if os.path.exists(self.settings.stopflag): + os.remove(self.settings.stopflag) + + def _confirmFakenetStopped(self): + return not os.path.exists(self.settings.stopflag) + + def _waitFakenetStopped(self, timeoutsec=None): + retval = False + + while True: + if self._confirmFakenetStopped(): + retval = True + break + time.sleep(1) + + if timeoutsec is not None: + timeoutsec -= 1 + if timeoutsec <= 0: + break + + return retval + + def _checkPid(self, pid): + retval = False + if self.settings.windows: + PROCESS_TERMINATE = 1 + p = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) + retval = p != 0; + if p: + ctypes.windll.kernel32.CloseHandle(p) + else: + # https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python + try: + os.kill(pid, 0) + except OSError: + pass + else: + retval = True + + return retval + + def _kill(self, pid): + if self.settings.windows: + PROCESS_TERMINATE = 1 + # Note, this will get a handle even after the process terminates, + # in which case TerminateProcess will simply return FALSE. + p = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) + if p: + ok = ctypes.windll.kernel32.TerminateProcess(p, 1) + ctypes.windll.kernel32.CloseHandle(p) + else: + os.kill(pid, signal.SIGKILL) + + def stopFakenetAndWait(self, timeoutsec=None, kill=False): + if not self.pid_fakenet: + raise RuntimeError('FakeNet-NG not running, nothing to stop') + + self._setStopFlag() + stopped_responsive = self._waitFakenetStopped(timeoutsec) + + if not stopped_responsive: + self._clearStopFlag() + + if kill and self._checkPid(self.pid_fakenet): + self._kill(self.pid_fakenet) + + self.pid_fakenet = None + + return stopped_responsive + + def executeFakenet(self): + if self.pid_fakenet: + raise RuntimeError('FakeNet-NG already running, PID %d' % + (self.pid_fakenet)) + + os.chdir(self.settings.fndir) + + max_del_attempts = 3 + if os.path.exists(self.settings.logpath): + for i in range(1, max_del_attempts + 1): + try: + os.remove(self.settings.logpath) + except WindowsError: # i.e. log file locked by another process + logger.warning('Failed to delete %s, attempt %d' % + (self.settings.logpath, i)) + if i == max_del_attempts: + logger.error('Final attempt, re-raising exception') + raise + else: + logger.warning('Retrying in %d seconds...' % (i)) + time.sleep(i) + else: + break + + cmd = self.settings.genFakenetCmd() + logger.info('About to run %s' % (cmd)) + self.pid_fakenet = execute_detached(cmd, self.settings.windows) + if self.pid_fakenet: + logger.info('FakeNet started with PID %s' % (str(self.pid_fakenet))) + + return (self.pid_fakenet is not None) + + def delConfig(self): + if os.path.exists(self.settings.configpath): + os.remove(self.settings.configpath) + + def doTests(self, match_spec): + self.testGeneral(match_spec) + self.testNoRedirect(match_spec) + self.testBlacklistProcess(match_spec) + self.testWhitelistProcess(match_spec) + + def _printStatus(self, desc, passed): + status = 'Passed' if passed else 'FAILED' + punc = '[ + ]' if passed else '[!!!]' + logger.info('%s %s: %s' % (punc, status, desc)) + + def _tryTest(self, desc, callback, args, expected): + retval = None + try: + retval = callback(*args) + except Exception as e: + logger.info('Test %s: Uncaught exception of type %s: %s' % + (desc, str(type(e)), str(e))) + + passed = (retval == expected) + + return passed + + def _filterMatchingTests(self, tests, matchspec): + """Remove tests that match negative specifications (regexes preceded by + a minus sign) or do not match positive specifications (regexes not + preceded by a minus sign). + + Modifies the contents of the tests dictionary. + """ + negatives = [] + positives = [] + + if len(matchspec): + # If the user specifies a minus sign before a regular expression, + # match negatively (exclude any matching tests) + for spec in matchspec: + if spec.startswith('-'): + negatives.append(spec[1:]) + else: + positives.append(spec) + + # Iterating over tests first, match specifications second to + # preserve the order of the selected tests. Less efficient to + # compile every regex several times, but less confusing. + for testname, test in tests.items(): + + # First determine if it is to be excluded, in which case, + # remove it and do not evaluate further match specifications. + exclude = False + for spec in negatives: + if bool(re.search(spec, testname)): + exclude = True + if exclude: + tests.pop(testname) + continue + + # If the user ONLY specified negative match specifications, + # then admit all tests + if not len(positives): + continue + + # Otherwise, only admit if it matches a positive spec + include = False + for spec in positives: + if bool(re.search(spec, testname)): + include = True + break + if not include: + tests.pop(testname) + + return + + def _mkzip(self, zip_path, files): + zip_basename = os.path.splitext(os.path.split(zip_path)[-1])[0] + with zipfile.ZipFile(zip_path, mode='w') as z: + for filepath in files: + filename = os.path.split(filepath)[-1] + arcname = os.path.join(zip_basename, filename) + print(arcname) + z.write(filepath, arcname) + + def _testGeneric(self, label, config, tests, matchspec=[]): + self._filterMatchingTests(tests, matchspec) + if not len(tests): + logger.info('No matching tests') + return False + + # If doing a multi-host test, then toggle the network mode + if not self.settings.singlehost: + config.multiHostMode() + + self.writeConfig(config) + + if self.settings.singlehost: + if not self.executeFakenet(): + self.delConfig() + return False + + sec = self.settings.sleep_after_start + logger.info('Sleeping %d seconds before commencing' % (sec)) + time.sleep(sec) + else: + zip_path = os.path.join(self.settings.ancillary_files_dest, + 'fakenet-test.zip') + afpaths = [os.path.join(self.settings.ancillary_files_dest, af) + for af in self.settings.ancillary_files] + files = [self.settings.configpath] + afpaths + self._mkzip(zip_path, files) + + logger.info('Waiting for you to transition the remote FakeNet-NG') + logger.info('system to run the %s test suite' % (label)) + logger.info(('***Copy and uncompress this archive on the test ' + 'system: %s') % (zip_path)) + logger.info('') + + while True: + logger.info('Type \'ok\' to continue, or \'exit\' to stop') + try: + ok = raw_input() + except EOFError: + ok = 'exit' + + if ok.lower() in ['exit', 'quit', 'stop', 'n', 'no']: + sys.exit(0) + elif ok.lower() in ['ok', 'okay', 'go', 'y', 'yes']: + break + + logger.info('-' * 79) + logger.info('Testing') + logger.info('-' * 79) + + # Do each test + for desc, (callback, args, expected) in tests.iteritems(): + logger.debug('Testing: %s' % (desc)) + passed = self._tryTest(desc, callback, args, expected) + + # Retry in case of transient error e.g. timeout + if not passed: + logger.debug('Retrying: %s' % (desc)) + passed = self._tryTest(desc, callback, args, expected) + + self._printStatus(desc, passed) + + time.sleep(0.5) + + logger.info('-' * 79) + logger.info('Tests complete') + logger.info('-' * 79) + + if self.settings.singlehost: + sec = self.settings.sleep_before_stop + logger.info('Sleeping %d seconds before transitioning' % (sec)) + time.sleep(sec) + + logger.info('Stopping FakeNet-NG and waiting for it to complete') + responsive = self.stopFakenetAndWait(15, True) + + if responsive: + logger.info('FakeNet-NG is stopped') + else: + logger.info('FakeNet-NG was no longer running or was stopped forcibly') + + time.sleep(1) + + self.delConfig() + + def _test_sk(self, proto, host, port, teststring=None, expected=None, + timeout=5): + """Test socket-oriented""" + retval = False + s = socket.socket(socket.AF_INET, proto) + s.settimeout(timeout) + + try: + s.connect((host, port)) + + if teststring is None: + teststring = 'Testing FakeNet-NG' + + if expected is None: + # RawListener is an echo server unless otherwise configured + expected = teststring + + remaining = len(teststring) + + while remaining: + sent = s.send(teststring[-remaining:]) + if sent == 0: + raise IOError('Failed to send any bytes') + remaining -= sent + + recvd = '' + + recvd = s.recv(4096) + + retval = (recvd == expected) + + except socket.error as e: + logger.error('Socket error: %s (%s %s:%d)' % + (str(e), proto, host, port)) + except Exception as e: + logger.error('Non-socket Exception received: %s' % (str(e))) + + return retval + + def _test_icmp(self, host): + r = pyping.ping(host, count=1) + return (r.ret_code == 0) + + def _test_ns(self, hostname, expected): + return (expected == socket.gethostbyname(hostname)) + + def _test_smtp_ssl(self, sender, recipient, msg, hostname, port=None, timeout=5): + smtpserver = smtplib.SMTP_SSL(hostname, port, 'fake.net', None, None, timeout) + server.sendmail(sender, recipient, msg) + smtpserver.quit() + + def _test_smtp(self, sender, recipient, msg, hostname, port=None, timeout=5): + smtpserver = smtplib.SMTP(hostname, port, 'fake.net', timeout) + smtpserver.sendmail(sender, recipient, msg) + smtpserver.quit() + + return True + + def _test_pop(self, hostname, port=None, timeout=5): + pop3server = poplib.POP3(hostname, port, timeout) + pop3server.user('popuser') + pop3server.pass_('password') + msg = pop3server.retr(1) + + response = msg[0] + lines = msg[1] + octets = msg[2] + + if not response.startswith('+OK'): + msg = 'POP3 response does not start with "+OK"' + logger.error(msg) + return False + + if not 'Alice' in ''.join(lines): + msg = 'POP3 message did not contain expected string' + raise FakeNetTestException(msg) + return False + + return True + + def _util_irc(self, nm, hostname, port, nick, callback): + r = irc.client.Reactor() + srv = r.server() + srv.connect(hostname, port, nick) + retval = callback(srv) + srv.close() + return retval + + def _test_irc(self, hostname, port=6667): + irc_tester = IrcTester(hostname, port) + return irc_tester.test_irc() + + def _test_http(self, hostname, port=None, scheme=None, uri=None, + teststring=None): + """Test HTTP Listener""" + retval = False + + scheme = scheme if scheme else 'http' + uri = uri.lstrip('/') if uri else 'asdf.html' + teststring = teststring if teststring else 'H T T P L I S T E N E R' + + if port: + url = '%s://%s:%d/%s' % (scheme, hostname, port, uri) + else: + url = '%s://%s/%s' % (scheme, hostname, uri) + + try: + r = requests.get(url, timeout=3) + + if r.status_code != 200: + raise FakeNetTestException('Status code %d' % (r.status_code)) + + if teststring not in r.text: + raise FakeNetTestException('Test string not in response') + + retval = True + + except requests.exceptions.Timeout as e: + pass + + except FakeNetTestException as e: + pass + + return retval + + def _test_ftp(self, hostname, port=None): + """Note that the FakeNet-NG Proxy listener won't know what to do with + this client if you point it at some random port, because the client + listens silently for the server 220 welcome message which doesn't give + the Proxy listener anything to work with to decide where to forward it. + """ + fullbuf = '' + + m = hashlib.md5() + + def update_hash(buf): + m.update(buf) + + f = ftplib.FTP() + f.connect(hostname, port) + f.login() + f.set_pasv(False) + f.retrbinary('RETR FakeNet.gif', update_hash) + f.quit() + + digest = m.digest() + expected = binascii.unhexlify('a6b78c4791dc8110dec6c55f8a756395') + + return (digest == expected) + + def testNoRedirect(self, matchspec=[]): + config = self.makeConfig(singlehostmode=True, proxied=False, redirectall=False) + + domain_dne = self.settings.domain_dne + ext_ip = self.settings.ext_ip + arbitrary = self.settings.arbitrary + localhost = self.settings.localhost + + tcp = socket.SOCK_STREAM + udp = socket.SOCK_DGRAM + + t = OrderedDict() # The tests + + t['RedirectAllTraffic disabled external IP @ bound'] = (self._test_sk, (tcp, ext_ip, 1337), True) + t['RedirectAllTraffic disabled external IP @ unbound'] = (self._test_sk, (tcp, ext_ip, 9999), False) + + t['RedirectAllTraffic disabled arbitrary host @ bound'] = (self._test_sk, (tcp, arbitrary, 1337), False) + t['RedirectAllTraffic disabled arbitrary host @ unbound'] = (self._test_sk, (tcp, arbitrary, 9999), False) + + t['RedirectAllTraffic disabled named host @ bound'] = (self._test_sk, (tcp, domain_dne, 1337), False) + t['RedirectAllTraffic disabled named host @ unbound'] = (self._test_sk, (tcp, domain_dne, 9999), False) + + if self.settings.singlehost: + t['RedirectAllTraffic disabled localhost @ bound'] = (self._test_sk, (tcp, localhost, 1337), True) + t['RedirectAllTraffic disabled localhost @ unbound'] = (self._test_sk, (tcp, localhost, 9999), False) + + return self._testGeneric('No Redirect', config, t, matchspec) + + def testBlacklistProcess(self, matchspec=[]): + config = self.makeConfig() + config.blacklistProcess(self.settings.pythonname) + + arbitrary = self.settings.arbitrary + + tcp = socket.SOCK_STREAM + udp = socket.SOCK_DGRAM + + t = OrderedDict() # The tests + + if self.settings.singlehost: + t['Global blacklisted process test'] = (self._test_sk, (tcp, arbitrary, 9999), False) + + return self._testGeneric('Global process blacklist', config, t, matchspec) + + def testWhitelistProcess(self, matchspec=[]): + config = self.makeConfig() + config.whitelistProcess(self.settings.pythonname) + + arbitrary = self.settings.arbitrary + + tcp = socket.SOCK_STREAM + udp = socket.SOCK_DGRAM + + t = OrderedDict() # The tests + + if self.settings.singlehost: + t['Global whitelisted process test'] = (self._test_sk, (tcp, arbitrary, 9999), True) + + return self._testGeneric('Global process whitelist', config, t, matchspec) + + def testGeneral(self, matchspec=[]): + config = self.makeConfig() + + domain_dne = self.settings.domain_dne + ext_ip = self.settings.ext_ip + arbitrary = self.settings.arbitrary + blacklistedhost = self.settings.blacklistedhost + blacklistedtcp = self.settings.blacklistedtcp + blacklistedudp = self.settings.blacklistedudp + localhost = self.settings.localhost + dns_expected = self.settings.dns_expected + hidden_tcp = self.settings.hidden_tcp + no_service = self.settings.no_service + + sender = self.settings.sender + recipient = self.settings.recipient + smtpmsg = self.settings.smtpmsg + + tcp = socket.SOCK_STREAM + udp = socket.SOCK_DGRAM + + t = OrderedDict() # The tests + + t['TCP external IP @ bound'] = (self._test_sk, (tcp, ext_ip, 1337), True) + t['TCP external IP @ unbound'] = (self._test_sk, (tcp, ext_ip, 9999), True) + t['TCP arbitrary @ bound'] = (self._test_sk, (tcp, arbitrary, 1337), True) + t['TCP arbitrary @ unbound'] = (self._test_sk, (tcp, arbitrary, 9999), True) + t['TCP domainname @ bound'] = (self._test_sk, (tcp, domain_dne, 1337), True) + t['TCP domainname @ unbound'] = (self._test_sk, (tcp, domain_dne, 9999), True) + if self.settings.singlehost: + t['TCP localhost @ bound'] = (self._test_sk, (tcp, localhost, 1337), True) + t['TCP localhost @ unbound'] = (self._test_sk, (tcp, localhost, 9999), False) + + t['TCP custom test static Base64'] = (self._test_sk, (tcp, ext_ip, 1000, 'whatever', '\x0fL\x0aR\x0e'), True) + t['TCP custom test static string'] = (self._test_sk, (tcp, ext_ip, 1001, 'whatever', 'static string TCP response'), True) + t['TCP custom test static file'] = (self._test_sk, (tcp, ext_ip, 1002, 'whatever', 'sample TCP raw file response'), True) + whatever = 'whatever' # Ensures matching test/expected for TCP dynamic + t['TCP custom test dynamic'] = (self._test_sk, (tcp, ext_ip, 1003, whatever, ''.join([chr(ord(c)+1) for c in whatever])), True) + + t['UDP external IP @ bound'] = (self._test_sk, (udp, ext_ip, 1337), True) + t['UDP external IP @ unbound'] = (self._test_sk, (udp, ext_ip, 9999), True) + t['UDP arbitrary @ bound'] = (self._test_sk, (udp, arbitrary, 1337), True) + t['UDP arbitrary @ unbound'] = (self._test_sk, (udp, arbitrary, 9999), True) + t['UDP domainname @ bound'] = (self._test_sk, (udp, domain_dne, 1337), True) + t['UDP domainname @ unbound'] = (self._test_sk, (udp, domain_dne, 9999), True) + if self.settings.singlehost: + t['UDP localhost @ bound'] = (self._test_sk, (udp, localhost, 1337), True) + t['UDP localhost @ unbound'] = (self._test_sk, (udp, localhost, 9999), False) + + t['UDP custom test static Base64'] = (self._test_sk, (udp, ext_ip, 1000, 'whatever', '\x0fL\x0aR\x0e'), True) + whatever = 'whatever2' # Ensures matching test/expected for UDP dynamic + t['UDP custom test dynamic'] = (self._test_sk, (udp, ext_ip, 1003, whatever, ''.join([chr(ord(c)+1) for c in whatever])), True) + + t['ICMP external IP'] = (self._test_icmp, (ext_ip,), True) + t['ICMP arbitrary host'] = (self._test_icmp, (arbitrary,), True) + t['ICMP domainname'] = (self._test_icmp, (domain_dne,), True) + + t['DNS listener test'] = (self._test_ns, (domain_dne, dns_expected), True) + t['HTTP listener test'] = (self._test_http, (arbitrary,), True) + # Enable HTTPS when we have either added Server Name Indication and Dynamic CA or have modified `_test_http` to + # Ignore certificate issues. Here is the error that arises otherwise. + # Starting new HTTPS connection (1): 8.8.8.8 + # Test HTTP listener test with SSL: Uncaught exception of type : [Errno 1] _ssl.c:510: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed + # Starting new HTTPS connection (1): 8.8.8.8 + # Test HTTP listener test with SSL: Uncaught exception of type : [Errno 1] _ssl.c:510: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed + # [!!!] FAILED: HTTP listener test with SSL + # t['HTTP listener test with SSL'] = (self._test_http, (arbitrary, None, 'https'), True) + t['HTTP custom test by URI'] = (self._test_http, (arbitrary, None, None, '/test.txt', 'Wraps this'), True) + t['HTTP custom test by hostname'] = (self._test_http, ('some.random.c2.com', None, None, None, 'success'), True) + t['HTTP custom test by both URI and hostname'] = (self._test_http, ('both_host.com', None, None, '/and_uri.txt', 'Ahoy'), True) + t['HTTP custom test by both URI and hostname wrong URI'] = (self._test_http, ('both_host.com', None, None, '/not_uri.txt', 'Ahoy'), False) + t['HTTP custom test by both URI and hostname wrong hostname'] = (self._test_http, ('non_host.com', None, None, '/and_uri.txt', 'Ahoy'), False) + t['HTTP custom test by ListenerType'] = (self._test_http, ('other.c2.com', 81, None, '/whatever.html', 'success'), True) + t['HTTP custom test by ListenerType host port negative match'] = (self._test_http, ('other.c2.com', 80, None, '/whatever.html', 'success'), False) + t['FTP listener test'] = (self._test_ftp, (arbitrary,), True) + t['POP3 listener test'] = (self._test_pop, (arbitrary, 110), True) + t['SMTP listener test'] = (self._test_smtp, (sender, recipient, smtpmsg, arbitrary), True) + + # Does not work, SSL error + t['SMTP SSL listener test'] = (self._test_smtp_ssl, (sender, recipient, smtpmsg, arbitrary), True) + + # Works on Linux, not on Windows + t['IRC listener test'] = (self._test_irc, (arbitrary,), True) + + t['Proxy listener HTTP test'] = (self._test_http, (arbitrary, no_service), True) + t['Proxy listener HTTP hidden test'] = (self._test_http, (arbitrary, hidden_tcp), True) + + t['TCP blacklisted host @ unbound'] = (self._test_sk, (tcp, blacklistedhost, 9999), False) + t['TCP arbitrary @ blacklisted unbound'] = (self._test_sk, (tcp, arbitrary, blacklistedtcp), False) + t['UDP arbitrary @ blacklisted unbound'] = (self._test_sk, (udp, arbitrary, blacklistedudp), False) + + if self.settings.singlehost: + t['Listener process blacklist'] = (self._test_http, (arbitrary, self.settings.listener_proc_black), False) + t['Listener process whitelist'] = (self._test_http, (arbitrary, self.settings.listener_proc_white), True) + t['Listener host blacklist'] = (self._test_http, (arbitrary, self.settings.listener_host_black), True) + t['Listener host whitelist'] = (self._test_http, (arbitrary, self.settings.listener_host_black), True) + + return self._testGeneric('General', config, t, matchspec) + + def makeConfig(self, singlehostmode=True, proxied=True, redirectall=True): + template = self.settings.configtemplate + return FakeNetConfig(template, singlehostmode, proxied, redirectall) + + def writeConfig(self, config): + logger.info('Writing config to %s' % (self.settings.configpath)) + config.write(self.settings.configpath) + for filename in self.settings.ancillary_files: + path = os.path.join(self.settings.startingpath, filename) + dest = os.path.join(self.settings.ancillary_files_dest, filename) + shutil.copyfile(path, dest) + +class FakeNetConfig: + """Convenience class to read/modify/rewrite a configuration template.""" + + def __init__(self, path, singlehostmode=True, proxied=True, redirectall=True): + self.rawconfig = ConfigParser.RawConfigParser() + self.rawconfig.read(path) + + if singlehostmode: + self.singleHostMode() + else: + self.multiHostMode() + + if not proxied: self.noProxy() + + self.setRedirectAll(redirectall) + + def blacklistProcess(self, process): self.rawconfig.set('Diverter', 'ProcessBlacklist', process) + def whitelistProcess(self, process): self.rawconfig.set('Diverter', 'ProcessWhitelist', process) + + def setRedirectAll(self, enabled): + if enabled: + self.rawconfig.set('Diverter', 'RedirectAllTraffic', 'Yes') + else: + self.rawconfig.set('Diverter', 'RedirectAllTraffic', 'No') + + def singleHostMode(self): self.rawconfig.set('Diverter', 'NetworkMode', 'SingleHost') + def multiHostMode(self): self.rawconfig.set('Diverter', 'NetworkMode', 'MultiHost') + + def noProxy(self): + self.rawconfig.remove_section('ProxyTCPListener') + self.rawconfig.remove_section('ProxyUDPListener') + self.rawconfig.set('Diverter', 'DefaultTCPListener', 'RawTCPListener') + self.rawconfig.set('Diverter', 'DefaultUDPListener', 'RawUDPListener') + + def write(self, path): + with open(path, 'w') as f: + return self.rawconfig.write(f) + +class FakeNetTestSettings: + """Test constants/literals, some of which may vary per OS, etc.""" + + def __init__(self, startingpath, singlehost=True): + # Where am I? Who are you? + self.platform_name = platform.system() + self.windows = (self.platform_name == 'Windows') + self.linux = (self.platform_name.lower().startswith('linux')) + + # Test parameters + self.singlehost = singlehost + self.startingpath = startingpath + self.configtemplate = os.path.join(startingpath, 'template.ini') + + self.ancillary_files_dest = self.genPath('%TEMP%', '/tmp/') + self.ancillary_files = [ + 'custom_responses.ini', + 'CustomProviderExample.py', + 'sample_raw_response.txt', + 'sample_raw_tcp_response.txt', + ] + + # Paths + self.configpath = self.genPath('%TEMP%\\fakenet.ini', '/tmp/fakenet.ini') + self.stopflag = self.genPath('%TEMP%\\stop_fakenet', '/tmp/stop_fakenet') + self.logpath = self.genPath('%TEMP%\\fakenet.log', '/tmp/fakenet.log') + self.fakenet = self.genPath('fakenet', 'python fakenet.py') + self.fndir = self.genPath('.', '$HOME/files/src/flare-fakenet-ng/fakenet') + + # For process blacklisting + self.pythonname = os.path.basename(sys.executable) + + # Various + self.ext_ip = get_external_ip() + self.arbitrary = '8.8.8.8' + self.blacklistedhost = '6.6.6.6' + self.blacklistedtcp = 139 + self.blacklistedudp = 67 + self.hidden_tcp = 12345 + self.no_service = 10 + self.listener_proc_black = 8080 # HTTP listener with process blacklist + self.listener_proc_white = 8081 # HTTP listener with process whitelists + self.listener_host_black = 8082 # HTTP listener with host blacklist + self.listener_host_white = 8083 # HTTP listener with host whitelists + self.localhost = '127.0.0.1' + self.dns_expected = '192.0.2.123' + self.domain_dne = 'does-not-exist-amirite.fireeye.com' + self.sender = 'from-fakenet@example.org' + self.recipient = 'to-fakenet@example.org' + self.smtpmsg = 'FakeNet-NG SMTP test email' + + # Behaviors + self.sleep_after_start = 4 + self.sleep_before_stop = 1 + + def genPath(self, winpath, unixypath): + if self.windows: + return os.path.expandvars(winpath) + else: + return os.path.expandvars(unixypath) + + def genFakenetCmd(self): + return ('%s -f %s -n -l %s -c %s' % + (self.fakenet, self.stopflag, self.logpath, self.configpath)) + +def is_ip(s): + pat = '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + return bool(re.match(pat, s)) + +def main(): + if not is_admin(): + logger.error('Not an admin, exiting...') + sys.exit(1) + + if len(sys.argv) < 2: + logger.error('Usage: test.py [matchspec1 [matchspec2 [...] ] ]') + logger.error('') + logger.error('Valid where:') + logger.error(' here') + logger.error(' Any dot-decimal IP address') + logger.error('') + logger.error('Each match specification is a regular expression that') + logger.error('will be compared against test names, and any matches') + logger.error('will be included. Because regular expression negative') + logger.error('matching is complicated to use, you can just prefix') + logger.error('a match specification with a minus sign to indicate') + logger.error('that you would like to include only tests that do NOT') + logger.error('match the expression.') + sys.exit(1) + + # Validate where + where = sys.argv[1] + + singlehost = (where.lower() == 'here') + + if not singlehost and not is_ip(where): + logger.error('Invalid where: %s' % (where)) + sys.exit(1) + + # Will execute only tests matching *match_spec if specified + match_spec = sys.argv[2:] + + if len(match_spec): + logger.info('Only running tests that match the following ' + + 'specifications:') + for spec in match_spec: + logger.info(' %s' % (spec)) + + # Doit + startingpath = os.getcwd() + settings = FakeNetTestSettings(startingpath, singlehost) + if not singlehost: # was an IP, so record it + settings.ext_ip = where + tester = FakeNetTester(settings) + logger.info('Running with privileges on %s' % (settings.platform_name)) + tester.doTests(match_spec) + +if __name__ == '__main__': + main()