From 53a20308de310b0493ae542a827151dec3a9179b Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 10 Sep 2017 10:36:00 +0300 Subject: [PATCH 1/5] Rewrite get_host_subnets, drastically simplify Linux implementation. Cleanup code in get_ips_from_interfaces Modern python bug fix --- chaos_monkey/network/info.py | 102 ++++++++++++++--------------------- 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py index 59b40a089..6c014a609 100644 --- a/chaos_monkey/network/info.py +++ b/chaos_monkey/network/info.py @@ -1,34 +1,34 @@ import os import sys -import array import socket import struct import psutil import ipaddress +import itertools +import netifaces from subprocess import check_output from random import randint if sys.platform == "win32": - import netifaces - def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] - def get_host_subnets(only_ips=False): - network_adapters = [] - valid_ips = local_ips() - if only_ips: - return valid_ips - interfaces = [netifaces.ifaddresses(x) for x in netifaces.interfaces()] - for inte in interfaces: - if netifaces.AF_INET in inte: - for add in inte[netifaces.AF_INET]: - if "netmask" in add and add["addr"] in valid_ips: - network_adapters.append((add["addr"], add["netmask"])) - return network_adapters + def get_host_subnets(): + ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] + for interface in netifaces.interfaces() + if netifaces.AF_INET in netifaces.ifaddresses(interface) + ] + # flatten + ipv4_nets = itertools.chain.from_iterable(ipv4_nets) + # remove loopback + ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] + # remove auto conf + ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] + return ipv4_nets + def get_routes(): raise NotImplementedError() @@ -36,46 +36,26 @@ if sys.platform == "win32": else: from fcntl import ioctl - def get_host_subnets(only_ips=False): - """Get the list of Linux network adapters.""" - max_bytes = 8096 - is_64bits = sys.maxsize > 2 ** 32 - if is_64bits: - offset1 = 16 - offset2 = 40 - else: - offset1 = 32 - offset2 = 32 - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - names = array.array('B', '\0' * max_bytes) - outbytes = struct.unpack('iL', ioctl( - sock.fileno(), - 0x8912, - struct.pack('iL', max_bytes, names.buffer_info()[0])))[0] - adapter_names = [names.tostring()[n_cnt:n_cnt + offset1].split('\0', 1)[0] - for n_cnt in xrange(0, outbytes, offset2)] - network_adapters = [] - for adapter_name in adapter_names: - ip_address = socket.inet_ntoa(ioctl( - sock.fileno(), - 0x8915, - struct.pack('256s', adapter_name))[20:24]) - if ip_address.startswith('127'): - continue - subnet_mask = socket.inet_ntoa(ioctl( - sock.fileno(), - 0x891b, - struct.pack('256s', adapter_name))[20:24]) - if only_ips: - network_adapters.append(ip_address) - else: - network_adapters.append((ip_address, subnet_mask)) + def get_host_subnets(): + ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] + for interface in netifaces.interfaces() + if netifaces.AF_INET in netifaces.ifaddresses(interface) + ] + # flatten + ipv4_nets = list(itertools.chain.from_iterable(ipv4_nets)) + # remove loopback + ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] + # remove auto conf + ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] + return ipv4_nets - return network_adapters def local_ips(): - return get_host_subnets(only_ips=True) + ipv4_nets = get_host_subnets() + valid_ips = [network['addr'] for network in ipv4_nets] + return valid_ips + def get_routes(): # based on scapy implementation for route parsing LOOPBACK_NAME = "lo" @@ -151,17 +131,17 @@ def check_internet_access(services): def get_ips_from_interfaces(): res = [] ifs = get_host_subnets() - for interface in ifs: - ipint = ipaddress.ip_interface(u"%s/%s" % interface) + for net_interface in ifs: + host_addr = ipaddress.ip_address(net_interface['addr']) + ip_interface = ipaddress.ip_interface(u"%s/%s" % (net_interface['addr'], net_interface['netmask'])) # limit subnet scans to class C only - if ipint.network.num_addresses > 255: - ipint = ipaddress.ip_interface(u"%s/24" % interface[0]) - for addr in ipint.network.hosts(): - if str(addr) == interface[0]: - continue - res.append(str(addr)) + if ip_interface.network.num_addresses > 255: + ip_interface = ipaddress.ip_interface(u"%s/24" % net_interface['addr']) + addrs = [str(addr) for addr in ip_interface.network.hosts() if addr != host_addr] + res.extend(addrs) return res + if sys.platform == "win32": def get_ip_for_connection(target_ip): return None @@ -171,7 +151,7 @@ else: query_str = 'ip route get %s' % target_ip resp = check_output(query_str.split()) substr = resp.split() - src = substr[substr.index('src')+1] + src = substr[substr.index('src') + 1] return src except Exception: - return None \ No newline at end of file + return None From bdc9b2fcb97e2a2d8eab9868f95650e478f5659d Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 10 Sep 2017 13:11:51 +0300 Subject: [PATCH 2/5] Return network information. --- chaos_monkey/system_info/__init__.py | 4 ++++ chaos_monkey/system_info/linux_info_collector.py | 1 + chaos_monkey/system_info/windows_info_collector.py | 1 + 3 files changed, 6 insertions(+) diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index 2238fb542..728dd8966 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -2,6 +2,7 @@ import sys import socket import psutil from enum import IntEnum +from network.info import get_host_subnets, local_ips __author__ = 'uri' @@ -69,3 +70,6 @@ class InfoCollector(object): } pass self.info['process_list'] = processes + + def get_network_info(self): + self.info['network'] = {'subnets': get_host_subnets(), 'local_ips': local_ips()} diff --git a/chaos_monkey/system_info/linux_info_collector.py b/chaos_monkey/system_info/linux_info_collector.py index 64a31c865..6c7570fc0 100644 --- a/chaos_monkey/system_info/linux_info_collector.py +++ b/chaos_monkey/system_info/linux_info_collector.py @@ -14,4 +14,5 @@ class LinuxInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + self.get_network_info() return self.info diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index 5cb1253ab..2ba26fd34 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -14,6 +14,7 @@ class WindowsInfoCollector(InfoCollector): def get_info(self): self.get_hostname() self.get_process_list() + self.get_network_info() mimikatz_collector = MimikatzCollector() self.info["credentials"] = mimikatz_collector.get_logon_info() return self.info From 7b4fb5d4f0c383c855eba52e4a75f149f528962d Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 11 Sep 2017 16:56:23 +0300 Subject: [PATCH 3/5] Don't return local IPs. Return computer fqdn for further information --- chaos_monkey/system_info/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index 728dd8966..fd1aa807f 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -46,7 +46,11 @@ class InfoCollector(object): self.info = {} def get_hostname(self): - self.info['hostname'] = socket.gethostname() + """ + Adds the computer hostname to the system information. + :return: + """ + self.info['hostname'] = socket.getfqdn() def get_process_list(self): processes = {} @@ -72,4 +76,9 @@ class InfoCollector(object): self.info['process_list'] = processes def get_network_info(self): - self.info['network'] = {'subnets': get_host_subnets(), 'local_ips': local_ips()} + """ + Adds network information from the host to the system information. + Currently returns a list of networks accessible from host, containing host ip and the subnet range. + :return: None + """ + self.info['network_info'] = {'networks': get_host_subnets()} From 1bd633a0b13271e856fe44aa87987379c32ba3c9 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 11 Sep 2017 16:57:37 +0300 Subject: [PATCH 4/5] get subnets is now cross OS since it's identical + remove broadcast key since we don't have anything to do with it. --- chaos_monkey/network/info.py | 46 ++++++++++++++---------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py index 6c014a609..45b733f45 100644 --- a/chaos_monkey/network/info.py +++ b/chaos_monkey/network/info.py @@ -9,6 +9,24 @@ import netifaces from subprocess import check_output from random import randint + +def get_host_subnets(): + ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] + for interface in netifaces.interfaces() + if netifaces.AF_INET in netifaces.ifaddresses(interface) + ] + # flatten + ipv4_nets = itertools.chain.from_iterable(ipv4_nets) + # remove loopback + ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] + # remove auto conf + ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] + for network in ipv4_nets: + if 'broadcast' in network: + network.pop('broadcast') + return ipv4_nets + + if sys.platform == "win32": def local_ips(): @@ -16,20 +34,6 @@ if sys.platform == "win32": return socket.gethostbyname_ex(local_hostname)[2] - def get_host_subnets(): - ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] - for interface in netifaces.interfaces() - if netifaces.AF_INET in netifaces.ifaddresses(interface) - ] - # flatten - ipv4_nets = itertools.chain.from_iterable(ipv4_nets) - # remove loopback - ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] - # remove auto conf - ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] - return ipv4_nets - - def get_routes(): raise NotImplementedError() @@ -37,20 +41,6 @@ else: from fcntl import ioctl - def get_host_subnets(): - ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] - for interface in netifaces.interfaces() - if netifaces.AF_INET in netifaces.ifaddresses(interface) - ] - # flatten - ipv4_nets = list(itertools.chain.from_iterable(ipv4_nets)) - # remove loopback - ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] - # remove auto conf - ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] - return ipv4_nets - - def local_ips(): ipv4_nets = get_host_subnets() valid_ips = [network['addr'] for network in ipv4_nets] From a85d4e8775d6495ae89aaf206c5edbdc4b7318fa Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Mon, 11 Sep 2017 19:24:18 +0300 Subject: [PATCH 5/5] Documentation --- chaos_monkey/network/info.py | 15 +++++++++++++++ chaos_monkey/system_info/__init__.py | 13 ++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/chaos_monkey/network/info.py b/chaos_monkey/network/info.py index 45b733f45..605799ce3 100644 --- a/chaos_monkey/network/info.py +++ b/chaos_monkey/network/info.py @@ -11,6 +11,11 @@ from random import randint def get_host_subnets(): + """ + Returns a list of subnets visible to host (omitting loopback and auto conf networks) + Each subnet item contains the host IP in that network + the subnet. + :return: List of dict, keys are "addr" and "subnet" + """ ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] for interface in netifaces.interfaces() if netifaces.AF_INET in netifaces.ifaddresses(interface) @@ -111,6 +116,11 @@ def get_free_tcp_port(min_range=1000, max_range=65535): def check_internet_access(services): + """ + Checks if any of the services are accessible, over ICMP + :param services: List of IPs/hostnames + :return: boolean depending on internet access + """ ping_str = "-n 1" if sys.platform.startswith("win") else "-c 1" for host in services: if os.system("ping " + ping_str + " " + host) == 0: @@ -119,6 +129,11 @@ def check_internet_access(services): def get_ips_from_interfaces(): + """ + Returns a list of IPs accessible in the host in each network interface, in the subnet. + Limits to a single class C if the network is larger + :return: List of IPs, marked as strings. + """ res = [] ifs = get_host_subnets() for net_interface in ifs: diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index fd1aa807f..b9a16d459 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -47,12 +47,18 @@ class InfoCollector(object): def get_hostname(self): """ - Adds the computer hostname to the system information. - :return: + Adds the fully qualified computer hostname to the system information. + :return: Nothing """ self.info['hostname'] = socket.getfqdn() def get_process_list(self): + """ + Adds process information from the host to the system information. + Currently lists process name, ID, parent ID, command line + and the full image path of each process. + :return: Nothing + """ processes = {} for process in psutil.process_iter(): try: @@ -78,7 +84,8 @@ class InfoCollector(object): def get_network_info(self): """ Adds network information from the host to the system information. - Currently returns a list of networks accessible from host, containing host ip and the subnet range. + Currently updates with a list of networks accessible from host, + containing host ip and the subnet range. :return: None """ self.info['network_info'] = {'networks': get_host_subnets()}