-
Notifications
You must be signed in to change notification settings - Fork 364
/
Copy pathdiverterbase.py
2066 lines (1670 loc) · 80.4 KB
/
diverterbase.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (C) 2016-2023 Mandiant, Inc. All rights reserved.
import os
import abc
import sys
import time
import dpkt
import signal
import jinja2
import socket
import logging
import threading
import subprocess
from . import fnpacket
from . import fnconfig
from .debuglevels import *
from collections import namedtuple
from collections import OrderedDict
from pathlib import Path
class DivertParms(object):
"""Class to abstract all criteria possible out of the Windows and Linux
diverters.
These criteria largely derive from both the diverter state and the packet
contents. This class is sometimes passed around alongside the packet to
provide context wtihout loading down the fnpacket.PacketCtx with extraneous
concepts.
Many of these critera are only applicable if the transport layer has
been parsed and validated.
"""
def __init__(self, diverter, pkt):
self.diverter = diverter
self.pkt = pkt
@property
def is_loopback0(self):
return (self.pkt.src_ip0 == self.pkt.dst_ip0 ==
self.diverter.loopback_ip)
@property
def is_loopback(self):
return self.pkt.src_ip == self.pkt.dst_ip == self.diverter.loopback_ip
@property
def dport_hidden_listener(self):
"""Does the destination port for the packet correspond to a hidden
listener (i.e. should the packet be redirected to the proxy)?
Returns:
True if dport corresponds to hidden listener, else False
"""
return self.diverter.listener_ports.isHidden(self.pkt.proto,
self.pkt.dport)
@property
def src_local(self):
"""Is the source address one of the local IPs of this system?
Returns:
True if local source IP, else False
"""
return self.pkt.src_ip in self.diverters.ip_addrs[self.pkt.ipver]
@property
def sport_bound(self):
"""Is the source port bound by a FakeNet-NG listener?
Returns:
True if sport is bound by FakeNet-NG, else False
"""
return self.diverter.listener_ports.isListener(self.pkt.proto,
self.pkt.sport)
@property
def dport_bound(self):
"""Is the destination port bound by a FakeNet-NG listener?
Returns:
True if dport is bound by FakeNet-NG, else False
"""
return self.diverter.listener_ports.isListener(self.pkt.proto,
self.pkt.dport)
@property
def first_packet_new_session(self):
"""Is this the first datagram from this conversation?
Returns:
True if this pair of endpoints hasn't conversed before, else False
"""
# sessions.get returns (dst_ip, dport, pid, comm, dport0, proto) or
# None. We just want dst_ip and dport for comparison.
session = self.diverter.sessions.get(self.pkt.sport)
if session is None:
return True
return not ((session.dst_ip, session.dport) ==
(self.pkt.dst_ip, self.pkt.dport))
class DiverterPerOSDelegate(object, metaclass=abc.ABCMeta):
"""Delegate class for OS-specific methods that FakeNet-NG implementors must
override.
TODO: The following methods may need to be combined to ensure that there is
at least a single Ethernet interface with all valid settings (instead of,
say, several interfaces, each with only one of the components that are
needed to make the system work).
check_active_ethernet_adapters
check_ipaddresses
check_gateways (currently only a warning)
check_dns_servers (currently only a warning)
"""
@abc.abstractmethod
def check_active_ethernet_adapters(self):
"""Check that there is at least one Ethernet interface.
Returns:
True if there is at least one interface, else False
"""
pass
@abc.abstractmethod
def check_ipaddresses(self):
"""Check that there is at least one non-null IP address associated with
at least one interface.
Returns:
True if at least one IP address, else False
"""
pass
@abc.abstractmethod
def check_gateways(self):
"""Check that at least one interface has a non-NULL gateway set.
Returns:
True if at least one gateway, else False
"""
pass
@abc.abstractmethod
def fix_gateway(self):
"""Check if there is a gateway configured on any of the Ethernet
interfaces. If not, then locate a configured IP address and set a gw
automatically. This is necessary for VMware's Host-Only DHCP server
which leaves the default gateway empty.
Returns:
True if successful, else False
"""
pass
@abc.abstractmethod
def check_dns_servers(self):
"""Check that a DNS server is set.
Returns:
True if a DNS server is set, else False
"""
pass
@abc.abstractmethod
def fix_dns(self):
"""Check if there is a DNS server on any of the Ethernet interfaces. If
not, then locate configured IP address and set a DNS server
automatically.
Returns:
True if successful, else False
"""
pass
@abc.abstractmethod
def get_pid_comm(self, pkt):
"""Get the PID and process name by IP/port info.
NOTE: the term "comm" is short for "command" and comes from the Linux
term for process name within task_struct and displayed in ps.
Args:
pkt: A fnpacket.PacketCtx or derived object
Returns:
Tuple of length 2, containing:
(pid, comm)
If the pid or comm cannot be discerned, the corresponding member of
the tuple will be None.
"""
pass
@abc.abstractmethod
def getNewDestinationIp(self, src_ip):
"""Get IP to redirect to after a redirection decision has been made.
This is OS-specific due to varying empirical results on Windows and
Linux, and may be subject to change.
On Windows, and possibly other operating systems, simply redirecting
external packets to the loopback address will cause the packets not to
be routable, so it is necessary to choose an external interface IP in
some cases.
Contrarywise, the Linux FTP tests will fail if all redirections are not
routed to 127.0.0.1.
Args:
src_ip: A str of the source IP address represented in ASCII
Returns:
A str of the destination IP address represented in ASCII that the
packet should be redirected to.
"""
pass
class ListenerAlreadyBoundThere(Exception):
pass
class ListenerBlackWhiteList(Exception):
pass
class ListenerMeta(object):
"""Info about each listener.
Makes hidden listeners explicit. Organizes process and host black/white
lists and ExecuteCmd format strings.
Mutators are here for building listener metadata before adding it to the
group. Accessors are in ListenerPorts for querying the collection for
listeners and their attributes.
"""
def __init__(self, proto, port, hidden=False):
self.proto = proto
self.port = port
self.hidden = hidden
self.proc_bl = None
self.proc_wl = None
self.host_bl = None
self.host_wl = None
self.cmd_template = None
def _splitBlackWhiteList(self, configtext):
"""Return list from comma-separated config line."""
return [item.strip() for item in configtext.split(',')]
def _validateBlackWhite(self):
"""Validate that only a black or a white list of either type (host or
process) is configured.
Side-effect:
Raises ListenerBlackWhiteList if invalid
"""
msg = None
fmt = 'Cannot specify both %s blacklist and whitelist for port %d'
if self.proc_wl and self.proc_bl:
msg = fmt % ('process', self.port)
self.proc_wl = self.proc_bl = None
elif self.host_wl and self.host_bl:
msg = fmt % ('host', self.port)
self.host_wl = self.host_bl = None
if msg:
raise ListenerBlackWhiteList(msg)
def setProcessWhitelist(self, configtext):
self.proc_wl = self._splitBlackWhiteList(configtext)
self._validateBlackWhite()
def setProcessBlacklist(self, configtext):
self.proc_bl = self._splitBlackWhiteList(configtext)
self._validateBlackWhite()
def setHostWhitelist(self, configtext):
self.host_wl = self._splitBlackWhiteList(configtext)
self._validateBlackWhite()
def setHostBlacklist(self, configtext):
self.host_bl = self._splitBlackWhiteList(configtext)
self._validateBlackWhite()
def setExecuteCmd(self, configtext):
self.cmd_template = configtext
class ListenerPorts(object):
"""Collection of listeners with convenience accessors.
Previously, FakeNet-NG had several parallel dictionaries associated with
listener settings and lots of code like this:
1.) Does this dictionary have a 'TCP' key?
2.) Oh, yeah? Well, is this port in the dictionary under 'TCP'?
3.) Ah, great! Now I can ask my question. Is there an ExecuteCmd for
this port?
At a cost of having to add a bit of code and a few more comment lines, This
class takes care of the checks and turns queries like this into one-liners
like this one:
cmd = obj.getExecuteCmd('TCP', 80) # Returns None if not applicable
"""
def __init__(self):
"""Initialize dictionary of dictionaries:
protocol name => dict
portno => ListenerMeta
"""
self.protos = {}
def addListener(self, listener):
"""Add a ListenerMeta under the corresponding protocol and port."""
proto = listener.proto
port = listener.port
if not proto in self.protos:
self.protos[proto] = {}
if port in self.protos[proto]:
raise ListenerAlreadyBoundThere(
'Listener already bound to %s port %s' % (proto, port))
self.protos[proto][port] = listener
def getListenerMeta(self, proto, port):
if proto in self.protos:
return self.protos[proto].get(port)
def isListener(self, proto, port):
"""Is this port associated with a listener?"""
return bool(self.getListenerMeta(proto, port))
def isHidden(self, proto, port):
"""Is this port associated with a listener that is hidden?"""
listener = self.getListenerMeta(proto, port)
return listener.hidden if listener else False
def getPortList(self, proto):
if proto in self.protos:
return list(self.protos[proto].keys())
return []
def intersectsWithPorts(self, proto, ports):
"""Check if ports intersect with bound listener ports.
Convenience method for checking whether source or destination port are
bound to a FakeNet-NG listener.
"""
return set(ports).intersection(self.getPortList(proto))
def getExecuteCmd(self, proto, port):
"""Get the ExecuteCmd format string specified by the operator.
Args:
proto: The protocol name
port: The port number
Returns:
The format string if applicable
None, otherwise
"""
listener = self.getListenerMeta(proto, port)
if listener:
return listener.cmd_template
def _isWhiteListMiss(self, thing, whitelist):
"""Check if thing is NOT in whitelist.
Args:
thing: thing to check whitelist for
whitelist: list of entries
Returns:
True if thing is in whitelist
False otherwise, or if there is no whitelist
"""
if not whitelist:
return False
return not (thing in whitelist)
def _isBlackListHit(self, thing, blacklist):
"""Check if thing is in blacklist.
Args:
thing: thing to check blacklist for
blacklist: list of entries
Returns:
True if thing is in blacklist
False otherwise, or if there is no blacklist
"""
if not blacklist:
return False
return (thing in blacklist)
def isProcessWhiteListMiss(self, proto, port, proc):
"""Check if proc is OUTSIDE the process WHITElist for a port.
Args:
proto: The protocol name
port: The port number
proc: The process name
Returns:
False if no listener on this port
Return value of _isWhiteListMiss otherwise
"""
listener = self.getListenerMeta(proto, port)
if not listener:
return False
return self._isWhiteListMiss(proc, listener.proc_wl)
def isProcessBlackListHit(self, proto, port, proc):
"""Check if proc is IN the process BLACKlist for a port.
Args:
proto: The protocol name
port: The port number
proc: The process name
Returns:
False if no listener on this port
Return value of _isBlackListHit otherwise
"""
listener = self.getListenerMeta(proto, port)
if not listener:
return False
return self._isBlackListHit(proc, listener.proc_bl)
def isHostWhiteListMiss(self, proto, port, host):
"""Check if host is OUTSIDE the process WHITElist for a port.
Args:
proto: The protocol name
port: The port number
host: The process name
Returns:
False if no listener on this port
Return value of _isWhiteListMiss otherwise
"""
listener = self.getListenerMeta(proto, port)
if not listener:
return False
return self._isWhiteListMiss(host, listener.host_wl)
def isHostBlackListHit(self, proto, port, host):
"""Check if host is IN the process BLACKlist for a port.
Args:
proto: The protocol name
port: The port number
host: The process name
Returns:
False if no listener on this port
Return value of _isBlackListHit otherwise
"""
listener = self.getListenerMeta(proto, port)
if not listener:
return False
return self._isBlackListHit(host, listener.host_bl)
class PidCommDest():
"""Helper for recognizing connections that were already displayed."""
def __init__(self, pid, comm, proto, ip, port):
self.pid = pid
self.comm = comm or 'program name unknown'
self.proto = proto or 'unknown protocol'
self.ip = ip or 'unknown IP'
self.port = str(port) or 'port unknown/not applicable'
def isDistinct(self, prev, bound_ips):
"""Not quite inequality.
Requires list of bound IPs for that IP protocol version and recognizes
when a foreign-destined packet was redirected to localhost or to an IP
occupied by an adapter local to the system to be able to suppress
output of these near-duplicates.
"""
return ((not prev) or (self.pid != prev.pid) or
(self.comm != prev.comm) or (self.port != prev.port) or
((self.ip != prev.ip) and (self.ip not in bound_ips)))
def __str__(self):
return '%s (%s) requested %s %s:%s' % (self.comm, self.pid, self.proto,
self.ip, self.port)
class DiverterBase(fnconfig.Config):
"""The beating heart.
You must implement the following methods to ride:
startCallback()
stopCallback()
"""
def __init__(self, diverter_config, listeners_config, ip_addrs,
logging_level=logging.INFO):
"""Initialize the DiverterBase.
TODO: Replace the sys.exit() calls from this function with exceptions
or some other mechanism appropriate for allowing the user of this class
to programmatically detect and handle these cases in their own way.
This may entail moving configuration parsing to a method with a return
value, or modifying fakenet.py to handle Diverter exceptions.
Args:
diverter_config: A dict of [Diverter] config section
listeners_config: A dict of listener configuration sections
ip_addrs: dictionary keyed by integers 4 and 6, with each element
being a list and each list member being a str that is an ASCII
representation of an IP address that is associated with a local
interface on this system.
logging_level: Optional integer logging level such as logging.DEBUG
Returns:
None
"""
# For fine-grained control of subclass debug output. Does not control
# debug output from DiverterBase. To see DiverterBase debug output,
# pass logging.DEBUG as the logging_level argument to init_base.
self.pdebug_level = 0
self.pdebug_labels = dict()
# Override in Windows implementation
self.running_on_windows = False
self.pid = os.getpid()
self.ip_addrs = ip_addrs
self.pcap = None
self.pcap_filename = ''
self.pcap_lock = None
self.logger = logging.getLogger('Diverter')
self.logger.setLevel(logging_level)
# Network Based Indicators
self.nbis = {}
# Index remote Process IDs for MultiHost operations
self.remote_pid_counter = 0
# Maps Proxy initiated source ports to original source ports
self.proxy_sport_to_orig_sport_map = {}
# Maps (proxy_sport, orig_sport) to pkt SSL encryption
self.is_proxied_pkt_ssl_encrypted = {}
# Rate limiting for displaying pid/comm/proto/IP/port
self.last_conn = None
portlists = ['BlackListPortsTCP', 'BlackListPortsUDP']
stringlists = ['HostBlackList']
self.configure(diverter_config, portlists, stringlists)
self.listeners_config = dict((k.lower(), v)
for k, v in listeners_config.items())
# Local IP address
self.external_ip = socket.gethostbyname(socket.gethostname())
self.loopback_ip = socket.gethostbyname('localhost')
# Sessions cache
# NOTE: A dictionary of source ports mapped to destination address,
# port tuples
self.sessions = dict()
# Manage logging of foreign-destined packets
self.nonlocal_ips_already_seen = []
self.log_nonlocal_only_once = True
# Port forwarding table, for looking up original unbound service ports
# when sending replies to foreign endpoints that have attempted to
# communicate with unbound ports. Allows fixing up source ports in
# response packets. Similar to the `sessions` member of the Windows
# Diverter implementation.
self.port_fwd_table = dict()
self.port_fwd_table_lock = threading.Lock()
# Track conversations that will be ignored so that e.g. an RST response
# from a closed port does not erroneously trigger port forwarding and
# silence later replies to legitimate clients.
self.ignore_table = dict()
self.ignore_table_lock = threading.Lock()
# IP forwarding table, for looking up original foreign destination IPs
# when sending replies to local endpoints that have attempted to
# communicate with other machines e.g. via hard-coded C2 IP addresses.
self.ip_fwd_table = dict()
self.ip_fwd_table_lock = threading.Lock()
# Ports bound by FakeNet-NG listeners
self.listener_ports = ListenerPorts()
# Parse listener configurations
self.parse_listeners_config(listeners_config)
#######################################################################
# Diverter settings
# Default TCP/UDP listeners
self.default_listener = dict()
# Global TCP/UDP port blacklist
self.blacklist_ports = {'TCP': [], 'UDP': []}
# Global process blacklist
# TODO: Allow PIDs
self.blacklist_processes = []
self.whitelist_processes = []
# Global host blacklist
# TODO: Allow domain resolution
self.blacklist_hosts = []
# Parse diverter config
self.parse_diverter_config()
slists = ['DebugLevel', ]
self.reconfigure(portlists=[], stringlists=slists)
dbg_lvl = 0
if self.is_configured('DebugLevel'):
for label in self.getconfigval('DebugLevel'):
label = label.upper()
if label == 'OFF':
dbg_lvl = 0
break
if not label in DLABELS_INV:
self.logger.warning('No such DebugLevel as %s' % (label))
else:
dbg_lvl |= DLABELS_INV[label]
self.set_debug_level(dbg_lvl, DLABELS)
#######################################################################
# Network verification - Implemented in OS-specific mixin
# Check active interfaces
if not self.check_active_ethernet_adapters():
self.logger.critical('ERROR: No active ethernet interfaces '
'detected!')
self.logger.critical(' Please enable a network interface.')
sys.exit(1)
# Check configured ip addresses
if not self.check_ipaddresses():
self.logger.critical('ERROR: No interface had IP address '
'configured!')
self.logger.critical(' Please configure an IP address on '
'network interface.')
sys.exit(1)
# Check configured gateways
gw_ok = self.check_gateways()
if not gw_ok:
self.logger.warning('WARNING: No gateways configured!')
if self.is_set('fixgateway'):
gw_ok = self.fix_gateway()
if not gw_ok:
self.logger.warning('Cannot fix gateway')
if not gw_ok:
self.logger.warning(' Please configure a default ' +
'gateway or route in order to intercept ' +
'external traffic.')
self.logger.warning(' Current interception abilities ' +
'are limited to local traffic.')
# Check configured DNS servers
dns_ok = self.check_dns_servers()
if not dns_ok:
self.logger.warning('WARNING: No DNS servers configured!')
if self.is_set('fixdns'):
dns_ok = self.fix_dns()
if not dns_ok:
self.logger.warning('Cannot fix DNS')
if not dns_ok:
self.logger.warning(' Please configure a DNS server ' +
'in order to allow network resolution.')
# OS-specific Diverters must initialize e.g. WinDivert,
# libnetfilter_queue, pf/alf, etc.
def start(self):
"""This method currently only serves the purpose of codifying what must
be implemented on a given OS to bring FakeNet-NG to that OS.
Further refactoring should be done to unify network interface checks,
gateway and DNS configuration, etc. into this method while calling out
to the already-defined (and potentially some yet-to-be-defined)
abstract methods that handle the real OS-specific stuff.
"""
self.logger.debug('Starting...')
return self.startCallback()
def stop(self):
self.logger.info('Stopping...')
self.prettyPrintNbi()
self.generate_html_report()
return self.stopCallback()
@abc.abstractmethod
def startCallback(self):
"""Initiate packet processing and return immediately.
Generally, install hooks/filters and start one or more threads to
siphon packet events.
Returns:
True if successful, else False
"""
pass
@abc.abstractmethod
def stopCallback(self):
"""Terminate packet processing.
Generally set a flag to tell the thread to stop, join with the thread,
uninstall hooks, and change network settings back to normal.
Returns:
True if successful, else False
"""
pass
def set_debug_level(self, lvl, labels={}):
"""Enable debug output if necessary, set the debug output level, and
maintain a reference to the dictionary of labels to print when a given
logging level is encountered.
Args:
lvl: An int mask of all debug logging levels
labels: A dict of int => str assigning names to each debug level
Returns:
None
"""
if lvl:
self.logger.setLevel(logging.DEBUG)
self.pdebug_level = lvl
self.pdebug_labels = labels
def pdebug(self, lvl, s):
"""Log only the debug trace messages that have been enabled via
set_debug_level.
Args:
lvl: An int indicating the debug level of this message
s: The mssage
Returns:
None
"""
if self.pdebug_level & lvl:
label = self.pdebug_labels.get(lvl)
prefix = '[' + label + '] ' if label else '[some component] '
self.logger.debug(prefix + str(s))
def check_privileged(self):
"""UNIXy and Windows-oriented check for superuser privileges.
Returns:
True if superuser, else False
"""
try:
privileged = (os.getuid() == 0)
except AttributeError:
privileged = (ctypes.windll.shell32.IsUserAnAdmin() != 0)
return privileged
def parse_listeners_config(self, listeners_config):
"""Parse listener config sections.
TODO: Replace the sys.exit() calls from this function with exceptions
or some other mechanism appropriate for allowing the user of this class
to programmatically detect and handle these cases in their own way.
This may entail modifying fakenet.py.
Args:
listeners_config: A dict of listener configuration sections
Returns:
None
"""
#######################################################################
# Populate diverter ports and process filters from the configuration
for listener_name, listener_config in listeners_config.items():
if 'port' in listener_config:
port = int(listener_config['port'])
hidden = (listener_config.get('hidden', 'false').lower() ==
'true')
if not 'protocol' in listener_config:
self.logger.error('ERROR: Protocol not defined for ' +
'listener %s', listener_name)
sys.exit(1)
protocol = listener_config['protocol'].upper()
if not protocol in ['TCP', 'UDP']:
self.logger.error('ERROR: Invalid protocol %s for ' +
'listener %s', protocol, listener_name)
sys.exit(1)
listener = ListenerMeta(protocol, port, hidden)
###############################################################
# Process filtering configuration
if ('processwhitelist' in listener_config and
'processblacklist' in listener_config):
self.logger.error('ERROR: Listener can\'t have both ' +
'process whitelist and blacklist.')
sys.exit(1)
elif 'processwhitelist' in listener_config:
self.logger.debug('Process whitelist:')
whitelist = listener_config['processwhitelist']
listener.setProcessWhitelist(whitelist)
# for port in self.port_process_whitelist[protocol]:
# self.logger.debug(' Port: %d (%s) Processes: %s',
# port, protocol, ', '.join(
# self.port_process_whitelist[protocol][port]))
elif 'processblacklist' in listener_config:
self.logger.debug('Process blacklist:')
blacklist = listener_config['processblacklist']
listener.setProcessBlacklist(blacklist)
# for port in self.port_process_blacklist[protocol]:
# self.logger.debug(' Port: %d (%s) Processes: %s',
# port, protocol, ', '.join(
# self.port_process_blacklist[protocol][port]))
###############################################################
# Host filtering configuration
if ('hostwhitelist' in listener_config and
'hostblacklist' in listener_config):
self.logger.error('ERROR: Listener can\'t have both ' +
'host whitelist and blacklist.')
sys.exit(1)
elif 'hostwhitelist' in listener_config:
self.logger.debug('Host whitelist:')
host_whitelist = listener_config['hostwhitelist']
listener.setHostWhitelist(host_whitelist)
# for port in self.port_host_whitelist[protocol]:
# self.logger.debug(' Port: %d (%s) Hosts: %s', port,
# protocol, ', '.join(
# self.port_host_whitelist[protocol][port]))
elif 'hostblacklist' in listener_config:
self.logger.debug('Host blacklist:')
host_blacklist = listener_config['hostblacklist']
listener.setHostBlacklist(host_blacklist)
# for port in self.port_host_blacklist[protocol]:
# self.logger.debug(' Port: %d (%s) Hosts: %s', port,
# protocol, ', '.join(
# self.port_host_blacklist[protocol][port]))
# Listener metadata is now configured, add it to the dictionary
self.listener_ports.addListener(listener)
###############################################################
# Execute command configuration
if 'executecmd' in listener_config:
template = listener_config['executecmd'].strip()
# Would prefer not to get into the middle of a debug
# session and learn that a typo has ruined the day, so we
# test beforehand to make sure all the user-specified
# insertion strings are valid.
test = self._build_cmd(template, 0, 'test', '1.2.3.4',
12345, '4.3.2.1', port)
if not test:
self.logger.error(('Terminating due to incorrectly ' +
'configured ExecuteCmd for ' +
'listener %s') % (listener_name))
sys.exit(1)
listener.setExecuteCmd(template)
self.logger.debug('Port %d (%s) ExecuteCmd: %s', port,
protocol,
template)
def build_cmd(self, pkt, pid, comm):
"""Retrieve the ExecuteCmd directive if applicable and build the
command to execute.
Args:
pkt: An fnpacket.PacketCtx or derived object
pid: Process ID associated with the packet
comm: Process name (command) that sent the packet
Returns:
A str that is the resultant command to execute
"""
cmd = None
template = self.listener_ports.getExecuteCmd(pkt.proto, pkt.dport)
if template:
cmd = self._build_cmd(template, pid, comm, pkt.src_ip, pkt.sport,
pkt.dst_ip, pkt.dport)
return cmd
def _build_cmd(self, tmpl, pid, comm, src_ip, sport, dst_ip, dport):
"""Build a command based on the template specified in an ExecuteCmd
config directive, applying the parameters as needed.
Accepts individual arguments instead of an fnpacket.PacketCtx so that
the Diverter can test any ExecuteCmd directives at configuration time
without having to synthesize a fnpacket.PacketCtx or construct a
NamedTuple to satisfy the requirement for such an argument.
Args:
tmpl: A str containing the body of the ExecuteCmd config directive
pid: Process ID associated with the packet
comm: Process name (command) that sent the packet
src_ip: The source IP address that originated the packet
sport: The source port that originated the packet
dst_ip: The destination IP that the packet was directed at
dport: The destination port that the packet was directed at
Returns:
A str that is the resultant command to execute
"""
cmd = None
try:
cmd = tmpl.format(
pid=str(pid),
procname=str(comm),
src_addr=str(src_ip),
src_port=str(sport),
dst_addr=str(dst_ip),
dst_port=str(dport))
except KeyError as e:
self.logger.error(('Failed to build ExecuteCmd for port %d due ' +
'to erroneous format key: %s') %
(dport, e.message))
return cmd
def execute_detached(self, execute_cmd):
"""OS-agnostic asynchronous subprocess creation.
Executes the process with the appropriate subprocess.Popen parameters
for UNIXy or Windows platforms to isolate the process from FakeNet-NG
to prevent it from being interrupted by termination of FakeNet-NG,
Ctrl-C, etc.
Args:
execute_cmd: A str that is the command to execute
Side-effects:
Creates the specified process.
Returns:
Success => an int that is the pid of the new process
Failure => None
"""
DETACHED_PROCESS = 0x00000008
cflags = DETACHED_PROCESS if self.running_on_windows else 0
cfds = False if self.running_on_windows else True
shl = False if self.running_on_windows 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 self.running_on_windows else ign_sigint
try:
pid = subprocess.Popen(execute_cmd, creationflags=cflags,
shell=shl,
close_fds=cfds,
preexec_fn=preexec).pid
except Exception as e: