fragattack: ability to test injection capabilities of device

This commit is contained in:
Mathy Vanhoef 2020-05-20 03:14:55 +04:00 committed by Mathy Vanhoef
parent 3331b80fb7
commit 173e11d400
4 changed files with 175 additions and 119 deletions

97
research/NOTES.md Normal file
View 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.

View File

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

View 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()