mirror of
https://github.com/vanhoefm/fragattacks.git
synced 2025-01-31 09:14:03 -05:00
fragattack: ability to test injection capabilities of device
This commit is contained in:
parent
3331b80fb7
commit
173e11d400
97
research/NOTES.md
Normal file
97
research/NOTES.md
Normal file
@ -0,0 +1,97 @@
|
||||
# Monitor mode injectin
|
||||
|
||||
Device that purely operate in monitor mode might overwrite certain fields of
|
||||
injected frames. Here we document the default behaviour of some devices.
|
||||
|
||||
When using a single physical interface to create a virtual managed _and_ monitor
|
||||
interface, there are additional unexpected consequences to injection of frames.
|
||||
These depend on the specific driver/device being used, and we discuss some of
|
||||
these issues here as well.
|
||||
|
||||
|
||||
## Intel 8265 / 8275 (rev 78) devices
|
||||
|
||||
Summary: this can be used without driver/firmware changes in pure monitor mode,
|
||||
but care is still needed that frames with different priority are not
|
||||
reordered (**TODO: Explain parameter to force this**).
|
||||
|
||||
- When connecting normally on Arch Linux, while connecting it sends frames with
|
||||
all three address equal to it's own address. This frame contains the numbers
|
||||
1 to 0x27 as 32-bit numbers for some reason. This is a strange bug, but at
|
||||
least it is not caused by our driver modifications.
|
||||
|
||||
- Had to patch driver to prevent sequence number and QoS TID to be overwritten
|
||||
**TODO: Also in pure monitor?**
|
||||
|
||||
- Unable to transmit any frames from a different transmitter address. This is
|
||||
because in `ieee80211_monitor_start_xmit` it cannot find a channel to transmit
|
||||
on (finding a valid chandef fails).
|
||||
**TODO: Also in pure monitor?**
|
||||
|
||||
- Cannot inject frames using a TID that is used for the first time. There's no
|
||||
queue in the driver allocated for it yet it seems, and this causes issues.
|
||||
To prevent this, and prevent frame reordering, we inject all frames on the
|
||||
same queue in the driver.
|
||||
**TODO: Also in pure monitor?**
|
||||
|
||||
- It ignores `IEEE80211_RADIOTAP_DATA_RETRIES` and retransmites frames 15 times
|
||||
both in purely monitor more and mixed managed/monitor mode (before and after
|
||||
authenticating).
|
||||
|
||||
- Unlike, ath9k_htc, in mixed managed/monitor, we can inject frames before the
|
||||
association request is sent. Strangely, the Intel device also sends some strange
|
||||
frames while connecting (even on Windows 10). But that only seems to slow down
|
||||
the injection of frames.
|
||||
|
||||
|
||||
## Ath9k_htc devices
|
||||
|
||||
Summary: when using this device, you must use a modified driver/firmware.
|
||||
Since this is a USB device, this can be done inside a virtual machine.
|
||||
|
||||
- The ath9k_htc devices by default overwrite the injected sequence number,
|
||||
**even when purely operating in monitor mode**.
|
||||
|
||||
Interestingly, the device will not increment the sequence number when the
|
||||
MoreFragments flag is set, meaning we can inject fragmented frames (albeit
|
||||
with a different sequence number than then one we use in the user-space
|
||||
script).
|
||||
|
||||
- The above trick does not work when we want to inject other frames between
|
||||
two fragmented frames (the chip will assign them difference sequence numbers).
|
||||
Even when the fragments use different QoS TIDs, sending frames between them
|
||||
will make the chip assign difference sequence numbers to both fragments.
|
||||
**TODO: This only is the case in mixed manager/monitor mode I think?**
|
||||
|
||||
- Overwriting the sequence can be avoided by patching `ath_tgt_tx_seqno_normal`
|
||||
and commenting out the two lines that modify `i_seq`. Note that these changes
|
||||
are in the firmware of the device.
|
||||
|
||||
- See also the comment in Station.perform_actions to avoid other bugs with
|
||||
ath9k_htc when injecting frames with the MF flag and while being in AP mode.
|
||||
|
||||
- The at9k_htc dongle, like other Wi-Fi devices, will reorder frames with
|
||||
different QoS priorities. This means injected frames with differen priorities
|
||||
may get reordered by the driver/chip. We avoided this by modifying the ath9k_htc
|
||||
driver to send all frames using the transmission queue of priority zero,
|
||||
independent of the actual QoS priority value used in the frame.
|
||||
**This happens even when purely operating in monitor mode.**
|
||||
|
||||
- It doesn't retransmit frames in pure monitor mode. In mixed managed/monitor
|
||||
after (or right before authentication) it retransmits frames at most ones.
|
||||
But it **injects a lot of RTS** as times?!
|
||||
|
||||
- In mixed/managed mode, we can inject frames when the managed interface is up
|
||||
but not being controlled by wpa_supplicant (but unknown which channel will be
|
||||
used). When connecting using wpa_supplicant, it seems we can only inject frames
|
||||
after the association request has been sent.
|
||||
|
||||
- In mixed AP/monitor mode, when injecting the first fragment of a frame, it will
|
||||
be injected properly, but afterards the chip won't second beacons for one second.
|
||||
This can be prevented by injected a dummy packet after the injected fragment.
|
||||
|
||||
# TODOs
|
||||
|
||||
- When using the mac80211_hwsim trick with one monitor interface, there is
|
||||
still the risk of frames with different QoS TIDs being reordered.
|
||||
|
@ -46,8 +46,8 @@ class TestOptions():
|
||||
self.peerip = None
|
||||
|
||||
def log_level2switch():
|
||||
if global_log_level == 1: return ["-d", "-K"]
|
||||
elif global_log_level <= 0: return ["-dd", "-K"]
|
||||
if options.debug >= 2: return ["-dd", "-K"]
|
||||
elif options.debug >= 1: return ["-d", "-K"]
|
||||
return ["-K"]
|
||||
|
||||
#TODO: Move to libwifi?
|
||||
@ -102,62 +102,6 @@ def freebsd_encap_eapolmsdu(p, src, dst, payload):
|
||||
p = p/freebsd_create_eapolmsdu(src, dst, payload)
|
||||
return p
|
||||
|
||||
def set_monitor_mode(iface):
|
||||
# Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly
|
||||
# sequence of commands assures the virtual interface is properly registered as a 802.11 monitor interface.
|
||||
subprocess.check_output(["ifconfig", iface, "down"])
|
||||
subprocess.check_output(["iw", iface, "set", "type", "monitor"])
|
||||
time.sleep(0.5)
|
||||
subprocess.check_output(["iw", iface, "set", "type", "monitor"])
|
||||
subprocess.check_output(["ifconfig", iface, "up"])
|
||||
subprocess.check_output(["ifconfig", iface, "mtu", "2200"])
|
||||
|
||||
|
||||
# ----------------------------------- Injection Tests -----------------------------------
|
||||
|
||||
def test_packet_injection(sout, sin, p, test_func):
|
||||
log(WARNING, "Testing injection")
|
||||
|
||||
# Append unique label to recognize frame & inject it
|
||||
label = b"AAAA" + struct.pack(">II", random.randint(0, 2**32), random.randint(0, 2**32))
|
||||
sout.send(RadioTap()/p/Raw(label))
|
||||
|
||||
#TODO: Should we prevent ath9k_htc injection bug after injecting a fragment?
|
||||
|
||||
# 1. When using a 2nd interface: capture the actual packet that was injected in the air.
|
||||
# 2. Not using 2nd interface: capture the "reflected" frame sent back by the kernel. This allows
|
||||
# us to at least detect if the kernel (and perhaps driver) is overwriting fields. It generally
|
||||
# doesn't allow us to detect if the device/firmware itself is overwriting fields.
|
||||
packets = sniff(opened_socket=sin, timeout=2, count=1, lfilter=lambda p: p != None and label in raw(p))
|
||||
if len(packets) < 1:
|
||||
raise IOError("Unable to inject test frame. Does your driver/device support monitor mode?")
|
||||
|
||||
# Property must hold for all frames
|
||||
return all([test_func(cap) for cap in packets])
|
||||
|
||||
def test_injection(iface_out, iface_in=None):
|
||||
# We start monitoring iface_in already so injected frame won't be missed
|
||||
sout = L2Socket(type=ETH_P_ALL, iface=iface_out)
|
||||
if iface_in == None:
|
||||
sin = sout
|
||||
else:
|
||||
sin = L2Socket(type=ETH_P_ALL, iface=iface_in)
|
||||
|
||||
p = Dot11(addr1="00:11:00:00:00:01", addr2="00:22:00:00:00:01", type=2, SC=33<<4)
|
||||
if not test_packet_injection(sout, sin, p, lambda cap: cap.SC == p.SC):
|
||||
raise IOError("Sequence number of injected frames is being overwritten!")
|
||||
|
||||
p = Dot11(addr1="00:11:00:00:00:02", addr2="00:22:00:00:00:02", type=2, SC=(33<<4)|1)
|
||||
if not test_packet_injection(sout, sin, p, lambda cap: (cap.SC & 0xf) == 1):
|
||||
raise IOError("Fragment number of injected frames is being overwritten!")
|
||||
|
||||
p = Dot11(addr1="00:11:00:00:00:03", addr2="00:22:00:00:00:03", type=2, subtype=8, SC=33)/Dot11QoS(TID=2)
|
||||
if not test_packet_injection(sout, sin, p, lambda cap: cap.TID == p.TID):
|
||||
raise IOError("QoS TID of injected frames is being overwritten!")
|
||||
|
||||
sout.close()
|
||||
sin.close()
|
||||
|
||||
|
||||
# ----------------------------------- Vulnerability Tests -----------------------------------
|
||||
|
||||
@ -647,6 +591,11 @@ class Station():
|
||||
script was sending data *before* the key had been installed (or the port
|
||||
authorized). This meant traffic was dropped. Use this function to manually
|
||||
send frames over the monitor interface to ensure delivery and encryption.
|
||||
|
||||
By default we use a TID of 1. Since our tests by default use a TID of 2,
|
||||
this reduces the chance the frames sent using this function (which most
|
||||
are EAP or EAPOL frames) interfere with the reassembly of frames sent by
|
||||
the tests.
|
||||
"""
|
||||
|
||||
# If it contains an Ethernet header, strip it, and take addresses from that
|
||||
@ -909,7 +858,6 @@ class Daemon(metaclass=abc.ABCMeta):
|
||||
self.nic_iface = None
|
||||
self.nic_mon = None
|
||||
self.nic_hwsim = None
|
||||
self.performed_injection_selftest = False
|
||||
|
||||
self.process = None
|
||||
self.sock_eth = None
|
||||
@ -973,11 +921,6 @@ class Daemon(metaclass=abc.ABCMeta):
|
||||
# Use the provided interface to monitor/inject frames
|
||||
self.nic_mon = self.options.inject
|
||||
|
||||
# Avoid the monitor interface from retransmitting frames? This was not needed for the
|
||||
# ath9k_htc and intel/mvm device that I tested. Perhaps for others it might be needed.
|
||||
#subprocess.call(["ifconfig", self.nic_mon, "down"])
|
||||
#subprocess.call(["macchanger", "-m", get_macaddress(self.nic_iface), self.nic_mon])
|
||||
|
||||
else:
|
||||
# Create second virtual interface in monitor mode. Note: some kernels
|
||||
# don't support interface names of 15+ characters.
|
||||
@ -1007,6 +950,10 @@ class Daemon(metaclass=abc.ABCMeta):
|
||||
if self.nic_hwsim:
|
||||
set_monitor_mode(self.nic_hwsim)
|
||||
|
||||
# 3. Configure test interface if used
|
||||
if self.options.inject_test:
|
||||
set_monitor_mode(self.options.inject_test)
|
||||
|
||||
def inject_mon(self, p):
|
||||
self.sock_mon.send(p)
|
||||
|
||||
@ -1028,24 +975,36 @@ class Daemon(metaclass=abc.ABCMeta):
|
||||
log(ERROR, "Did you disable Wi-Fi in the network manager? Otherwise it won't start properly.")
|
||||
raise
|
||||
|
||||
def injection_selftest(self):
|
||||
if self.performed_injection_selftest:
|
||||
return
|
||||
def follow_channel(self):
|
||||
channel = get_channel(self.nic_iface)
|
||||
if self.options.inject:
|
||||
set_channel(self.nic_mon, channel)
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
elif self.options.hwsim:
|
||||
set_channel(self.nic_hwsim, channel)
|
||||
set_channel(self.nic_mon, channel)
|
||||
log(STATUS, f"{self.nic_hwsim}: setting to channel {channel}")
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
|
||||
# - When using a 2nd interface to inject frames, this self-test should trivially succeed.
|
||||
# - This is mainly useful when using one interface as both client and monitor interface,
|
||||
# in which case a modified kernel/driver must be used.
|
||||
# - With the Intel/mvm injection in this case only works if hostapd/wpa_supp started.
|
||||
# - When in client mode, it seems the scanning operation interferes with this test. So it
|
||||
# must be executed once we are trying to connect so the channel is "stable".
|
||||
try: test_injection(self.nic_mon)
|
||||
if self.options.inject_test:
|
||||
set_channel(self.options.inject_test, channel)
|
||||
log(STATUS, f"{self.options.inject_test}: setting to channel {channel}")
|
||||
# When explicitly testing we can afford a longer timeout. Otherwise we should avoid it.
|
||||
time.sleep(0.5)
|
||||
|
||||
def injection_test(self, peermac):
|
||||
# Only perform the test when explicitly requested
|
||||
if self.options.inject_test == None: return
|
||||
|
||||
try:
|
||||
test_injection(self.nic_mon, self.options.inject_test, peermac)
|
||||
except IOError as ex:
|
||||
log(WARNING, ex.args[0])
|
||||
log(ERROR, "Injection self-test failed. Are you using the correct kernel/driver/device for injection?")
|
||||
log(ERROR, "Unexpected error. Are you using the correct kernel/driver/device?")
|
||||
quit(1)
|
||||
|
||||
log(DEBUG, f"Passed injection self-test on interface {self.nic_mon}.")
|
||||
self.performed_injection_selftest = True
|
||||
quit(1)
|
||||
|
||||
def forward_hwsim(self, p, s):
|
||||
if p == None: return
|
||||
@ -1072,6 +1031,7 @@ class Daemon(metaclass=abc.ABCMeta):
|
||||
self.sock_hwsim = MonitorSocket(type=ETH_P_ALL, iface=self.nic_hwsim)
|
||||
|
||||
# Post-startup configuration of the supplicant or AP
|
||||
wpaspy_command(self.wpaspy_ctrl, "SET ext_eapol_frame_io 1")
|
||||
self.configure_daemon()
|
||||
|
||||
# Monitor the virtual monitor interface of the client and perform the needed actions
|
||||
@ -1193,6 +1153,8 @@ class Authenticator(Daemon):
|
||||
station.set_ip_addresses(self.options.ip, self.options.peerip)
|
||||
|
||||
def handle_wpaspy(self, msg):
|
||||
log(DEBUG, "daemon: " + msg)
|
||||
|
||||
if "AP-STA-CONNECTING" in msg:
|
||||
cmd, clientmac = msg.split()
|
||||
self.add_station(clientmac)
|
||||
@ -1202,6 +1164,11 @@ class Authenticator(Daemon):
|
||||
station.handle_connecting(self.apmac)
|
||||
station.set_peermac(clientmac)
|
||||
|
||||
# When in client mode, the scanning operation might interferes with this test.
|
||||
# So it must be executed once we are connecting so the channel is stable.
|
||||
# TODO: Avoid client from disconnecting during test.
|
||||
self.injection_test(clientmac)
|
||||
|
||||
elif "EAPOL-TX" in msg:
|
||||
cmd, clientmac, payload = msg.split()
|
||||
if not clientmac in self.stations:
|
||||
@ -1209,7 +1176,6 @@ class Authenticator(Daemon):
|
||||
return
|
||||
self.stations[clientmac].handle_eapol_tx(bytes.fromhex(payload))
|
||||
|
||||
# XXX WPA1: Take into account group key handshake on initial 4-way HS
|
||||
elif "AP-STA-CONNECTED" in msg:
|
||||
cmd, clientmac = msg.split()
|
||||
if not clientmac in self.stations:
|
||||
@ -1231,9 +1197,6 @@ class Authenticator(Daemon):
|
||||
self.apmac = get_macaddress(self.nic_iface)
|
||||
|
||||
def configure_daemon(self):
|
||||
# Intercept EAPOL packets that the AP wants to send
|
||||
wpaspy_command(self.wpaspy_ctrl, "SET ext_eapol_frame_io 1")
|
||||
|
||||
# Let scapy handle DHCP requests
|
||||
self.dhcp = DHCP_sock(sock=self.sock_eth,
|
||||
domain='mathyvanhoef.com',
|
||||
@ -1252,18 +1215,7 @@ class Authenticator(Daemon):
|
||||
#log(STATUS, f"Will inject ARP packets using sender IP {self.arp_sender_ip}")
|
||||
|
||||
# When using a separate interface to inject, switch to correct channel
|
||||
if self.options.inject:
|
||||
channel = get_channel(self.nic_iface)
|
||||
set_channel(self.nic_mon, channel)
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
elif self.options.hwsim:
|
||||
channel = get_channel(self.nic_iface)
|
||||
set_channel(self.nic_hwsim, channel)
|
||||
set_channel(self.nic_mon, channel)
|
||||
log(STATUS, f"{self.nic_hwsim}: setting to channel {channel}")
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
|
||||
self.injection_selftest()
|
||||
self.follow_channel()
|
||||
|
||||
|
||||
class Supplicant(Daemon):
|
||||
@ -1392,18 +1344,7 @@ class Supplicant(Daemon):
|
||||
|
||||
elif "Trying to authenticate with" in msg:
|
||||
# When using a separate interface to inject, switch to correct channel
|
||||
if self.options.inject:
|
||||
channel = get_channel(self.nic_iface)
|
||||
set_channel(self.nic_mon, channel)
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
|
||||
elif self.options.hwsim:
|
||||
# FIXME: There is some delay, causing the first authentication to fail
|
||||
channel = get_channel(self.nic_iface)
|
||||
set_channel(self.nic_mon, channel)
|
||||
set_channel(self.nic_hwsim, channel)
|
||||
log(STATUS, f"{self.nic_mon}: setting to channel {channel}")
|
||||
log(STATUS, f"{self.nic_hwsim}: setting to channel {channel}")
|
||||
self.follow_channel()
|
||||
|
||||
p = re.compile("Trying to authenticate with (.*) \(SSID")
|
||||
bss = p.search(msg).group(1)
|
||||
@ -1412,8 +1353,7 @@ class Supplicant(Daemon):
|
||||
elif "Trying to associate with" in msg:
|
||||
# With the ath9k_htc, injection in mixed managed/monitor only works after
|
||||
# sending the association request. So only perform injection test now.
|
||||
# TODO: Only do a self-test when in mixed managed/monitor mode?
|
||||
self.injection_selftest()
|
||||
self.injection_test(self.station.bss)
|
||||
|
||||
elif "EAPOL-TX" in msg:
|
||||
cmd, srcaddr, payload = msg.split()
|
||||
@ -1430,15 +1370,15 @@ class Supplicant(Daemon):
|
||||
|
||||
def reconnect(self, station):
|
||||
log(STATUS, "Reconnecting to the AP.", color="green")
|
||||
|
||||
# Optimize reassoc-to-same-BSS by default. This makes the "REASSOCIATE" command skip
|
||||
# the authentication phase (reducing the chance that packet queues are reset).
|
||||
optim = "0" if self.options.full_reconnect else "1"
|
||||
|
||||
wpaspy_command(self.wpaspy_ctrl, f"SET reassoc_same_bss_optim {optim}")
|
||||
wpaspy_command(self.wpaspy_ctrl, "REASSOCIATE")
|
||||
|
||||
def configure_daemon(self):
|
||||
# Optimize reassoc-to-same-BSS. This makes the "REASSOCIATE" command skip the
|
||||
# authentication phase (reducing the chance that packet queues are reset).
|
||||
wpaspy_command(self.wpaspy_ctrl, "SET ext_eapol_frame_io 1")
|
||||
|
||||
# If the user already supplied IPs we can immediately perform tests
|
||||
if self.options.ip and self.options.peerip:
|
||||
self.initialize_ips(self.options.ip, self.options.peerip)
|
||||
@ -1645,7 +1585,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument('testname', help="Name or identifier of the test to run.")
|
||||
parser.add_argument('actions', nargs='?', help="Optional textual descriptions of actions")
|
||||
parser.add_argument('--inject', default=None, help="Interface to use to inject frames.")
|
||||
parser.add_argument('--inject-test', default=None, help="Use main interface to test injection through this interface.")
|
||||
parser.add_argument('--inject-test', default=None, help="Use given interface to test injection through monitor interface.")
|
||||
parser.add_argument('--hwsim', default=None, help="Use provided interface in monitor mode, and simulate AP/client through hwsim.")
|
||||
parser.add_argument('--ip', help="IP we as a sender should use.")
|
||||
parser.add_argument('--peerip', help="IP of the device we will test.")
|
||||
@ -1676,24 +1616,18 @@ if __name__ == "__main__":
|
||||
# Default value for options that should not be command line parameters
|
||||
options.inject_workaround = False
|
||||
|
||||
# Perform test using a second interface if requested
|
||||
if options.inject_test:
|
||||
log(WARNING, "TODO: Perform proper injection tests (including with active AP/STA")
|
||||
quit(1)
|
||||
|
||||
# Sanity check and convert some arguments to more usable form
|
||||
options.ptype = args2ptype(options)
|
||||
options.as_msdu = args2msdu(options)
|
||||
|
||||
# Construct the test
|
||||
options.test = prepare_tests(options)
|
||||
|
||||
if options.test == None:
|
||||
log(STATUS, f"Test name/id '{options.testname}' not recognized. Specify a valid test case.")
|
||||
quit(1)
|
||||
|
||||
# Parse remaining options
|
||||
global_log_level -= options.debug
|
||||
change_log_level(-options.debug)
|
||||
|
||||
# Now start the tests --- TODO: Inject Deauths before connecting with client...
|
||||
if options.ap:
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 0665c34816c64f50e8a7c3dd7b9b4134bd0d1992
|
||||
Subproject commit ef622fd62617e4375207c17a5ab4d8abc521b793
|
25
research/test-injection-puremon.py
Executable file
25
research/test-injection-puremon.py
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
from libwifi import *
|
||||
import argparse, time
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Test packet injection properties of a device.")
|
||||
parser.add_argument('inject', help="Interface to use to inject frames.")
|
||||
parser.add_argument('monitor', help="Interface to use to monitor for frames.")
|
||||
options = parser.parse_args()
|
||||
|
||||
subprocess.check_output(["rfkill", "unblock", "wifi"])
|
||||
|
||||
set_monitor_mode(options.inject)
|
||||
set_monitor_mode(options.monitor)
|
||||
|
||||
if get_channel(options.inject) != get_channel(options.monitor):
|
||||
log(ERROR, "Both devices are not on the same channel")
|
||||
quit(1)
|
||||
|
||||
log(STATUS, "Performing injection tests ...")
|
||||
test_injection(options.inject, options.monitor)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Loading…
Reference in New Issue
Block a user