From 58b9c9927986a83f4e6d6fd8ed0b20af77e54bae Mon Sep 17 00:00:00 2001 From: Tina Johnson Date: Thu, 21 Nov 2024 15:33:41 +0000 Subject: [PATCH] Modify logging in DNS listener, Diverter for blacklisted processes (#192) * Fix documentation * Minor code fix * Modify logging in DNS listener, Diverter for blacklisted processes * Code cleanup * Get config from binary location for Pyinstaller bundles * Update version and add logs * Fix text length in CHANGELOG * Update documentation --- CHANGELOG.txt | 7 ++++ README.md | 17 ++++++-- docs/developing.md | 6 +-- fakenet/diverters/diverterbase.py | 65 +++++++++++++++++++++++++++--- fakenet/fakenet.py | 4 +- fakenet/listeners/DNSListener.py | 47 ++++++++++++++++----- fakenet/listeners/ListenerBase.py | 2 +- fakenet/listeners/ProxyListener.py | 2 +- setup.py | 2 +- 9 files changed, 125 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 216900c..bb3415c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +Version 3.3 +----------- +* Hide logging in DNS listener and Diverter for blacklisted processes + when not in verbose mode +* Use binary location instead of current directory when getting config + files in Pyinstaller bundles + Version 3.2 ----------- * Use .1 for default gateway instead of .254 because this is the default Virtual diff --git a/README.md b/README.md index e083ea8..53c3905 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ D O C U M E N T A T I O N -FakeNet-NG 3.2 is a next generation dynamic network analysis tool for malware +FakeNet-NG 3.3 is a next generation dynamic network analysis tool for malware analysts and penetration testers. It is open source and designed for the latest versions of Windows (and Linux, for certain modes of operation). FakeNet-NG is based on the excellent Fakenet tool developed by Andrew Honig and Michael @@ -77,18 +77,18 @@ Finally if you would like to avoid installing FakeNet-NG and just want to run it as-is (e.g. for development), then you would need to obtain the source code and install dependencies as follows: -1) Install 64-bit or 32-bit Python 3.7.x for the 64-bit or 32-bit versions +1) Install 64-bit or 32-bit Python 3.10.11 for the 64-bit or 32-bit versions of Windows respectively. 2) Install Python dependencies: - pip install pydivert dnslib dpkt pyopenssl pyftpdlib netifaces + pip install pydivert dnslib dpkt pyopenssl pyftpdlib netifaces jinja2 *NOTE*: pydivert will also download and install WinDivert library and driver in the `%PYTHONHOME%\DLLs` directory. FakeNet-NG bundles those files so they are not necessary for normal use. -2b) Optionally, you can install the following module used for testing: + Optionally, you can install the following module used for testing: pip install requests @@ -734,6 +734,15 @@ plugins and extend existing functionality. For details, see Known Issues ============ +[WinError 87] The parameter is incorrect +---------------------------------------- +As of this wriring, the default buffer size in pydivert is 1500. If FakeNet-NG +encounters a packet larger than the default buffer size, you may observe this error. +A workaround is to specify the desired buffer size in self.handle.recv(bufsize=) +in fakenet/diverters/windows. +See [here](https://github.com/ffalcinelli/pydivert/issues/42#issuecomment-495036124) + + Does not work on VMWare with host-only mode enabled --------------------------------------------------- diff --git a/docs/developing.md b/docs/developing.md index 47a01b8..a85d58b 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -181,9 +181,9 @@ utilities (i.e. `pip`). Use an administrative command prompt where applicable for installing Python modules for all users. Pre-requisites: -* Python 2.7 x86 with `pip` -* Visual C++ for Python 2.7 development, available at: - +* Python 3.10.11 x86 with `pip` +* Visual C++ for Python development, available at: + Before installing `pyinstaller`, you may wish to take the following steps to prevent the error `ImportError: No module named PyInstaller`: diff --git a/fakenet/diverters/diverterbase.py b/fakenet/diverters/diverterbase.py index 7ebe8bd..c826fc1 100644 --- a/fakenet/diverters/diverterbase.py +++ b/fakenet/diverters/diverterbase.py @@ -1054,6 +1054,8 @@ def parse_diverter_config(self): self.getconfigval('processblacklist').split(',')] self.logger.debug('Blacklisted processes: %s', ', '.join( [str(p) for p in self.blacklist_processes])) + if self.logger.level == logging.INFO: + self.logger.info('Hiding logs from blacklisted processes') # Only redirect whitelisted processes if self.is_configured('processwhitelist'): @@ -1202,7 +1204,18 @@ def handle_pkt(self, pkt, callbacks3, callbacks4): pc = PidCommDest(pid, comm, pkt.proto, pkt.dst_ip0, pkt.dport0) if pc.isDistinct(self.last_conn, self.ip_addrs[pkt.ipver]): self.last_conn = pc - self.logger.info('%s' % (str(pc))) + # As a user may not wish to see any logs from a blacklisted + # process, messages are logged with level DEBUG. Executing + # FakeNet in the verbose mode will print these logs + is_process_blacklisted, _, _ = self.isProcessBlackListed( + pkt.proto, + process_name=comm, + dport=pkt.dport0 + ) + if is_process_blacklisted: + self.logger.debug('%s' % (str(pc))) + else: + self.logger.info('%s' % (str(pc))) # 2: Call layer 3 (network) callbacks for cb in callbacks3: @@ -1825,9 +1838,8 @@ def logNbi(self, sport, nbi, proto, application_layer_proto, is_ssl_encrypted): """Collects the NBIs from all listeners into a dictionary. - All listeners (currently only HTTPListener) use this - method to notify the diverter about any NBI captured - within their scope. + All listeners use this method to notify the diverter about any NBI + captured within their scope. Args: sport: int port bound by listener @@ -1956,7 +1968,7 @@ def generate_html_report(self): """ if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): # Inside a Pyinstaller bundle - fakenet_dir_path = os.getcwd() + fakenet_dir_path = os.path.dirname(sys.executable) else: fakenet_dir_path = os.fspath(Path(__file__).parents[1]) @@ -1972,7 +1984,44 @@ def generate_html_report(self): output_file.write(template.render(nbis=self.nbis)) self.logger.info(f"Generated new HTML report: {output_filename}") - + + def isProcessBlackListed(self, proto, sport=None, process_name=None, dport=None): + """Checks if a process is blacklisted. + Expected arguments are either: + - process_name and dport, or + - sport + """ + pid = None + + if self.single_host_mode and proto is not None: + if process_name is None or dport is None: + if sport is None: + return False, process_name, pid + + orig_sport = self.proxy_sport_to_orig_sport_map.get((proto, sport), sport) + session = self.sessions.get(orig_sport) + if session: + pid = session.pid + process_name = session.comm + dport = session.dport0 + else: + return False, process_name, pid + + # Check process blacklist + if process_name in self.blacklist_processes: + self.pdebug(DIGN, ('Ignoring %s packet from process %s ' + + 'in the process blacklist.') % (proto, + process_name)) + return True, process_name, pid + + # Check per-listener blacklisted process list + if self.listener_ports.isProcessBlackListHit( + proto, dport, process_name): + self.pdebug(DIGN, ('Ignoring %s request packet from ' + + 'process %s in the listener process ' + + 'blacklist.') % (proto, process_name)) + return True, process_name, pid + return False, process_name, pid class DiverterListenerCallbacks(): @@ -2011,3 +2060,7 @@ def mapProxySportToOrigSport(self, proto, orig_sport, proxy_sport, self.__diverter.mapProxySportToOrigSport(proto, orig_sport, proxy_sport, is_ssl_encrypted) + def isProcessBlackListed(self, proto, sport): + """Check if the process is blacklisted. + """ + return self.__diverter.isProcessBlackListed(proto, sport=sport) diff --git a/fakenet/fakenet.py b/fakenet/fakenet.py index 850d1c8..e2ac616 100644 --- a/fakenet/fakenet.py +++ b/fakenet/fakenet.py @@ -64,7 +64,7 @@ def __init__(self, logging_level = logging.INFO): def parse_config(self, config_filename): # Handling Pyinstaller bundle scenario: https://pyinstaller.org/en/stable/runtime-information.html if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - dir_path = os.getcwd() + dir_path = os.path.dirname(sys.executable) else: dir_path = os.path.dirname(__file__) @@ -349,7 +349,7 @@ def main(): | | / ____ \| . \| |____| |\ | |____ | | | |\ | |__| | |_|/_/ \_\_|\_\______|_| \_|______| |_| |_| \_|\_____| - Version 3.2 + Version 3.3 _____________________________________________________________ Developed by FLARE Team Copyright (C) 2016-2024 Mandiant, Inc. All rights reserved. diff --git a/fakenet/listeners/DNSListener.py b/fakenet/listeners/DNSListener.py index 745e0d2..1b510f7 100644 --- a/fakenet/listeners/DNSListener.py +++ b/fakenet/listeners/DNSListener.py @@ -49,6 +49,8 @@ def __init__( self.logger.debug('Initialized with config:') for key, value in config.items(): self.logger.debug(' %10s: %s', key, value) + if self.logger.level == logging.INFO: + self.logger.info('Hiding logs from blacklisted processes') def start(self): @@ -81,18 +83,38 @@ def acceptDiverterListenerCallbacks(self, diverterListenerCallbacks): class DNSHandler(): + def log_message(self, log_level, is_process_blacklisted, message, *args): + """The primary objective of this method is to control the log messages + generated for requests from blacklisted processes. + + In a case where the DNS server is same as the local machine, the DNS + requests from a blacklisted process will reach the DNS listener (which + listens on port 53 locally) nevertheless. As a user may not wish to see + logs from a blacklisted process, messages are logged with level DEBUG. + Executing FakeNet in the verbose mode will print these logs. + """ + if is_process_blacklisted: + self.server.logger.log(logging.DEBUG, message, *args) + else: + self.server.logger.log(log_level, message, *args) - def parse(self,data): + def parse(self, data): response = "" - + proto = 'TCP' if self.server.socket_type == socket.SOCK_STREAM else 'UDP' + is_process_blacklisted, process_name, pid = self.server \ + .diverterListenerCallbacks \ + .isProcessBlackListed( + proto, + sport=self.client_address[1]) + try: # Parse data as DNS d = DNSRecord.parse(data) except Exception as e: - self.server.logger.error('Error: Invalid DNS Request') + self.log_message(logging.ERROR, is_process_blacklisted, 'Error: Invalid DNS Request') for line in hexdump_table(data): - self.server.logger.warning(INDENT + line) + self.log_message(logging.WARNING, is_process_blacklisted, INDENT + line) else: # Only Process DNS Queries @@ -110,7 +132,14 @@ def parse(self,data): self.qname = qname self.qtype = qtype - self.server.logger.info('Received %s request for domain \'%s\'.', qtype, qname) + if process_name is None or pid is None: + self.log_message(logging.INFO, is_process_blacklisted, + 'Received %s request for domain \'%s\'.', + qtype, qname) + else: + self.log_message(logging.INFO, is_process_blacklisted, + 'Received %s request for domain \'%s\' from %s (%s)', + qtype, qname, process_name, pid) # Create a custom response to the query response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1), q=d.q) @@ -165,11 +194,11 @@ def parse(self,data): fake_record = socket.gethostbyname(socket.gethostname()) if self.server.nxdomains > 0: - self.server.logger.info('Ignoring query. NXDomains: %d', + self.log_message(logging.INFO, is_process_blacklisted, 'Ignoring query. NXDomains: %d', self.server.nxdomains) self.server.nxdomains -= 1 else: - self.server.logger.debug('Responding with \'%s\'', + self.log_message(logging.DEBUG, is_process_blacklisted, 'Responding with \'%s\'', fake_record) response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](fake_record))) @@ -180,7 +209,7 @@ def parse(self,data): # dnslib doesn't like trailing dots if fake_record[-1] == ".": fake_record = fake_record[:-1] - self.server.logger.debug('Responding with \'%s\'', + self.log_message(logging.DEBUG, is_process_blacklisted, 'Responding with \'%s\'', fake_record) response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](fake_record))) @@ -189,7 +218,7 @@ def parse(self,data): fake_record = self.server.config.get('responsetxt', 'FAKENET') - self.server.logger.debug('Responding with \'%s\'', + self.log_message(logging.DEBUG, is_process_blacklisted, 'Responding with \'%s\'', fake_record) response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](fake_record))) diff --git a/fakenet/listeners/ListenerBase.py b/fakenet/listeners/ListenerBase.py index 19dd22e..f0db49a 100644 --- a/fakenet/listeners/ListenerBase.py +++ b/fakenet/listeners/ListenerBase.py @@ -34,7 +34,7 @@ def abs_config_path(path): return abspath if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - relpath = os.path.join(os.getcwd(), path) + relpath = os.path.join(os.path.dirname(sys.executable), path) else: # Try to locate the location relative to application path diff --git a/fakenet/listeners/ProxyListener.py b/fakenet/listeners/ProxyListener.py index 65bb15c..07f7f2d 100644 --- a/fakenet/listeners/ProxyListener.py +++ b/fakenet/listeners/ProxyListener.py @@ -141,7 +141,7 @@ def run(self): self.listener_q.put(data) else: self.sock.close() - exit(1) + sys.exit(1) except Exception as e: self.logger.debug('Listener socket exception %s' % e.message) diff --git a/setup.py b/setup.py index 3641d57..47ba0ce 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name='FakeNet NG', - version='3.2', + version='3.3', description="", long_description="", author="Mandiant FLARE Team with credit to Peter Kacherginsky as the original developer",