From 7f93c1cec788f1c41f6de4b3d59ba0d4490d3ac7 Mon Sep 17 00:00:00 2001 From: Mathy Vanhoef Date: Sat, 8 May 2021 19:35:46 +0400 Subject: [PATCH] fragattacks: directly track libwifi and not as submodule This will make it easier for users to clone the repository and will assure that they always use the correct version of libwifi. --- .gitmodules | 3 - research/libwifi | 1 - research/libwifi/README.md | 1 + research/libwifi/__init__.py | 4 + research/libwifi/crypto.py | 169 ++++++++ research/libwifi/dragonfly.py | 334 +++++++++++++++ research/libwifi/injectiontest.py | 268 ++++++++++++ research/libwifi/mschap.py | 77 ++++ research/libwifi/run-tests.sh | 3 + research/libwifi/tests/test_crypto.py | 41 ++ research/libwifi/tests/test_dragonfly.py | 83 ++++ research/libwifi/tests/test_mschap.py | 14 + research/libwifi/wifi.py | 494 +++++++++++++++++++++++ 13 files changed, 1488 insertions(+), 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 research/libwifi create mode 100644 research/libwifi/README.md create mode 100644 research/libwifi/__init__.py create mode 100644 research/libwifi/crypto.py create mode 100644 research/libwifi/dragonfly.py create mode 100644 research/libwifi/injectiontest.py create mode 100644 research/libwifi/mschap.py create mode 100755 research/libwifi/run-tests.sh create mode 100644 research/libwifi/tests/test_crypto.py create mode 100644 research/libwifi/tests/test_dragonfly.py create mode 100644 research/libwifi/tests/test_mschap.py create mode 100644 research/libwifi/wifi.py diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ea7bac21e..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "research/libwifi"] - path = research/libwifi - url = https://github.com/vanhoefm/libwifi.git diff --git a/research/libwifi b/research/libwifi deleted file mode 160000 index 35b3f4faf..000000000 --- a/research/libwifi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 35b3f4fafcbc0227c4d800e941bccee4789f9f33 diff --git a/research/libwifi/README.md b/research/libwifi/README.md new file mode 100644 index 000000000..e291f614e --- /dev/null +++ b/research/libwifi/README.md @@ -0,0 +1 @@ +This is an experimental library that I internally use in some projects. One day this might be useful for others too. diff --git a/research/libwifi/__init__.py b/research/libwifi/__init__.py new file mode 100644 index 000000000..3bc67f98c --- /dev/null +++ b/research/libwifi/__init__.py @@ -0,0 +1,4 @@ +from .wifi import * +from .dragonfly import * +from .crypto import * +from .injectiontest import * diff --git a/research/libwifi/crypto.py b/research/libwifi/crypto.py new file mode 100644 index 000000000..cb7482d0a --- /dev/null +++ b/research/libwifi/crypto.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +import struct, binascii +from .wifi import * +#from binascii import a2b_hex +#from struct import unpack,pack + +from Crypto.Cipher import AES, ARC4 +from scapy.layers.dot11 import Dot11, Dot11CCMP, Dot11QoS + +import zlib + +def pn2bytes(pn): + pn_bytes = [0] * 6 + for i in range(6): + pn_bytes[i] = pn & 0xFF + pn >>= 8 + return pn_bytes + +def pn2bin(pn): + return struct.pack(">Q", pn)[2:] + +def dot11ccmp_get_pn(p): + pn = p.PN5 + pn = (pn << 8) | p.PN4 + pn = (pn << 8) | p.PN3 + pn = (pn << 8) | p.PN2 + pn = (pn << 8) | p.PN1 + pn = (pn << 8) | p.PN0 + return pn + +def ccmp_get_nonce(priority, addr, pn): + return struct.pack("B", priority) + addr2bin(addr) + pn2bin(pn) + +def ccmp_get_aad(p, amsdu_spp=False): + # FC field with masked values + fc = raw(p)[:2] + fc = struct.pack("I", pn)[1:] + cipher = ARC4.new(iv + key) + ciphertext = cipher.encrypt(payload) + + # Construct packet ourselves to avoid scapy bugs + newp = p/iv/struct.pack(" addr2 else addr2 + addr1 + + for counter in range(1, 100): + hash_data = str2bytes(password) + struct.pack("= curve.p: + continue + x = Integer(pwd_value) + + y_sqr = (x**3 - x * 3 + curve.b) % curve.p + if legendre_symbol(y_sqr, curve.p) != 1: + continue + + y = y_sqr.sqrt(curve.p) + y_bit = getord(pwd_seed[-1]) & 1 + if y & 1 == y_bit: + return ECC.EccPoint(x, y, curve_name) + else: + return ECC.EccPoint(x, curve.p - y, curve_name) + + +# TODO: Use this somewhere??? +def calc_k_kck_pmk(pwe, peer_element, peer_scalar, my_rand, my_scalar): + k = ((pwe * peer_scalar + peer_element) * my_rand).x + + keyseed = HMAC256(b"\x00" * 32, int_to_data(k)) + kck_and_pmk = KDF_Length(keyseed, "SAE KCK and PMK", + int_to_data((my_scalar + peer_scalar) % secp256r1_r), 512) + kck = kck_and_pmk[0:32] + pmk = kck_and_pmk[32:] + + return k, kck, pmk + + +def calculate_confirm_hash(kck, send_confirm, scalar, element, peer_scalar, peer_element): + return HMAC256(kck, struct.pack(" 1 + + # COMMIT-ELEMENT = inverse(mask * PWE) + temp = self.pwe * mask + self.element = ECC.EccPoint(temp.x, Integer(secp256r1_p) - temp.y) + + auth = build_sae_commit(self.srcaddr, self.dstaddr, self.scalar, self.element) + sendp(RadioTap()/auth) + + def process_commit(self, p): + payload = str(p[Dot11Auth].payload) + + group_id = struct.unpack(" 1 else b"" + hash_data += struct.pack(">H", i) + str2bytes(label) + struct.pack(">H", length) + digest = HMAC256(data, hash_data) + result += digest + + result = result[:num_bytes] + if length % 8 != 0: + num_clear = 8 - (length % 8) + trailbyte = result[-1] >> num_clear << num_clear + result = result[:-1] + struct.pack(">B", trailbyte) + return result + + +def derive_pwe_ecc_eappwd(password, peer_id, server_id, token, curve_name="p256", info=None): + curve = ECC._curves[curve_name] + bits = curve.modulus_bits + + hash_pw = struct.pack(">I", token) + str2bytes(peer_id + server_id + password) + for counter in range(1, 100): + hash_data = hash_pw + struct.pack("> (8 - (521 % 8)) + + if pwd_value >= curve.p: + continue + x = Integer(pwd_value) + + log(DEBUG, "X-candidate: %x" % x) + y_sqr = (x**3 - x * 3 + curve.b) % curve.p + if legendre_symbol(y_sqr, curve.p) != 1: + continue + + y = y_sqr.sqrt(curve.p) + y_bit = getord(pwd_seed[-1]) & 1 + if y & 1 == y_bit: + if not info is None: info["counter"] = counter + return ECC.EccPoint(x, y, curve_name) + else: + if not info is None: info["counter"] = counter + return ECC.EccPoint(x, curve.p - y, curve_name) + + +def calculate_confirm_eappwd(k, element1, scalar1, element2, scalar2, group_num=19, rand_func=1, prf=1): + hash_data = int_to_data(k) + hash_data += point_to_data(element1) + hash_data += int_to_data(scalar1) + hash_data += point_to_data(element2) + hash_data += int_to_data(scalar2) + hash_data += struct.pack(">HBB", group_num, rand_func, prf) + confirm = HMAC256(b"\x00" * 32, hash_data) + return confirm + +# ----------------------- Fuzzing/Testing --------------------------------- + +def inject_sae_auth(srcaddr, bssid): + p = Dot11(addr1=bssid, addr2=srcaddr, addr3=bssid) + p = p/Dot11Auth(algo=3, seqnum=1, status=0) + + group_id = 19 + scalar = 0 + element_x = 0 + element_y = 0 + p = p/Raw(struct.pack(" +# +# This code may be distributed under the terms of the BSD license. +# See README for more details. + +from scapy.all import * +from .wifi import * + +FLAG_FAIL, FLAG_NOCAPTURE = [2**i for i in range(2)] + +#### Utility #### + +def get_nearby_ap_addr(sin): + # If this interface itself is also hosting an AP, the beacons transmitted by it might be + # returned as well. We filter these out by the condition `p.dBm_AntSignal != None`. + beacons = list(sniff(opened_socket=sin, timeout=0.5, lfilter=lambda p: (Dot11 in p or Dot11FCS in p) \ + and p.type == 0 and p.subtype == 8 \ + and p.dBm_AntSignal != None)) + if len(beacons) == 0: + return None, None + beacons.sort(key=lambda p: p.dBm_AntSignal, reverse=True) + return beacons[0].addr2, get_ssid(beacons[0]) + +def inject_and_capture(sout, sin, p, count=0, retries=1): + # Append unique label to recognize injected frame + label = b"AAAA" + struct.pack(">II", random.randint(0, 2**32), random.randint(0, 2**32)) + toinject = p/Raw(label) + + attempt = 0 + while True: + log(DEBUG, "Injecting test frame: " + repr(toinject)) + sout.send(RadioTap(present="TXFlags", TXFlags="NOSEQ+ORDER")/toinject) + + # TODO:Move this to a shared socket interface? + # Note: this workaround for Intel is only needed if the fragmented frame is injected using + # valid MAC addresses. But for simplicity just execute it after any fragmented frame. + if sout.mf_workaround and toinject.FCfield & Dot11(FCfield="MF").FCfield != 0: + sout.send(RadioTap(present="TXFlags", TXFlags="NOSEQ+ORDER")/Dot11()) + log(DEBUG, "Sending dummy frame after injecting frame with MF flag set") + + # 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=1, count=count, lfilter=lambda p: p != None and label in raw(p)) + + if len(packets) > 0 or attempt >= retries: + break + + log(STATUS, " Unable to capture injected frame, retrying.") + attempt += 1 + + return packets + + +#### Injection tests #### + +def test_injection_fragment(sout, sin, ref): + log(STATUS, "--- Testing injection of fragmented frame using (partly) valid MAC addresses") + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, type=2, subtype=8, SC=33<<4) + p = p/Dot11QoS(TID=2)/LLC()/SNAP()/EAPOL()/EAP() + p.FCfield |= Dot11(FCfield="MF").FCfield + captured = inject_and_capture(sout, sin, p, count=1) + if len(captured) == 0: + log(ERROR, "[-] Unable to inject frame with More Fragment flag using (partly) valid MAC addresses.") + else: + log(STATUS, "[+] Frame with More Fragment flag using (partly) valid MAC addresses can be injected.", color="green") + return FLAG_FAIL if len(captured) == 0 else 0 + +def test_packet_injection(sout, sin, p, test_func, frametype, msgfail): + """Check if given property holds of all injected frames""" + packets = inject_and_capture(sout, sin, p, count=1) + if len(packets) < 1: + log(ERROR, f"[-] Unable to capture injected {frametype}.") + return FLAG_NOCAPTURE + if not all([test_func(cap) for cap in packets]): + log(ERROR, f"[-] " + msgfail.format(frametype=frametype)) + return FLAG_FAIL + log(STATUS, f" Properly captured injected {frametype}.") + return 0 + +def test_injection_fields(sout, sin, ref, strtype): + log(STATUS, f"--- Testing injection of fields using {strtype}") + status = 0 + + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, addr3=ref.addr3, type=2, SC=30<<4)/LLC()/SNAP()/EAPOL()/EAP() + status |= test_packet_injection(sout, sin, p, lambda cap: EAPOL in cap, f"EAPOL frame with {strtype}", + "Scapy thinks injected {frametype} is a different frame?") + + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, addr3=ref.addr3, type=2, SC=31<<4) + status |= test_packet_injection(sout, sin, p, lambda cap: cap.SC == p.SC, f"empty data frame with {strtype}", + "Sequence number of injected {frametype} is being overwritten!") + + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, addr3=ref.addr3, type=2, SC=(32<<4)|1) + status |= test_packet_injection(sout, sin, p, lambda cap: (cap.SC & 0xf) == 1, f"fragmented empty data frame with {strtype}", + "Fragment number of injected {frametype} is being overwritten!") + + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, addr3=ref.addr3, type=2, subtype=8, SC=33<<4)/Dot11QoS(TID=2) + status |= test_packet_injection(sout, sin, p, lambda cap: cap.TID == p.TID, f"empty QoS data frame with {strtype}", + "QoS TID of injected {frametype} is being overwritten!") + + p = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, addr3=ref.addr3, type=2, subtype=8, SC=33<<4)/Dot11QoS(TID=2)/Raw("BBBB") + set_amsdu(p[Dot11QoS]) + status |= test_packet_injection(sout, sin, p, \ + lambda cap: cap.TID == p.TID and is_amsdu(cap) and b"BBBB" in raw(cap), \ + f"A-MSDU frame with {strtype}", "A-MSDU frame is not properly injected!") + + if status == 0: log(STATUS, f"[+] All tested fields are properly injected when using {strtype}.", color="green") + return status + +def test_injection_order(sout, sin, ref, strtype, retries=1): + log(STATUS, f"--- Testing order of injected QoS frames using {strtype}") + + label = b"AAAA" + struct.pack(">II", random.randint(0, 2**32), random.randint(0, 2**32)) + p2 = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, type=2, subtype=8, SC=33<<4)/Dot11QoS(TID=2) + p6 = Dot11(FCfield=ref.FCfield, addr1=ref.addr1, addr2=ref.addr2, type=2, subtype=8, SC=33<<4)/Dot11QoS(TID=6) + + for i in range(retries + 1): + # First frame causes Tx queue to be busy. Next two frames tests if frames are reordered. + for p in [p2] * 4 + [p6]: + sout.send(RadioTap(present="TXFlags", TXFlags="NOSEQ+ORDER")/p/Raw(label)) + + packets = sniff(opened_socket=sin, timeout=1.5, lfilter=lambda p: Dot11QoS in p and label in raw(p)) + tids = [p[Dot11QoS].TID for p in packets] + log(STATUS, "Captured TIDs: " + str(tids)) + + # Sanity check the captured TIDs, and then analyze the results + if not (2 in tids and 6 in tids): + log(STATUS, f"We didn't capture all injected QoS TID frames, retrying.") + else: + break + + if not (2 in tids and 6 in tids): + log(ERROR, f"[-] We didn't capture all injected QoS TID frames with {strtype}. Test failed.") + return FLAG_NOCAPTURE + elif tids != sorted(tids): + log(ERROR, f"[-] Frames with different QoS TIDs are reordered during injection with {strtype}.") + return FLAG_FAIL + else: + log(STATUS, f"[+] Frames with different QoS TIDs are not reordered during injection with {strtype}.", color="green") + return 0 + +def test_injection_ack(sout, sin, addr1, addr2): + suspicious = False + test_fail = False + + # Test number of retransmissions + p = Dot11(FCfield="to-DS", addr1="00:11:00:00:02:01", addr2="00:11:00:00:02:01", type=2, SC=33<<4) + num = len(inject_and_capture(sout, sin, p, retries=1)) + log(STATUS, f"Injected frames seem to be (re)transitted {num} times") + if num == 0: + log(ERROR, "Couldn't capture injected frame. Please restart the test.") + test_fail = True + elif num == 1: + log(WARNING, "Injected frames don't seem to be retransmitted!") + suspicious = True + + # Test ACK towards an unassigned MAC address + p = Dot11(FCfield="to-DS", addr1=addr1, addr2="00:22:00:00:00:01", type=2, SC=33<<4) + num = len(inject_and_capture(sout, sin, p, retries=1)) + log(STATUS, f"Captured {num} (re)transmitted frames to the AP when using a spoofed sender address") + if num == 0: + log(ERROR, "Couldn't capture injected frame. Please restart the test.") + test_fail = True + if num > 2: + log(STATUS, " => Acknowledged frames with a spoofed sender address are still retransmitted. This has low impact.") + + # Test ACK towards an assigned MAC address + p = Dot11(FCfield="to-DS", addr1=addr1, addr2=addr2, type=2, SC=33<<4) + num = len(inject_and_capture(sout, sin, p, retries=1)) + log(STATUS, f"Captured {num} (re)transmitted frames to the AP when using the real sender address") + if num == 0: + log(ERROR, "Couldn't capture injected frame. Please restart the test.") + test_fail = True + elif num > 2: + log(STATUS, " => Acknowledged frames with real sender address are still retransmitted. This might impact time-sensitive tests.") + suspicious = True + + if suspicious: + log(WARNING, "[-] Retransmission behaviour isn't ideal. This test can be unreliable (e.g. due to background noise).") + elif not test_fail: + log(STATUS, "[+] Retransmission behaviour is good. This test can be unreliable (e.g. due to background noise).", color="green") + + +#### Main test function #### + +def test_injection(iface_out, iface_in=None, peermac=None, ownmac=None, testack=True): + status = 0 + + # We start monitoring iface_in already so injected frame won't be missed + sout = L2Socket(type=ETH_P_ALL, iface=iface_out) + driver_out = get_device_driver(iface_out) + + # Workaround to properly inject fragmented frames (and prevent it from blocking Tx queue). + sout.mf_workaround = driver_out in ["iwlwifi", "ath9k_htc"] + if sout.mf_workaround: + log(WARNING, f"Detected {driver_out}, using workaround to reliably inject fragmented frames.") + + # Print out what we are tested. Abort if the driver is known not to support a self-test. + log(STATUS, f"Injection test: using {iface_out} ({driver_out}) to inject frames") + if iface_in == None: + log(WARNING, f"Injection selftest: also using {iface_out} to capture frames. This means the tests can detect if the kernel") + log(WARNING, f" interferes with injection, but it cannot check the behaviour of the device itself.") + if driver_out in ["mt76x2u"]: + log(WARNING, f" WARNING: self-test with the {driver_out} driver can be unreliable.") + elif not driver_out in ["iwlwifi", "ath9k_htc"]: + log(WARNING, f" WARNING: it is unknown whether a self-test works with the {driver_out} driver.") + + sin = sout + else: + driver_in = get_device_driver(iface_in) + log(STATUS, f"Injection test: using {iface_in} ({driver_in}) to capture frames") + sin = L2Socket(type=ETH_P_ALL, iface=iface_in) + + # Injection using the "own" MAC address is mainly a problem when using a second virtual + # interface for injection when the first interface is used as client or AP. We want to + # test injection when using the MAC address of the client or AP. The caller should supply + # this address because the MAC address of the second virtual interface may be different + # from the MAC address used by the client or AP. Only use the MAC address of sout.iface + # if no "own" address is supplied by the caller. + if ownmac == None: + ownmac = get_macaddress(sout.iface) + + # Some devices only properly inject frames when either the to-DS or from-DS flag is set, + # so set one of them as well. + spoofed = Dot11(FCfield="from-DS", addr1="00:11:00:00:02:01", addr2="00:22:00:00:02:01") + valid = Dot11(FCfield="from-DS", addr1=peermac, addr2=ownmac) + + # This tests basic injection capabilities + status |= test_injection_fragment(sout, sin, valid) + + # Perform some actual injection tests + status |= test_injection_fields(sout, sin, spoofed, "spoofed MAC addresses") + status |= test_injection_fields(sout, sin, valid, "(partly) valid MAC addresses") + status |= test_injection_order(sout, sin, spoofed, "spoofed MAC addresses") + status |= test_injection_order(sout, sin, valid, "(partly) valid MAC addresses") + + # Acknowledgement behaviour tests + if iface_in != None and testack: + # We search for an AP on the interface that injects frames because: + # 1. In mixed managed/monitor mode, we will otherwise detect our own AP on the sout interface + # 2. If sout interface "sees" the AP this assure it will also receive its ACK frames + # 3. The given peermac might be a client that goes into sleep mode + channel = get_channel(sin.iface) + log(STATUS, f"--- Searching for AP on channel {channel} to test ACK behaviour.") + apmac, ssid = get_nearby_ap_addr(sout) + if apmac == None and peermac == None: + raise IOError("Unable to find nearby AP to test injection") + elif apmac == None: + log(WARNING, f"Unable to find AP. Try a different channel? Testing ACK behaviour with peer {peermac}.") + destmac = peermac + else: + log(STATUS, f"Testing ACK behaviour by injecting frames to AP {ssid} ({apmac}).") + destmac = apmac + test_injection_ack(sout, sin, addr1=destmac, addr2=ownmac) + + # Show a summary of results/advice + log(STATUS, "") + if status == 0: + log(STATUS, "==> The most important tests have been passed successfully!", color="green") + if status & FLAG_NOCAPTURE != 0: + log(WARNING, f"==> Failed to capture some frames. Try another channel or use another monitoring device.") + if status & FLAG_FAIL !=0 : + log(ERROR, f"==> Some tests failed. Are you using patched drivers/firmware?") + + sout.close() + sin.close() + diff --git a/research/libwifi/mschap.py b/research/libwifi/mschap.py new file mode 100644 index 000000000..f0afcbc78 --- /dev/null +++ b/research/libwifi/mschap.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import binascii, struct +from Crypto.Hash import MD4, SHA +from Crypto.Cipher import DES + + +def des_encrypt(clear, key, offset): + cNext = 0 + cWorking = 0 + hexKey = {} + + for x in range(0,8): + cWorking = 0xFF & key[x + offset] + hexKey[x] = ((cWorking >> x) | cNext | 1) & 0xFF + cWorking = 0xFF & key[x + offset] + cNext = ((cWorking << (7 - x))) + + newKey = b"" + for x in range(0, len(hexKey)): + newKey += struct.pack(">B", hexKey[x]) + + des = DES.new(newKey, DES.MODE_ECB) + return des.encrypt(clear) + +def challenge_hash(peer_challenge, authenticator_challenge, username): + challenge = SHA.new(peer_challenge + authenticator_challenge + username).digest() + return challenge[0:8] + +def nt_password_hash(password): + unicode_pw = password.encode("utf-16-le") + return MD4.new(unicode_pw).digest() + +def hash_nt_password_hash(password_hash): + md4 = MD4.new() + md4.update(password_hash) + return md4.digest() + +def challenge_response(challenge, pwhash): + # for some reason in python we need to pad an extra byte so that + # the offset works out correctly when we call DesEncrypt + pwhash += b'\x00' * (22 - len(pwhash)) + + response = b"" + for x in range(0, 3): + encrypted = des_encrypt(challenge, pwhash, x * 7) + response += encrypted + + return response + +def generate_nt_response_mschap2(authenticator_challenge, peer_challenge, username, password): + challenge = challenge_hash(peer_challenge, authenticator_challenge, username) + password_hash = nt_password_hash(password) + return challenge_response(challenge, password_hash) + +def generate_authenticator_response(password, nt_response, peer_challenge, authenticator_challenge, username): + magic1 = b"\x4D\x61\x67\x69\x63\x20\x73\x65\x72\x76\x65\x72\x20\x74\x6F\x20\x63\x6C\x69\x65\x6E\x74\x20\x73\x69\x67\x6E\x69\x6E\x67\x20\x63\x6F\x6E\x73\x74\x61\x6E\x74" + magic2 = b"\x50\x61\x64\x20\x74\x6F\x20\x6D\x61\x6B\x65\x20\x69\x74\x20\x64\x6F\x20\x6D\x6F\x72\x65\x20\x74\x68\x61\x6E\x20\x6F\x6E\x65\x20\x69\x74\x65\x72\x61\x74\x69\x6F\x6E" + + password_hash = nt_password_hash(password) + password_hash_hash = hash_nt_password_hash(password_hash) + + sha_hash = SHA.new() + sha_hash.update(password_hash_hash) + sha_hash.update(nt_response) + sha_hash.update(magic1) + digest = sha_hash.digest() + + challenge = challenge_hash(peer_challenge, authenticator_challenge, username) + + sha_hash = SHA.new() + sha_hash.update(digest) + sha_hash.update(challenge) + sha_hash.update(magic2) + digest = sha_hash.digest() + + return digest + diff --git a/research/libwifi/run-tests.sh b/research/libwifi/run-tests.sh new file mode 100755 index 000000000..f393cc2cb --- /dev/null +++ b/research/libwifi/run-tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd .. +python -m pytest $@ diff --git a/research/libwifi/tests/test_crypto.py b/research/libwifi/tests/test_crypto.py new file mode 100644 index 000000000..1cf68b9fe --- /dev/null +++ b/research/libwifi/tests/test_crypto.py @@ -0,0 +1,41 @@ +from libwifi.crypto import * + +def get_ciphertext_mic(encrypted): + dot11ccmp = encrypted[Dot11CCMP].payload + ciphertext = dot11ccmp.load + mic = dot11ccmp.payload.load + return ciphertext, mic + +def test_ccmp(): + payload = b"A" * 16 + ptk = b'\x00' * 48 + tk = ptk[32:48] + pn = 0 + + plaintext = Dot11(type="Data", subtype=0, FCfield="to-DS", addr1="11:11:11:11:11:11",\ + addr2="22:22:22:22:22:22", addr3="33:33:33:33:33:33", SC=0) + plaintext = plaintext/Raw(payload) + encrypted = encrypt_ccmp(plaintext, tk, pn) + ciphertext, mic = get_ciphertext_mic(encrypted) + assert ciphertext == bytes.fromhex("bedf2769dcdde9e002ab5b9df9342bc6") + assert mic == bytes.fromhex("3a49543fa1ecb1e0") + + plaintext.SC = 1 + encrypted = encrypt_ccmp(plaintext, tk, pn) + ciphertext, mic = get_ciphertext_mic(encrypted) + assert ciphertext == bytes.fromhex("bedf2769dcdde9e002ab5b9df9342bc6") + assert mic == bytes.fromhex("1fdbedc0538f98f2") + + plaintext.FCfield |= Dot11(FCfield="MF").FCfield + encrypted = encrypt_ccmp(plaintext, tk, pn) + ciphertext, mic = get_ciphertext_mic(encrypted) + assert ciphertext == bytes.fromhex("bedf2769dcdde9e002ab5b9df9342bc6") + assert mic == bytes.fromhex("8795d9c3fba25e76") + + pn = 0x1122 + plaintext.FCfield |= Dot11(FCfield="MF").FCfield + encrypted = encrypt_ccmp(plaintext, tk, pn) + ciphertext, mic = get_ciphertext_mic(encrypted) + assert ciphertext == bytes.fromhex("ff76206822afb77decc7ee87568a02c6") + assert mic == bytes.fromhex("8d6fd7578170ecb1") + diff --git a/research/libwifi/tests/test_dragonfly.py b/research/libwifi/tests/test_dragonfly.py new file mode 100644 index 000000000..437f03262 --- /dev/null +++ b/research/libwifi/tests/test_dragonfly.py @@ -0,0 +1,83 @@ +from libwifi import * + +def test_crypto(): + x = 32774075109236952337158599048510140249162039589740847669274255820096074575478 + y = 65816200486131266053931191249788950977703402544735864608496951271815280382692 + assert point_on_curve(x, y) + assert not point_on_curve(x, y + 1) + + +def test_sae(): + # KDF_Length with 256 bits + data = b'p(b\x08\x84%\xd4\xfc\x85\x02`>Z\x8e8\x02\x06\xdcak1_\x8a\xca\x90D2[\xda\x88\x87\xbe' + label = "SAE Hunting and Pecking" + context = b'\xff\xff\xff\xff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + result = KDF_Length(data, label, context, 256) + assert result == b"\xce\xd7\x0c`n\xc3\xa0V\xbc\xe5J' + expected += b'\x80P\x1f[PB\xfaF\x13\xae\xef\xfd\xddBkX\xff\xe03\xd3q(\x80\x9f"S\xad\xfeK:\x98\x90\xde\xdfw' + expected += b'\xd5\xe4\xf0O\xcf\x9c\xa1\r^\xf3\xf6\x1fp_\xd9\x13<\xf7\x0e\xf11\x81\x88\xb9\xfe\x80mn\xed\xf2' + expected += b'\xdel\x8f8f\\\xd4\x91\x83}|\xd0\x90\xa6I\xac\xca\xdb\x1d4\x00\xf5\xf8\xf5/\xa1' + assert result == expected + + k = 34571911558658786479991772754682275693090212979118519100150784653967632958583 + e1x = 35056562182093533434520846036041768262744712948121085631797144112066231820275 + e1y = 30178867311470377631935198005507521070028138958470370567962433403317268006022 + e1 = ECC.EccPoint(e1x, e1y) + s1 = 25673957538626389018921350300691255233489834439459044820849488820063961042178 + e2x = 55846548926525467025361797152934092596912359473099878093027981331310692689958 + e2y = 25540727936496301520339336932631497861346599764823572263118430938562903665071 + e2 = ECC.EccPoint(e2x, e2y) + s2 = 89671311642711662572527453485728796207545960881415665173397225314404138450610 + confirm = calculate_confirm_eappwd(k, e1, s1, e2, s2) + assert confirm == b"n\xe1N\xc1\x86\x0f\x94\x85W*Y\xf8\xf2'\x19\xac\x9c\xf6\xe6\xe6\x14\x8c+\xf7\x0e\xd0\xfdF\x87\x03G\xcc" + + pwe = derive_pwe_ecc_eappwd("password", "user", "server", 2903600207) + assert pwe.x == 65324672961960968584356420288746215288928369435013474055323481826901726558522 + assert pwe.y == 81287605691219879983190651062276165371083848816381214499332721121120114417256 + + pwe = derive_pwe_ecc_eappwd("password", "user", "server", 2546484939) + assert pwe.x == 32774075109236952337158599048510140249162039589740847669274255820096074575478 + assert pwe.y == 65816200486131266053931191249788950977703402544735864608496951271815280382692 + + pwe = derive_pwe_ecc_eappwd("hello", "bob", "theserver@example.com", 0xEE04524, curve_name="p521") + assert pwe.x == 3008622341264366589487649162226557348235630833654679745848438214237061388319208914517686003128943873854271397962689455307621303688693893126759626682265352869 + assert pwe.y == 649775647643090676911381912723346979966421674682002310678312738784243727860456911539411456724737204490685667258758093054491548052506429972664016924839683943 + diff --git a/research/libwifi/tests/test_mschap.py b/research/libwifi/tests/test_mschap.py new file mode 100644 index 000000000..f9c956b94 --- /dev/null +++ b/research/libwifi/tests/test_mschap.py @@ -0,0 +1,14 @@ +from libwifi.mschap import * + +def test_mschap(): + username = b"peapuser" + password = "password" + auth_challenge = binascii.unhexlify("59 ff 64 4c 14 62 df 4d 59 a4 46 5d 6b c8 09 6c".replace(" ", "")) + peer_challenge = binascii.unhexlify("0d 60 5a 24 da 8d 6e f7 58 ee 23 69 8f 37 04 46".replace(" ", "")) + + nt_response = generate_nt_response_mschap2(auth_challenge, peer_challenge, username, password) + assert nt_response == b"\xd8\xf7\xd6\x10\xa6\x1f\x0c\x0b\x49\x1d\x21\xac\xbb\xd3\x6d\x86\xb9\x91\x6f\x8e\x69\xa6\x5f\x97" + + auth_resp = generate_authenticator_response(password, nt_response, peer_challenge, auth_challenge, username) + assert auth_resp == b"\x0f\x91\x69\x7e\x8e\x8f\xd6\xb7\x25\xf3\x3c\x30\xd8\x1d\x67\xa7\x47\xfc\xba\x01" + diff --git a/research/libwifi/wifi.py b/research/libwifi/wifi.py new file mode 100644 index 000000000..7fa3e0338 --- /dev/null +++ b/research/libwifi/wifi.py @@ -0,0 +1,494 @@ +# Copyright (c) 2019-2021, Mathy Vanhoef +# +# This code may be distributed under the terms of the BSD license. +# See README for more details. +from scapy.all import * +from Crypto.Cipher import AES +from datetime import datetime +import binascii + +#### Constants #### + +IEEE_TLV_TYPE_SSID = 0 +IEEE_TLV_TYPE_CHANNEL = 3 +IEEE_TLV_TYPE_RSN = 48 +IEEE_TLV_TYPE_CSA = 37 +IEEE_TLV_TYPE_FT = 55 +IEEE_TLV_TYPE_VENDOR = 221 + +WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY = 4 +WLAN_REASON_CLASS2_FRAME_FROM_NONAUTH_STA = 6 +WLAN_REASON_CLASS3_FRAME_FROM_NONASSOC_STA = 7 + +#TODO: Not sure if really needed... +IEEE80211_RADIOTAP_RATE = (1 << 2) +IEEE80211_RADIOTAP_CHANNEL = (1 << 3) +IEEE80211_RADIOTAP_TX_FLAGS = (1 << 15) +IEEE80211_RADIOTAP_DATA_RETRIES = (1 << 17) + +#### Basic output and logging functionality #### + +ALL, DEBUG, INFO, STATUS, WARNING, ERROR = range(6) +COLORCODES = { "gray" : "\033[0;37m", + "green" : "\033[0;32m", + "orange": "\033[0;33m", + "red" : "\033[0;31m" } + +global_log_level = INFO +def log(level, msg, color=None, showtime=True): + if level < global_log_level: return + if level == DEBUG and color is None: color="gray" + if level == WARNING and color is None: color="orange" + if level == ERROR and color is None: color="red" + msg = (datetime.now().strftime('[%H:%M:%S] ') if showtime else " "*11) + COLORCODES.get(color, "") + msg + "\033[1;0m" + print(msg) + +def change_log_level(delta): + global global_log_level + global_log_level += delta + +def croprepr(p, length=175): + string = repr(p) + if len(string) > length: + return string[:length - 3] + "..." + return string + +#### Back-wards compatibility with older scapy + +if not "Dot11FCS" in locals(): + class Dot11FCS(): + pass +if not "Dot11Encrypted" in locals(): + class Dot11Encrypted(): + pass + class Dot11CCMP(): + pass + class Dot11TKIP(): + pass + +#### Linux #### + +def get_device_driver(iface): + path = "/sys/class/net/%s/device/driver" % iface + try: + output = subprocess.check_output(["readlink", "-f", path]) + return output.decode('utf-8').strip().split("/")[-1] + except: + return None + +#### Utility #### + +def get_macaddress(iface): + try: + # This method has been widely tested. + s = get_if_raw_hwaddr(iface)[1] + return ("%02x:" * 6)[:-1] % tuple(orb(x) for x in s) + except: + # Keep the old method as a backup though. + return open("/sys/class/net/%s/address" % iface).read().strip() + +def addr2bin(addr): + return binascii.a2b_hex(addr.replace(':', '')) + +def get_channel(iface): + output = str(subprocess.check_output(["iw", iface, "info"])) + p = re.compile("channel (\d+)") + m = p.search(output) + if m == None: return None + return int(m.group(1)) + +def set_channel(iface, channel): + if isinstance(channel, int): + # Compatibility with old channels encoded as simple integers + subprocess.check_output(["iw", iface, "set", "channel", str(channel)]) + else: + # Channels represented as strings with extra info (e.g "11 HT40-") + subprocess.check_output(["iw", iface, "set", "channel"] + channel.split()) + +def set_macaddress(iface, macaddr): + # macchanger throws an error if the interface already has the given MAC address + if get_macaddress(iface) != macaddr: + subprocess.check_output(["ifconfig", iface, "down"]) + subprocess.check_output(["macchanger", "-m", macaddr, iface]) + +def get_iface_type(iface): + output = str(subprocess.check_output(["iw", iface, "info"])) + p = re.compile("type (\w+)") + return str(p.search(output).group(1)) + +def set_monitor_mode(iface, up=True, mtu=1500): + # Note: we let the user put the device in monitor mode, such that they can control optional + # parameters such as "iw wlan0 set monitor active" for devices that support it. + if get_iface_type(iface) != "monitor": + # 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"]) + + if up: + subprocess.check_output(["ifconfig", iface, "up"]) + subprocess.check_output(["ifconfig", iface, "mtu", str(mtu)]) + +def rawmac(addr): + return bytes.fromhex(addr.replace(':', '')) + +def set_amsdu(p): + if "A_MSDU_Present" in [field.name for field in Dot11QoS.fields_desc]: + p.A_MSDU_Present = 1 + else: + p.Reserved = 1 + +def is_amsdu(p): + if "A_MSDU_Present" in [field.name for field in Dot11QoS.fields_desc]: + return p.A_MSDU_Present == 1 + else: + return p.Reserved == 1 + +#### Packet Processing Functions #### + +class DHCP_sock(DHCP_am): + def __init__(self, **kwargs): + self.sock = kwargs.pop("sock") + self.server_ip = kwargs["gw"] + super(DHCP_sock, self).__init__(**kwargs) + + def prealloc_ip(self, clientmac, ip=None): + """Allocate an IP for the client before it send DHCP requests""" + if clientmac not in self.leases: + if ip == None: + ip = self.pool.pop() + self.leases[clientmac] = ip + return self.leases[clientmac] + + def make_reply(self, req): + rep = super(DHCP_sock, self).make_reply(req) + + # Fix scapy bug: set broadcast IP if required + if rep is not None and BOOTP in req and IP in rep: + if req[BOOTP].flags & 0x8000 != 0 and req[BOOTP].giaddr == '0.0.0.0' and req[BOOTP].ciaddr == '0.0.0.0': + rep[IP].dst = "255.255.255.255" + + # Explicitly set source IP if requested + if not self.server_ip is None: + rep[IP].src = self.server_ip + + return rep + + def send_reply(self, reply): + self.sock.send(reply, **self.optsend) + + def print_reply(self, req, reply): + log(STATUS, "%s: DHCP reply %s to %s" % (reply.getlayer(Ether).dst, reply.getlayer(BOOTP).yiaddr, reply.dst), color="green") + + def remove_client(self, clientmac): + clientip = self.leases[clientmac] + self.pool.append(clientip) + del self.leases[clientmac] + +class ARP_sock(ARP_am): + def __init__(self, **kwargs): + self.sock = kwargs.pop("sock") + super(ARP_am, self).__init__(**kwargs) + + def send_reply(self, reply): + self.sock.send(reply, **self.optsend) + + def print_reply(self, req, reply): + log(STATUS, "%s: ARP: %s ==> %s on %s" % (reply.getlayer(Ether).dst, req.summary(), reply.summary(), self.iff)) + + +#### Packet Processing Functions #### + +# Compatibility with older Scapy versions +if not "ORDER" in scapy.layers.dot11._rt_txflags: + scapy.layers.dot11._rt_txflags.append("ORDER") + +class MonitorSocket(L2Socket): + def __init__(self, iface, dumpfile=None, detect_injected=False, **kwargs): + super(MonitorSocket, self).__init__(iface, **kwargs) + self.pcap = None + if dumpfile: + self.pcap = PcapWriter("%s.%s.pcap" % (dumpfile, self.iface), append=False, sync=True) + self.detect_injected = detect_injected + self.default_rate = None + + def set_channel(self, channel): + subprocess.check_output(["iw", self.iface, "set", "channel", str(channel)]) + + def attach_filter(self, bpf): + log(DEBUG, "Attaching filter to %s: <%s>" % (self.iface, bpf)) + attach_filter(self.ins, bpf, self.iface) + + def set_default_rate(self, rate): + self.default_rate = rate + + def send(self, p, rate=None): + # Hack: set the More Data flag so we can detect injected frames (and so clients stay awake longer) + if self.detect_injected: + p.FCfield |= 0x20 + + # Control data rate injected frames + if rate is None and self.default_rate is None: + rtap = RadioTap(present="TXFlags", TXFlags="NOSEQ+ORDER") + else: + use_rate = rate if rate != None else self.default_rate + rtap = RadioTap(present="TXFlags+Rate", Rate=use_rate, TXFlags="NOSEQ+ORDER") + + L2Socket.send(self, rtap/p) + if self.pcap: self.pcap.write(RadioTap()/p) + + def _strip_fcs(self, p): + """ + Scapy may throw exceptions when handling malformed short frames, + so we need to catch these exceptions and just ignore these frames. + This in particular happened with short malformed beacons. + """ + try: + return Dot11(raw(p[Dot11FCS])[:-4]) + except: + return None + + def _detect_and_strip_fcs(self, p): + # Older scapy can't handle the optional Frame Check Sequence (FCS) field automatically + if p[RadioTap].present & 2 != 0 and not Dot11FCS in p: + rawframe = raw(p[RadioTap]) + pos = 8 + while orb(rawframe[pos - 1]) & 0x80 != 0: pos += 4 + + # If the TSFT field is present, it must be 8-bytes aligned + if p[RadioTap].present & 1 != 0: + pos += (8 - (pos % 8)) + pos += 8 + + # Remove FCS if present + if orb(rawframe[pos]) & 0x10 != 0: + return self._strip_fcs(p) + + return p[Dot11] + + def recv(self, x=MTU, reflected=False): + p = L2Socket.recv(self, x) + if p == None or not (Dot11 in p or Dot11FCS in p): + return None + if self.pcap: + self.pcap.write(p) + + # Hack: ignore frames that we just injected and are echoed back by the kernel + if self.detect_injected and p.FCfield & 0x20 != 0: + return None + + # Ignore reflection of injected frames. These have a small RadioTap header. + if not reflected and p[RadioTap].len < 13: + return None + + # Strip the FCS if present, and drop the RadioTap header + if Dot11FCS in p: + return self._strip_fcs(p) + else: + return self._detect_and_strip_fcs(p) + + def close(self): + if self.pcap: self.pcap.close() + super(MonitorSocket, self).close() + +# For backwards compatibility +class MitmSocket(MonitorSocket): + pass + +def dot11_get_seqnum(p): + return p.SC >> 4 + +def dot11_is_encrypted_data(p): + # All these different cases are explicitly tested to handle older scapy versions + return (p.FCfield & 0x40) or Dot11CCMP in p or Dot11TKIP in p or Dot11WEP in p or Dot11Encrypted in p + +def payload_to_iv(payload): + iv0 = payload[0] + iv1 = payload[1] + wepdata = payload[4:8] + + # FIXME: Only CCMP is supported (TKIP uses a different IV structure) + return orb(iv0) + (orb(iv1) << 8) + (struct.unpack(">I", wepdata)[0] << 16) + +def dot11_get_iv(p): + """ + This function assumes the frame is encrypted using either CCMP or WEP. + It does not work for other encrypion protocol (e.g. TKIP). + """ + + # The simple and default case + if Dot11CCMP in p: + payload = raw(p[Dot11CCMP]) + return payload_to_iv(payload) + + # Scapy uses a heuristic to differentiate CCMP/TKIP and this may be wrong. + # So even when we get a Dot11TKIP frame, we'll still treat it like a Dot11CCMP frame. + elif Dot11TKIP in p: + payload = raw(p[Dot11TKIP]) + return payload_to_iv(payload) + + elif Dot11WEP in p: + wep = p[Dot11WEP] + # Older Scapy versions parse CCMP-encrypted frames as Dot11WEP. So we check if the + # extended IV flag is set, and if so, treat it like a CCMP frame. + if wep.keyid & 32: + # This only works for CCMP (TKIP uses a different IV structure). + return orb(wep.iv[0]) + (orb(wep.iv[1]) << 8) + (struct.unpack(">I", wep.wepdata[:4])[0] << 16) + + # If the extended IV flag is not set meaning it's indeed WEP. + else: + return orb(wep.iv[0]) + (orb(wep.iv[1]) << 8) + (orb(wep.iv[2]) << 16) + + # Scapy uses Dot11Encrypted if it couldn't determine how the frame was encrypted. Assume CCMP. + elif Dot11Encrypted in p: + payload = raw(p[Dot11Encrypted]) + return payload_to_iv(payload) + + # Manually detect encrypted frames in case (older versions of) Scapy failed to do this. Assume CCMP. + elif p.FCfield & 0x40: + return payload_to_iv(p[Raw].load) + + # Couldn't determine the IV + return None + +def dot11_get_priority(p): + if not Dot11QoS in p: return 0 + return p[Dot11QoS].TID + + +#### Crypto functions and util #### + +def get_ccmp_payload(p): + if Dot11WEP in p: + # Extract encrypted payload: + # - Skip extended IV (4 bytes in total) + # - Exclude first 4 bytes of the CCMP MIC (note that last 4 are saved in the WEP ICV field) + return str(p.wepdata[4:-4]) + elif Dot11CCMP in p: + return p[Dot11CCMP].data + elif Dot11TKIP in p: + return p[Dot11TKIP].data + elif Dot11Encrypted in p: + return p[Dot11Encrypted].data + else: + return p[Raw].load + +class IvInfo(): + def __init__(self, p): + self.iv = dot11_get_iv(p) + self.seq = dot11_get_seqnum(p) + self.time = p.time + + def is_reused(self, p): + """Return true if frame p reuses an IV and if p is not a retransmitted frame""" + iv = dot11_get_iv(p) + seq = dot11_get_seqnum(p) + return self.iv == iv and self.seq != seq and p.time >= self.time + 1 + +class IvCollection(): + def __init__(self): + self.ivs = dict() # maps IV values to IvInfo objects + + def reset(self): + self.ivs = dict() + + def track_used_iv(self, p): + iv = dot11_get_iv(p) + self.ivs[iv] = IvInfo(p) + + def is_iv_reused(self, p): + """Returns True if this is an *observed* IV reuse and not just a retransmission""" + iv = dot11_get_iv(p) + return iv in self.ivs and self.ivs[iv].is_reused(p) + + def is_new_iv(self, p): + """Returns True if the IV in this frame is higher than all previously observed ones""" + iv = dot11_get_iv(p) + if len(self.ivs) == 0: return True + return iv > max(self.ivs.keys()) + +def create_fragments(header, data, num_frags): + # This special case is useful so scapy keeps the full "interpretation" of the frame + # instead of afterwards treating/displaying the payload as just raw data. + if num_frags == 1: return [header/data] + + data = raw(data) + fragments = [] + fragsize = (len(data) + num_frags - 1) // num_frags + for i in range(num_frags): + frag = header.copy() + frag.SC |= i + if i < num_frags - 1: + frag.FCfield |= Dot11(FCfield="MF").FCfield + + payload = data[fragsize * i : fragsize * (i + 1)] + frag = frag/Raw(payload) + fragments.append(frag) + + return fragments + +def get_element(el, id): + if not Dot11Elt in el: return None + el = el[Dot11Elt] + while not el is None: + if el.ID == id: + return el + el = el.payload + return None + +def get_ssid(beacon): + if not (Dot11 in beacon or Dot11FCS in beacon): return + if Dot11Elt not in beacon: return + if beacon[Dot11].type != 0 and beacon[Dot11].subtype != 8: return + el = get_element(beacon, IEEE_TLV_TYPE_SSID) + return el.info.decode() + +def is_from_sta(p, macaddr): + if not (Dot11 in p or Dot11FCS in p): + return False + if p.addr1 != macaddr and p.addr2 != macaddr: + return False + return True + +def get_bss(iface, clientmac, timeout=20): + ps = sniff(count=1, timeout=timeout, lfilter=lambda p: is_from_sta(p, clientmac) and p.addr2 != None, iface=iface) + if len(ps) == 0: + return None + return ps[0].addr1 if ps[0].addr1 != clientmac else ps[0].addr2 + +def create_msdu_subframe(src, dst, payload, last=False): + length = len(payload) + p = Ether(dst=dst, src=src, type=length) + + payload = raw(payload) + + total_length = len(p) + len(payload) + padding = "" + if not last and total_length % 4 != 0: + padding = b"\x00" * (4 - (total_length % 4)) + + return p / payload / Raw(padding) + +def find_network(iface, ssid): + ps = sniff(count=1, timeout=0.3, lfilter=lambda p: get_ssid(p) == ssid, iface=iface) + if ps is None or len(ps) < 1: + log(STATUS, "Searching for target network on other channels") + for chan in [1, 6, 11, 3, 8, 2, 7, 4, 10, 5, 9, 12, 13]: + set_channel(iface, chan) + log(DEBUG, "Listening on channel %d" % chan) + ps = sniff(count=1, timeout=0.3, lfilter=lambda p: get_ssid(p) == ssid, iface=iface) + if ps and len(ps) >= 1: break + + if ps and len(ps) >= 1: + # Even though we capture the beacon we might still be on another channel, + # so it's important to explicitly switch to the correct channel. + actual_chan = orb(get_element(ps[0], IEEE_TLV_TYPE_CHANNEL).info) + set_channel(iface, actual_chan) + + # Return the beacon that we captured + return ps[0] + + return None +