2020-02-27 07:07:19 -05:00
|
|
|
#!/usr/bin/env python3
|
2020-06-28 04:35:45 -04:00
|
|
|
# Copyright (c) 2020, Mathy Vanhoef <mathy.vanhoef@nyu.edu>
|
|
|
|
#
|
|
|
|
# This code may be distributed under the terms of the BSD license.
|
|
|
|
# See README for more details.
|
|
|
|
|
2020-07-26 04:00:22 -04:00
|
|
|
# Note that tests_*.py files are imported automatically
|
2020-10-07 05:44:23 -04:00
|
|
|
import glob, importlib, argparse
|
2020-05-28 09:10:37 -04:00
|
|
|
from fraginternals import *
|
2020-04-20 19:25:54 -04:00
|
|
|
|
2020-03-30 13:13:21 -04:00
|
|
|
# ----------------------------------- Main Function -----------------------------------
|
2020-03-01 19:03:08 -05:00
|
|
|
|
2020-03-07 21:10:26 -05:00
|
|
|
def cleanup():
|
|
|
|
daemon.stop()
|
2020-03-01 19:03:08 -05:00
|
|
|
|
2020-04-23 10:19:14 -04:00
|
|
|
def char2trigger(c):
|
|
|
|
if c == 'S': return Action.StartAuth
|
|
|
|
elif c == 'B': return Action.BeforeAuth
|
|
|
|
elif c == 'A': return Action.AfterAuth
|
|
|
|
elif c == 'C': return Action.Connected
|
|
|
|
else: raise Exception("Unknown trigger character " + c)
|
|
|
|
|
|
|
|
def stract2action(stract):
|
2020-07-30 09:53:46 -04:00
|
|
|
"""Parse a single trigger and action pair"""
|
|
|
|
|
2020-04-23 10:19:14 -04:00
|
|
|
if len(stract) == 1:
|
|
|
|
trigger = Action.Connected
|
|
|
|
c = stract[0]
|
|
|
|
else:
|
|
|
|
trigger = char2trigger(stract[0])
|
|
|
|
c = stract[1]
|
|
|
|
|
2020-04-22 19:14:35 -04:00
|
|
|
if c == 'I':
|
2020-04-23 10:19:14 -04:00
|
|
|
return Action(trigger, action=Action.GetIp)
|
2020-07-30 09:53:46 -04:00
|
|
|
elif c == 'F':
|
2020-04-23 10:19:14 -04:00
|
|
|
return Action(trigger, action=Action.Rekey)
|
2020-07-30 09:53:46 -04:00
|
|
|
elif c == 'R':
|
2020-04-23 15:43:47 -04:00
|
|
|
return Action(trigger, action=Action.Reconnect)
|
2020-04-22 19:14:35 -04:00
|
|
|
elif c == 'P':
|
2020-04-23 10:19:14 -04:00
|
|
|
return Action(trigger, enc=False)
|
2020-04-22 19:14:35 -04:00
|
|
|
elif c == 'E':
|
2020-04-23 10:19:14 -04:00
|
|
|
return Action(trigger, enc=True)
|
2020-04-24 09:56:32 -04:00
|
|
|
elif c == 'D':
|
2020-06-13 12:37:16 -04:00
|
|
|
# Note: the trigger condition of MetaDrop is ignored
|
2020-04-24 09:56:32 -04:00
|
|
|
return Action(meta_action=Action.MetaDrop)
|
2020-04-22 19:14:35 -04:00
|
|
|
|
|
|
|
raise Exception("Unrecognized action")
|
|
|
|
|
2020-07-30 09:53:46 -04:00
|
|
|
def str2actions(stractions, default):
|
|
|
|
"""Parse a list of trigger and action pairs"""
|
|
|
|
if stractions != None:
|
|
|
|
return [stract2action(stract) for stract in stractions.split(",")]
|
|
|
|
else:
|
|
|
|
return default
|
|
|
|
|
2020-04-29 18:40:41 -04:00
|
|
|
def prepare_tests(opt):
|
2020-08-12 04:31:03 -04:00
|
|
|
# --------------- Main Tests ---------------
|
|
|
|
|
2020-04-29 18:40:41 -04:00
|
|
|
stractions = opt.actions
|
|
|
|
if opt.testname == "ping":
|
2020-07-30 09:53:46 -04:00
|
|
|
actions = str2actions(stractions,
|
|
|
|
[Action(Action.Connected, action=Action.GetIp),
|
|
|
|
Action(Action.Connected, enc=True)])
|
2020-04-29 18:40:41 -04:00
|
|
|
test = PingTest(REQ_ICMP, actions, opt=opt)
|
2020-04-22 19:14:35 -04:00
|
|
|
|
2020-05-23 19:34:41 -04:00
|
|
|
elif opt.testname == "ping-frag-sep":
|
2020-04-25 17:29:31 -04:00
|
|
|
# Check if we can send frames in between fragments. The seperator by default uses a different
|
|
|
|
# QoS TID. The second fragment must use an incremental PN compared to the first fragment.
|
|
|
|
# So this also tests if the receivers uses a per-QoS receive replay counter. By overriding
|
|
|
|
# the TID you can check whether fragments are cached for multiple sequence numbers in one TID.
|
|
|
|
tid = 1 if stractions == None else int(stractions)
|
|
|
|
separator = Dot11(type="Data", subtype=8, SC=(33 << 4) | 0)/Dot11QoS(TID=tid)/LLC()/SNAP()
|
2020-04-22 19:14:35 -04:00
|
|
|
test = PingTest(REQ_ICMP,
|
|
|
|
[Action(Action.Connected, action=Action.GetIp),
|
|
|
|
Action(Action.Connected, enc=True),
|
2020-04-23 16:05:34 -04:00
|
|
|
Action(Action.Connected, enc=True, inc_pn=0)],
|
2020-04-29 18:40:41 -04:00
|
|
|
separate_with=separator, opt=opt)
|
2020-04-22 19:14:35 -04:00
|
|
|
|
2020-07-30 09:53:46 -04:00
|
|
|
elif opt.testname in ["eapol-inject", "eapol-inject-large"]:
|
|
|
|
large = opt.testname.endswith("-large")
|
2020-06-13 12:37:16 -04:00
|
|
|
test = ForwardTest(eapol=True, dst=stractions, large=large)
|
2020-04-29 18:40:41 -04:00
|
|
|
|
2020-07-30 09:53:46 -04:00
|
|
|
elif opt.testname in ["eapol-amsdu", "eapol-amsdu-bad"]:
|
|
|
|
freebsd = opt.testname.endswith("-bad")
|
|
|
|
actions = str2actions(stractions,
|
|
|
|
[Action(Action.StartAuth, enc=False),
|
|
|
|
Action(Action.StartAuth, enc=False)])
|
2020-06-07 08:08:04 -04:00
|
|
|
test = EapolAmsduTest(REQ_ICMP, actions, freebsd, opt)
|
2020-04-23 11:50:21 -04:00
|
|
|
|
2020-05-23 19:34:41 -04:00
|
|
|
elif opt.testname == "linux-plain":
|
2020-04-25 17:29:31 -04:00
|
|
|
decoy_tid = None if stractions == None else int(stractions)
|
|
|
|
test = LinuxTest(REQ_ICMP, decoy_tid)
|
2020-04-23 12:06:04 -04:00
|
|
|
|
2020-08-12 04:31:03 -04:00
|
|
|
elif opt.testname in ["amsdu-inject", "amsdu-inject-bad"]:
|
|
|
|
malformed = opt.testname.endswith("-bad")
|
|
|
|
test = AmsduInject(REQ_ICMP, malformed)
|
|
|
|
|
2020-07-30 09:53:46 -04:00
|
|
|
elif opt.testname == "eapfrag":
|
|
|
|
actions = str2actions(stractions,
|
|
|
|
[Action(Action.StartAuth, enc=False),
|
|
|
|
Action(Action.StartAuth, enc=False)])
|
|
|
|
test = BcastEapFragTest(REQ_ICMP, actions, opt.bcast_dst)
|
2020-04-23 11:50:21 -04:00
|
|
|
|
2020-08-12 04:31:03 -04:00
|
|
|
elif opt.testname == "wep-mixed-key":
|
|
|
|
log(WARNING, "Cannot predict WEP key reotation. Fragment may time out, use very short key rotation!", color="orange")
|
|
|
|
test = PingTest(REQ_ICMP,
|
|
|
|
[Action(Action.Connected, action=Action.GetIp),
|
|
|
|
Action(Action.Connected, enc=True),
|
|
|
|
# On a WEP key rotation we get a Connected event. So wait for that.
|
|
|
|
Action(Action.AfterAuth, enc=True),
|
|
|
|
])
|
|
|
|
|
|
|
|
# --------------- Research Tests ---------------
|
|
|
|
|
|
|
|
elif opt.testname == "forward":
|
|
|
|
test = ForwardTest(eapol=False, dst=stractions)
|
|
|
|
|
2020-05-23 19:34:41 -04:00
|
|
|
elif opt.testname == "qca-test":
|
2020-04-23 11:50:21 -04:00
|
|
|
test = QcaDriverTest()
|
|
|
|
|
2020-05-23 19:34:41 -04:00
|
|
|
elif opt.testname == "qca-split":
|
2020-04-23 11:50:21 -04:00
|
|
|
test = QcaTestSplit()
|
|
|
|
|
2020-05-23 19:34:41 -04:00
|
|
|
elif opt.testname == "qca-rekey":
|
2020-04-23 11:50:21 -04:00
|
|
|
test = QcaDriverRekey()
|
|
|
|
|
2020-04-27 11:47:15 -04:00
|
|
|
# No valid test ID/name was given
|
|
|
|
else: return None
|
|
|
|
|
2020-04-23 12:26:00 -04:00
|
|
|
# If requested, override delay and inc_pn parameters in the test.
|
2020-04-29 18:40:41 -04:00
|
|
|
test.set_general_options(opt.delay, opt.inc_pn)
|
2020-04-23 10:19:14 -04:00
|
|
|
|
2020-04-23 12:26:00 -04:00
|
|
|
# If requested, override the ptype
|
2020-04-29 18:40:41 -04:00
|
|
|
if opt.ptype != None:
|
2020-04-23 12:26:00 -04:00
|
|
|
if not hasattr(test, "ptype"):
|
2020-04-25 17:29:31 -04:00
|
|
|
log(WARNING, "Cannot override request type of the selected test.")
|
2020-04-23 12:26:00 -04:00
|
|
|
quit(1)
|
2020-04-29 18:40:41 -04:00
|
|
|
test.ptype = opt.ptype
|
2020-04-23 12:26:00 -04:00
|
|
|
|
2020-03-30 13:53:56 -04:00
|
|
|
return test
|
2020-03-01 19:03:08 -05:00
|
|
|
|
2020-04-23 12:26:00 -04:00
|
|
|
def args2ptype(args):
|
|
|
|
# Only one of these should be given
|
2020-04-29 21:38:50 -04:00
|
|
|
if args.arp + args.dhcp + args.icmp + args.ipv6 > 1:
|
|
|
|
log(STATUS, "You cannot combine --arp, --dhcp, --ipv6, or --icmp. Please only supply one of them.")
|
2020-04-23 12:26:00 -04:00
|
|
|
quit(1)
|
|
|
|
|
|
|
|
if args.arp: return REQ_ARP
|
|
|
|
if args.dhcp: return REQ_DHCP
|
|
|
|
if args.icmp: return REQ_ICMP
|
2020-04-29 21:38:50 -04:00
|
|
|
if args.ipv6: return REQ_ICMPv6_RA
|
2020-07-30 09:54:04 -04:00
|
|
|
if args.udp: return REQ_UDP
|
2020-04-23 12:26:00 -04:00
|
|
|
|
|
|
|
return None
|
|
|
|
|
2020-04-23 21:15:15 -04:00
|
|
|
def args2msdu(args):
|
|
|
|
# Only one of these should be given
|
2020-08-04 23:17:59 -04:00
|
|
|
if args.amsdu + args.amsdu_fake > 1:
|
|
|
|
log(STATUS, "You cannot combine --amsdu and --amsdu-fake. Please only supply one of them.")
|
2020-04-23 21:15:15 -04:00
|
|
|
quit(1)
|
|
|
|
|
2020-05-28 21:02:19 -04:00
|
|
|
if args.amsdu: return 1
|
2020-08-04 23:17:59 -04:00
|
|
|
if args.amsdu_fake: return 2
|
2020-04-23 21:15:15 -04:00
|
|
|
|
|
|
|
return None
|
|
|
|
|
2020-02-27 07:07:19 -05:00
|
|
|
if __name__ == "__main__":
|
2020-04-15 10:27:22 -04:00
|
|
|
parser = argparse.ArgumentParser(description="Test for fragmentation vulnerabilities.")
|
|
|
|
parser.add_argument('iface', help="Interface to use for the tests.")
|
|
|
|
parser.add_argument('testname', help="Name or identifier of the test to run.")
|
2020-04-22 19:14:35 -04:00
|
|
|
parser.add_argument('actions', nargs='?', help="Optional textual descriptions of actions")
|
2020-05-11 12:57:46 -04:00
|
|
|
parser.add_argument('--inject', default=None, help="Interface to use to inject frames.")
|
2020-05-19 19:14:55 -04:00
|
|
|
parser.add_argument('--inject-test', default=None, help="Use given interface to test injection through monitor interface.")
|
2020-06-27 09:27:46 -04:00
|
|
|
parser.add_argument('--inject-test-postauth', default=None, help="Same as --inject-test but run the test after authenticating.")
|
2020-05-12 10:33:44 -04:00
|
|
|
parser.add_argument('--hwsim', default=None, help="Use provided interface in monitor mode, and simulate AP/client through hwsim.")
|
2020-04-15 10:27:22 -04:00
|
|
|
parser.add_argument('--ip', help="IP we as a sender should use.")
|
|
|
|
parser.add_argument('--peerip', help="IP of the device we will test.")
|
|
|
|
parser.add_argument('--ap', default=False, action='store_true', help="Act as an AP to test clients.")
|
|
|
|
parser.add_argument('--debug', type=int, default=0, help="Debug output level.")
|
2020-04-23 15:43:47 -04:00
|
|
|
parser.add_argument('--delay', type=float, default=0, help="Delay between fragments in certain tests.")
|
2020-04-23 16:05:16 -04:00
|
|
|
parser.add_argument('--inc-pn', type=int, help="To test non-sequential packet number in fragments.")
|
2020-05-28 21:02:19 -04:00
|
|
|
parser.add_argument('--amsdu', default=False, action='store_true', help="Encapsulate pings in an A-MSDU frame.")
|
2020-08-04 23:17:59 -04:00
|
|
|
parser.add_argument('--amsdu-fake', default=False, action='store_true', help="Set A-MSDU flag but include normal payload.")
|
2020-10-20 06:33:09 -04:00
|
|
|
parser.add_argument('--amsdu-spp', '--amsdu-ssp', default=False, action='store_true', help="Force authentication of QoS A-MSDU flag.")
|
2020-04-23 12:26:00 -04:00
|
|
|
parser.add_argument('--arp', default=False, action='store_true', help="Override default request with ARP request.")
|
|
|
|
parser.add_argument('--dhcp', default=False, action='store_true', help="Override default request with DHCP discover.")
|
|
|
|
parser.add_argument('--icmp', default=False, action='store_true', help="Override default request with ICMP ping request.")
|
2020-04-29 21:38:50 -04:00
|
|
|
parser.add_argument('--ipv6', default=False, action='store_true', help="Override default request with ICMPv6 router advertisement.")
|
2020-07-30 09:54:04 -04:00
|
|
|
# TODO: Test the --udp option more
|
|
|
|
parser.add_argument('--udp', type=int, default=None, help="Override default request with UDP packet to the given port.")
|
2020-04-29 21:38:50 -04:00
|
|
|
parser.add_argument('--no-dhcp', default=False, action='store_true', help="Do not reply to DHCP requests as an AP.")
|
2020-04-27 11:47:15 -04:00
|
|
|
parser.add_argument('--icmp-size', type=int, default=None, help="Second to wait after AfterAuth before triggering Connected event")
|
2020-04-29 18:40:41 -04:00
|
|
|
parser.add_argument('--padding', type=int, default=None, help="Add padding data to ARP/DHCP/ICMP requests.")
|
2020-04-23 16:05:34 -04:00
|
|
|
parser.add_argument('--rekey-request', default=False, action='store_true', help="Actively request PTK rekey as client.")
|
2020-04-24 14:02:05 -04:00
|
|
|
parser.add_argument('--rekey-plaintext', default=False, action='store_true', help="Do PTK rekey with plaintext EAPOL frames.")
|
2020-04-25 17:29:31 -04:00
|
|
|
parser.add_argument('--rekey-early-install', default=False, action='store_true', help="Install PTK after sending Msg3 during rekey.")
|
2020-04-24 15:02:05 -04:00
|
|
|
parser.add_argument('--full-reconnect', default=False, action='store_true', help="Reconnect by deauthenticating first.")
|
2020-06-07 08:08:04 -04:00
|
|
|
parser.add_argument('--bcast-ra', default=False, action='store_true', help="Send pings using broadcast *receiver* address (= addr1).")
|
|
|
|
parser.add_argument('--bcast-dst', default=False, action='store_true', help="Send pings using broadcast *destination* when to AP ().")
|
2020-06-19 15:24:12 -04:00
|
|
|
# TODO: Properly test the --bad-mic option
|
|
|
|
parser.add_argument('--bad-mic', default=False, action='store_true', help="Send pings using an invalid authentication tag.")
|
2020-04-25 17:29:31 -04:00
|
|
|
parser.add_argument('--pn-per-qos', default=False, action='store_true', help="Use separate Tx packet counter for each QoS TID.")
|
2020-04-26 12:51:39 -04:00
|
|
|
parser.add_argument('--freebsd-cache', default=False, action='store_true', help="Sent EAP(OL) frames as (malformed) broadcast EAPOL/A-MSDUs.")
|
2020-06-10 19:09:35 -04:00
|
|
|
parser.add_argument('--connected-delay', type=float, default=1, help="Second to wait after AfterAuth before triggering Connected event")
|
2020-04-29 20:09:42 -04:00
|
|
|
parser.add_argument('--to-self', default=False, action='store_true', help="Send ARP/DHCP/ICMP with same src and dst MAC address.")
|
2020-07-26 14:55:22 -04:00
|
|
|
parser.add_argument('--no-drivercheck', default=False, action='store_true', help="Don't check if patched drivers are being used.")
|
2020-04-29 18:40:41 -04:00
|
|
|
options = parser.parse_args()
|
|
|
|
|
2020-05-11 14:54:13 -04:00
|
|
|
# Default value for options that should not be command line parameters
|
2020-05-22 09:24:23 -04:00
|
|
|
options.inject_mf_workaround = False
|
2020-05-11 14:54:13 -04:00
|
|
|
|
2020-04-29 18:40:41 -04:00
|
|
|
# Sanity check and convert some arguments to more usable form
|
|
|
|
options.ptype = args2ptype(options)
|
|
|
|
options.as_msdu = args2msdu(options)
|
|
|
|
|
2020-06-27 09:27:46 -04:00
|
|
|
# Make the --inject-test-postauth flags easier to check
|
|
|
|
if options.inject_test_postauth != None:
|
|
|
|
options.inject_test = options.inject_test_postauth
|
|
|
|
options.inject_test_postauth = True
|
|
|
|
|
|
|
|
else:
|
|
|
|
options.inject_test_postauth = False
|
|
|
|
|
2020-06-28 01:49:07 -04:00
|
|
|
# Dynamically import tests depending on their availability in the directory
|
|
|
|
for test in glob("tests_*.py"):
|
|
|
|
module = importlib.import_module(test[:-3])
|
|
|
|
globals().update(
|
|
|
|
{n: getattr(module, n) for n in module.__all__} if hasattr(module, '__all__')
|
|
|
|
else
|
|
|
|
{k: v for (k, v) in module.__dict__.items() if not k.startswith('_')
|
|
|
|
})
|
|
|
|
|
2020-04-29 18:40:41 -04:00
|
|
|
# Construct the test
|
|
|
|
options.test = prepare_tests(options)
|
2020-04-27 11:47:15 -04:00
|
|
|
if options.test == None:
|
2020-08-06 23:44:50 -04:00
|
|
|
log(STATUS, f"Test name '{options.testname}' not recognized. Specify a valid test case.")
|
2020-04-27 11:47:15 -04:00
|
|
|
quit(1)
|
2020-03-01 19:03:08 -05:00
|
|
|
|
2020-03-24 08:52:31 -04:00
|
|
|
# Parse remaining options
|
2020-05-19 19:14:55 -04:00
|
|
|
change_log_level(-options.debug)
|
2020-03-01 19:03:08 -05:00
|
|
|
|
2020-04-19 20:15:44 -04:00
|
|
|
# Now start the tests --- TODO: Inject Deauths before connecting with client...
|
2020-04-29 18:40:41 -04:00
|
|
|
if options.ap:
|
2020-03-07 21:10:26 -05:00
|
|
|
daemon = Authenticator(options)
|
|
|
|
else:
|
|
|
|
daemon = Supplicant(options)
|
2020-02-27 07:07:19 -05:00
|
|
|
atexit.register(cleanup)
|
2020-03-07 21:10:26 -05:00
|
|
|
daemon.run()
|
2020-02-27 07:07:19 -05:00
|
|
|
|