Skip to content

Commit

Permalink
Support for SNI and dynamic certificate
Browse files Browse the repository at this point in the history
- Dynamically generate a certificate based on client request using
Server Name Indication (SNI).
- Sign the new certificate with either a static CA certificate, or
with a newly generated CA.
- Add config options to specify a path to static CA certificate.

This code was written by Nhan Huynh while he was employed at
FireEye/Mandiant/Google and Google holds the copyright.

Co-authored-by: Michael Bailey <[email protected]>
Co-authored-by: Tina Johnson <[email protected]>
  • Loading branch information
3 people authored and Ana06 committed Dec 12, 2024
1 parent 6e3e72b commit c6f74ab
Show file tree
Hide file tree
Showing 16 changed files with 1,383 additions and 1,136 deletions.
20 changes: 14 additions & 6 deletions fakenet/configs/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ NetworkMode: Auto
# DebugLevel: specify fine-grained debug print flags to enable. Enabling all
# logging when verbose mode is selected results in overwhelming output, hence
# this setting. Valid values (comma-separated) are:
#
#
# GENPKT Generic packet information
# GENPKTV Packet analysis, displays IP, TCP, UDP fields, very wide output
# CB Diverter packet handler callback start/finish logging
Expand Down Expand Up @@ -88,7 +88,7 @@ FixDNS: Yes
# ephemeral change.
ModifyLocalDNS: Yes

# Enable 'StopDNSService' to stop Windows DNS client to see the actual
# Enable 'StopDNSService' to stop Windows DNS client to see the actual
# processes resolving domains. This is a no-op on Linux, until such time as DNS
# caching is observed to interfere with finding the pid associated with a DNS
# request.
Expand All @@ -99,16 +99,16 @@ StopDNSService: Yes
# 'DefaultUDPListener' will handle TCP and UDP traffic going to unspecified ports.
#
# NOTE: Setting default UDP listener will intercept all DNS traffic unless you
# enable a dedicated UDP port 53 DNS listener or add UDP port 53 to the
# enable a dedicated UDP port 53 DNS listener or add UDP port 53 to the
# 'BlackListPortsUDP' below so that system's default DNS server is used instead.

RedirectAllTraffic: Yes
DefaultTCPListener: ProxyTCPListener
DefaultUDPListener: ProxyUDPListener

# Specify TCP and UDP ports to ignore when diverting packets.
# Specify TCP and UDP ports to ignore when diverting packets.
# For example, you may want to avoid diverting UDP port 53 (DNS) traffic
# when trying to intercept a specific process while allowing the rest to
# when trying to intercept a specific process while allowing the rest to
# function normally
#
# NOTE: This setting is only honored when 'RedirectAllTraffic' is enabled.
Expand All @@ -131,7 +131,7 @@ BlackListPortsUDP: 67, 68, 137, 138, 443, 1900, 5355
# Listener Configuration
#
# Listener configuration consists of generic settings used by the diverter which
# are the same for all listeners and listener specific settings.
# are the same for all listeners and listener specific settings.
#
# NOTE: Listener section names will be used for logging.
#
Expand Down Expand Up @@ -199,6 +199,11 @@ BlackListPortsUDP: 67, 68, 137, 138, 443, 1900, 5355
# hostname string, !hostname to insert the actual hostname
# of the system, or !random to generate a random hostname
# between 1 and 15 characters (inclusive).
# * Static_CA - Set FakeNet to use user provided CA certificate to sign generated certificates.
# * CA_Cert - CA certificate in PEM format to be used when Static_CA config is set. Manually
# add this certificate to Windows trust store before executing FakeNet.
# * CA_Key - CA private key in PEM format to be used when Static_CA config is set.


[ProxyTCPListener]
Enabled: True
Expand All @@ -207,6 +212,9 @@ Listener: ProxyListener
Port: 38926
Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener
Hidden: False
Static_CA: No
CA_Cert: configs/fakenet_ca.crt
CA_Key: configs/fakenet_ca.key

[ProxyUDPListener]
Enabled: True
Expand Down
19 changes: 19 additions & 0 deletions fakenet/configs/fakenet_ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDHzCCAgegAwIBAgIUdwSkHdOM2mMDw094Kha+9Z9/w60wDQYJKoZIhvcNAQEL
BQAwHzELMAkGA1UEBhMCVVMxEDAOBgNVBAMMB2Zha2VuZXQwHhcNMjMxMjE0MDAx
NDUxWhcNMjQwMTEzMDAxNDUxWjAfMQswCQYDVQQGEwJVUzEQMA4GA1UEAwwHZmFr
ZW5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM7pGzS8bX8M3SSQ
mk79puvqGBHwWVpDK82T44N/8mXHJ1R/7jMDq2wpkSjliiAE0mxPpkzMbr9mkeP/
/31GKAszbJYnurrxxYbLyOdRst2VqoXkWTia61lrsRIGcjwzKe2zyMCcuiyRTLcP
BmYd/ie5AzyHxitlS49cub+QkIODUAKTiZT3mPu6Yw2XvYkg+up69NzC0a/XexUv
PvgbBizquKj/YzMSp5X7ieYGv0xHf8Dhf3mqh9oLk35X/qV3LqdnVPjweCR8X9ze
yhfBbDr1VoBnOe2Nb5hlU9MB//A0hgDYj5TrHa4JrbNkv3lYMd4uv/CBW6o18Ba+
/zjEvyECAwEAAaNTMFEwHQYDVR0OBBYEFD3wRGMPQdWtBCKRy5c9N5YWnki2MB8G
A1UdIwQYMBaAFD3wRGMPQdWtBCKRy5c9N5YWnki2MA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBAGT5rmafrlv8VIXAgc0iazNd7A6rT7xNLkuF2JGK
7NV3yvsWM6SA+DlG7y70om+eKjd+qxzinxnSt6uFJhcdCqot1LU1u3OZDifTTJmk
31yEYp/+A93qjwe1Ag2rsVcztRl88KtsKrKNohv24iiWfIVDnHo0joerXoaGQwo6
zXl4GVJBEEAhf2GQRgyXcoWkSrsq8UKtVV9dI5QgIS6vZ65oNQEeoAXH56ihFUBX
hS+4Ko2FfUtxbfbw7tpDaNtqhAzJ+LE4RoDUepyCDXPha0Wb4giGOd5EEubYrFKi
DOdAMiiQT1WLK3/UnMlCOV4lne+g9JwBCXL8C0F0W1fFZY8=
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions fakenet/configs/fakenet_ca.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDO6Rs0vG1/DN0k
kJpO/abr6hgR8FlaQyvNk+ODf/JlxydUf+4zA6tsKZEo5YogBNJsT6ZMzG6/ZpHj
//99RigLM2yWJ7q68cWGy8jnUbLdlaqF5Fk4mutZa7ESBnI8Mynts8jAnLoskUy3
DwZmHf4nuQM8h8YrZUuPXLm/kJCDg1ACk4mU95j7umMNl72JIPrqevTcwtGv13sV
Lz74GwYs6rio/2MzEqeV+4nmBr9MR3/A4X95qofaC5N+V/6ldy6nZ1T48HgkfF/c
3soXwWw69VaAZzntjW+YZVPTAf/wNIYA2I+U6x2uCa2zZL95WDHeLr/wgVuqNfAW
vv84xL8hAgMBAAECggEAAyp6KDPcHCjH5XU4hXGPeGYvkhldQtxqsw2Rr7xpVWrl
dw1q8/dR2kVGVFSlBWe7tIPk0Ew8fD14+xtXG4xhmECAoElTHY+b7VgxkVJPem9h
RD4XOLsP4ba4rlNes+DiUCHKbyHTOox1RXytQZxNbgbVr7tOVNU1nf0rVmzt9Zbw
uPltpCcL+yvnaWmBUgdCLIhIT/HDrp4+uZP+zW7pVm/KMQL0I9OMnm6dCimP88pU
meepDRdxHkHVb347jx0+nWSH2V63wxH6WR8lAb4oAXeQXUgS8Q0lXh70EW/x0Ut3
neB/mfuaXLVrBcBTbhPTU7PKd/Sc+dkkqhEpNRR3dQKBgQDyLNPyWEJVXwQ6iOom
xxyqm61DZw8IpBYVmfYrOisTUWVC+oNlpsNPzWsgwmWzpcJ8s6ykupWFcWZeJv2h
XPlq+Ai6Ky0v+yESiUtNb+lKuXGvHqTpTxvPgL/20ZMfRIkWs9YBLA+aTh5RC8Vs
i12fRW9JTZTKgyf7PEkj+DjCCwKBgQDauOszqtFs6d36AMVJJ7OlmZnEWaELv9bW
ns7kdeBgr35gzGbDOkyQGBII5SCwpgWbXyxT39T9xuNoWCWXSZdHDco/UlAG47qI
Pq/KmtbrKxvg6yKsPIDV+cexuBrLIvzsaz8qPbfE/W7nxf+FtvDhvnGyqoiYd3DS
XSD5HcwLAwKBgQDdQpG+mF66qx4s8Myl80NAqQ1LSMyWg3xd7hXYdsPGWZaf9Eu6
wvstXSvkeVf8I5Um4+33bzWO/wWdPhh6pnyG++jVVv9pGBOmYOP48yd9iyLP8bqQ
IyPwmNxKgD3f0nlB0brTxVLYE0llmNCelFJMY18C5SvtPpl31COrBm2s8wKBgG8D
zN28pe+SBIkQOxKWhChZfiKbG5LLHFBy6rAq5GguqwaWuNH+lT3N+dlp8t22ZsIl
3Gn2AjWM7X/Yvbu8LnxyE2Vwcg4NKHBe4PsE/HEAwHW44zBoxTvWO/WIbJEOgTG+
faEDEnN57wDVDozf/gOWlj8JL6uzdCBSBJps9VPhAoGAWQUkdpxRxEARhL7wYR7g
EidDWKnULiULjlrP7VPMJ5hrZf5PmWyZLWW3SkEUhcf25CnJoUoq1OOB1GjL9lBL
0+tXalLnA/mRxO5ILzwJivHyJKnljuPKyXmpKt8H4KRUXV3Uk2may58Jwn8InuRE
aW956i0O1tgrDOj3tSAT8KI=
-----END PRIVATE KEY-----
4 changes: 2 additions & 2 deletions fakenet/diverters/diverterbase.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved.
# Copyright (C) 2016-2024 Mandiant, Inc. All rights reserved.

import os
import abc
Expand Down Expand Up @@ -958,7 +958,7 @@ def _build_cmd(self, tmpl, pid, comm, src_ip, sport, dst_ip, dport):
except KeyError as e:
self.logger.error(('Failed to build ExecuteCmd for port %d due ' +
'to erroneous format key: %s') %
(dport, e.message))
(dport, e))

return cmd

Expand Down
28 changes: 14 additions & 14 deletions fakenet/diverters/linutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ def start(self, timeout_sec=0.5):
self._bound = True
except OSError as e:
self.logger.error('Failed to start queue for %s: %s' %
(str(self), e.message))
(str(self), e))
except RuntimeWarning as e:
self.logger.error('Failed to start queue for %s: %s' %
(str(self), e.message))
(str(self), e))

if not self._bound:
return False
Expand All @@ -166,7 +166,7 @@ def start(self, timeout_sec=0.5):
self._thread.start()
self._started = True
except RuntimeError as e:
self.logger.error('Failed to start queue thread: %s' % (e.message))
self.logger.error('Failed to start queue thread: %s' % (e))

return self._started

Expand Down Expand Up @@ -251,7 +251,7 @@ def parse(self, multi=False, max_col=None):
retval = cb_retval
break
except IOError as e:
self.logger.error('Failed accessing %s: %s' % (path, e.message))
self.logger.error('Failed accessing %s: %s' % (path, e))
# All or nothing
retval = [] if multi else None

Expand Down Expand Up @@ -313,7 +313,7 @@ def linux_capture_iptables(self):
ret = p.wait()
except OSError as e:
self.logger.error('Error executing iptables-save: %s' %
(e.message))
(e))

return ret

Expand All @@ -328,7 +328,7 @@ def linux_restore_iptables(self):
ret = p.wait()
except OSError as e:
self.logger.error('Error executing iptables-restore: %s' %
(e.message))
(e))

return ret

Expand All @@ -351,7 +351,7 @@ def linux_flush_iptables(self):
self.logger.error('Received return code %d from %s' +
(ret, cmd))
except OSError as e:
self.logger.error('Error executing %s: %s' % (cmd, e.message))
self.logger.error('Error executing %s: %s' % (cmd, e))

return rets

Expand Down Expand Up @@ -388,7 +388,7 @@ def linux_get_current_nfnlq_bindings(self):
self.logger.debug(('Failed to open %s to enumerate netfilter '
'netlink queues, caller may proceed as if '
'none are in use: %s') %
(procfs_path, e.message))
(procfs_path, e))

return qnos

Expand Down Expand Up @@ -445,7 +445,7 @@ def _linux_get_ifaces(self):
ifaces.append(fields[0].strip())
except IOError as e:
self.logger.error('Failed to open %s to enumerate interfaces: %s' %
(procfs_path, e.message))
(procfs_path, e))

return ifaces

Expand All @@ -472,7 +472,7 @@ def linux_modifylocaldns_ephemeral(self):
except IOError as e:
self.logger.error(('Failed to open %s to save DNS ' +
'configuration: %s') % (resolvconf_path,
e.message))
e))

if self.old_dns:
try:
Expand All @@ -484,7 +484,7 @@ def linux_modifylocaldns_ephemeral(self):
except IOError as e:
self.logger.error(('Failed to open %s to modify DNS ' +
'configuration: %s') % (resolvconf_path,
e.message))
e))

def linux_restore_local_dns(self):
resolvconf_path = '/etc/resolv.conf'
Expand All @@ -496,7 +496,7 @@ def linux_restore_local_dns(self):
except IOError as e:
self.logger.error(('Failed to open %s to restore DNS ' +
'configuration: %s') % (resolvconf_path,
e.message))
e))

def linux_find_processes(self, names):
"""But what if a blacklisted process spawns after we call
Expand Down Expand Up @@ -618,7 +618,7 @@ def _linux_find_sock_by_endpoint_unsafe(self, ipver, proto_name, ip, port,
(line.strip()))
except IOError as e:
self.logger.error('No such protocol/IP ver (%s) or error: %s' %
(procfs_path, e.message))
(procfs_path, e))

return inode

Expand Down Expand Up @@ -701,7 +701,7 @@ def linux_get_comm_by_pid(self, pid):
comm = f.read().strip()
except IOError as e:
self.pdebug(DPROCFS, 'Failed to open %s: %s' %
(procfs_path, e.message))
(procfs_path, e))
return comm

def linux_get_pid_comm_by_endpoint(self, ipver, proto_name, ip, port):
Expand Down
2 changes: 1 addition & 1 deletion fakenet/fakenet.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def start(self):
self.logger.error("%s" % e)

else:

listener_config['networkmode'] = self.diverter_config['networkmode']
listener_provider_instance = listener_provider(
listener_config, listener_name, self.logging_level)

Expand Down
2 changes: 1 addition & 1 deletion fakenet/listeners/DNSListener.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class DNSListener(object):

def taste(self, data, dport):

confidence = 1 if dport is 53 else 0
confidence = 1 if dport == 53 else 0

try:
d = DNSRecord.parse(data)
Expand Down
54 changes: 36 additions & 18 deletions fakenet/listeners/HTTPListener.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved.
# Copyright (C) 2016-2024 Mandiant, Inc. All rights reserved.

import logging
from configparser import ConfigParser

import os
import sys
import imp
import importlib.util
import importlib.machinery

import threading
import socketserver
Expand All @@ -19,6 +20,7 @@

import time

from .ssl_utils import SSLWrapper
from . import *

MIME_FILE_RESPONSE = {
Expand Down Expand Up @@ -46,6 +48,16 @@ def qualify_file_path(filename, fallbackdir):

return path

def load_source(modname, filename):
loader = importlib.machinery.SourceFileLoader(modname, filename)
spec = importlib.util.spec_from_file_location(modname, filename, loader=loader)
module = importlib.util.module_from_spec(spec)
# The module is always executed and not cached in sys.modules.
# Uncomment the following line to cache the module.
# sys.modules[module.__name__] = module
loader.exec_module(module)
return module


class CustomResponse(object):
def __init__(self, name, conf, configroot):
Expand Down Expand Up @@ -83,7 +95,7 @@ def __init__(self, name, conf, configroot):
self.handler = None
pymod_path = qualify_file_path(conf.get('httpdynamic'), configroot)
if pymod_path:
pymod = imp.load_source('cr_' + self.name, pymod_path)
pymod = load_source('cr_' + self.name, pymod_path)
funcname = 'HandleHttp'
funcname_legacy = 'HandleRequest'
if hasattr(pymod, funcname):
Expand Down Expand Up @@ -137,6 +149,10 @@ def respond(self, req, meth, postdata=None):


class HTTPListener(object):
SSL_UTILS = os.path.join("listeners", "ssl_utils")
CA_CERT = os.path.join(SSL_UTILS, "server.pem")
CA_KEY = os.path.join(SSL_UTILS, "privkey.pem")
NOT_AFTER_DELTA_SECONDS = 300 * 24 * 60 * 60

def taste(self, data, dport):

Expand Down Expand Up @@ -168,11 +184,13 @@ def __init__(

self.logger = logging.getLogger(name)
self.logger.setLevel(logging_level)

self.config = config
self.name = name
self.local_ip = config.get('ipaddr')
self.server = None
self.port = self.config.get('port', 80)
self.sslwrapper = None

self.logger.debug('Initialized with config:')
for key, value in config.items():
Expand All @@ -185,29 +203,29 @@ def __init__(
self.logger.error('Could not locate webroot directory: %s', path)
sys.exit(1)


def start(self):
self.logger.debug('Starting...')
self.server = ThreadedHTTPServer((self.local_ip, int(self.config.get('port'))), ThreadedHTTPRequestHandler)

self.server = ThreadedHTTPServer((self.local_ip,
int(self.config.get('port'))), ThreadedHTTPRequestHandler)
self.server.logger = self.logger
self.server.config = self.config
self.server.webroot_path = self.webroot_path
self.server.extensions_map = self.extensions_map

if self.config.get('usessl') == 'Yes':
self.logger.debug('Using SSL socket.')

keyfile_path = 'listeners/ssl_utils/privkey.pem'
keyfile_path = ListenerBase.abs_config_path(keyfile_path)
if keyfile_path is None:
raise RuntimeError('Could not locate %s' % (keyfile_path))

certfile_path = 'listeners/ssl_utils/server.pem'
certfile_path = ListenerBase.abs_config_path(certfile_path)
if certfile_path is None:
raise RuntimeError('Could not locate %s' % (certfile_path))

self.server.socket = ssl.wrap_socket(self.server.socket, keyfile=keyfile_path, certfile=certfile_path, server_side=True, ciphers='RSA')
self.logger.debug("HTTP Listener starting with SSL")
config = {
'cert_dir': self.config.get('cert_dir', 'configs/temp_certs'),
'networkmode': self.config.get('networkmode', None),
'static_ca': self.config.get('static_ca', "No"),
'ca_cert': self.config.get('ca_cert'),
'ca_key': self.config.get('ca_key')
}
self.sslwrapper = SSLWrapper(config)
self.server.sslwrapper = self.sslwrapper
self.server.socket = self.server.sslwrapper.wrap_socket(
self.server.socket)

self.server.custom_responses = []
custom = self.config.get('custom')
Expand Down
Loading

0 comments on commit c6f74ab

Please sign in to comment.