From 0131acaec2f2d569043cbc5ae8de04cee31eaefa Mon Sep 17 00:00:00 2001 From: jsz Date: Fri, 16 Feb 2024 12:24:28 -0500 Subject: [PATCH] 1.0.16 (#174) * client chipset detection heuristics * add beamformee sts for vht and he (#121) * compat 11 to align with other packages * bump scapy from 2.4.5 to 2.5.0 * bump extra requirements pins * bump py requires to 3.9 as 3.7 is eol * draft 6 GHz changes which don't seem to work * remove redundant logging arg * format with tox * add wpa3-personal and wpa3-personal transition mode support * fix crash when profiling OnePlus 11 5G * big 1.0.16 commit, reader: don't do this --- .github/ISSUE_TEMPLATE/bug_report.md | 2 + AUTHORS.rst | 2 +- CAPABILITY_LOGIC.md | 23 +- CONTRIBUTING.md | 3 +- DEPLOYMENT.md | 2 +- DEVELOPMENT.md | 28 ++ LICENSE | 2 +- MANIFEST.in | 4 +- RELEASE_NOTES.md | 12 +- coverage.svg | 4 +- debian/changelog | 14 +- debian/compat | 2 +- debian/control | 4 +- debian/copyright | 2 +- debian/rules | 2 +- debian/wlanpi-profiler.1 | 2 +- debian/wlanpi-profiler.1.md | 2 +- etc/wlanpi-profiler/config.ini | 32 +- extras.in | 10 + extras.txt | 109 ++++++- profiler/__main__.py | 8 +- profiler/__version__.py | 4 +- profiler/config.ini | 3 + profiler/constants.py | 5 +- profiler/fakeap.py | 304 +++++++++++++++--- profiler/helpers.py | 100 +++--- profiler/interface.py | 2 +- profiler/manager.py | 4 +- profiler/profiler.py | 209 ++++++++++-- requirements.in | 2 + requirements.txt | 14 +- setup.py | 39 ++- ...1_4th_Gen_UK_82-8b-75-2d-f2-c0_5.8GHz.pcap | Bin 0 -> 329 bytes tests/test_fakeap.py | 10 +- tests/test_helpers.py | 2 +- tests/test_profiler.py | 29 +- tox.ini | 9 +- utils/anonymizer.py | 2 +- 38 files changed, 800 insertions(+), 207 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 extras.in create mode 100644 requirements.in create mode 100644 tests/pcaps/iPad11_4th_Gen_UK_82-8b-75-2d-f2-c0_5.8GHz.pcap diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0e72740..8181b7c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,5 +27,7 @@ Request to include two debug outputs: - Hardware: [e.g. RBPi 4, WLAN Pi Pro] - Version [e.g. 1.0.0 (`$ profiler --version`)] +Output from `uname -a`. + **Additional context** Add any other context about the problem here. diff --git a/AUTHORS.rst b/AUTHORS.rst index 7aa72a5..b447cee 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,4 +1,4 @@ -profiler was created by Josh Schmelzle. +wlanpi-profiler was created by Josh Schmelzle. Keepers ``````` diff --git a/CAPABILITY_LOGIC.md b/CAPABILITY_LOGIC.md index 35ba49d..4c82184 100644 --- a/CAPABILITY_LOGIC.md +++ b/CAPABILITY_LOGIC.md @@ -22,9 +22,11 @@ values to determine client capabilities. - MCS 0-9 if pairs are set to '11' - c. inspect octet 1 (one of the four vht capability octets) - if bit zero set to '1', client is SU Beam-formee capable - - d. inspect octet 2 (one of the four vht capability octets) + - d. inspect octect 1 (one of the four vht capability octets) + - add bit 5, 6, 7 to determine VHT Beamformee STS + - e. inspect octet 2 (one of the four vht capability octets) - if bit zero set to '1', client is MU Beam-formee capable - - e. inspect octet 0 (one of the four vht capability octets) + - f. inspect octet 0 (one of the four vht capability octets) - if bit zero set to '1', client supports VHT 160 MHz 3. 802.11k: inspect tagged parameter 70 (RM Enabled Capabilities) - RM = radio management @@ -91,6 +93,14 @@ values to determine client capabilities. - h. Buffer Status Report (BSR) support: B19 of HE PHY Capabilities - Y - supported - N - not supported + - i. HE SU Beamformer: Bit 31 of HE PHY Capabilities + - 1 - supported + - 0 - not supported + - j. HE SU Beamformee: Bit 32 of HE PHY Capabilities + - 1 - supported + - 0 - not supported + - k. HE Beamformee STS: Bits 36-34 of HE PHY Capabilities + - Add Bits 36-34 to determine HE Beamformee STS 10. 802.11ax spatial reuse: inspect spatial reuse tag number 39 (Spatial Reuse Parameter Set) - a. is Spatial Reuse Parameter Set tagged parameter present? @@ -117,7 +127,14 @@ values to determine client capabilities. - Y - Return match - N - Unable to match -14. Detecting 6 GHz Capability Out-of-band via Alternative Operating Class +14. Chipset manufacturer detection through heuristics + - a. can Vendor Specific Tag 221 OUI be resolved by lookup of OUI in manuf db? + - N - Unable to match + - Y - Check OUI matches our heuristics + - Y - return match + - N - unknown / unable to match + +15. Detecting 6 GHz Capability Out-of-band via Alternative Operating Class - a. is Supported Operating Classes tagged parameter present? - N - not supported - Y - may be supported diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc0de79..7b95f6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Please be aware of the following things when filing bug reports: ### Development Environment -Consider using PyCharm or Visual Studio Code with the official Python and Pylance extensions from Microsoft (recommended). +Consider using Visual Studio Code with the official Python extensions from Microsoft. ### Development Setup ('without pip install or pipx', 'may be recommended for development work'): @@ -60,6 +60,7 @@ git clone cd virtualenv venv source venv/bin/activate +pip install -U pip pip-tools setuptools wheel pip install -r requirements.txt sudo ./venv/bin/python3 -m profiler sudo ./venv/bin/python3 -m profiler diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 3c7d6e4..bca9047 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -15,7 +15,7 @@ There are two workflows defined. On your build host, install the build tools (these are only needed on the device doing the build): ```bash -sudo apt-get install build-essential debhelper devscripts equivs python3-pip python3-all python3-dev python3-setuptools dh-virtualenv +sudo apt-get install build-essential debhelper devscripts equivs python3-pip python3-all python3-dev python3-setuptools dh-virtualenv dh-python ``` Install Python depends so that the tooling doesn't fail when it tries to evaluate which tests to run. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..01650a2 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,28 @@ +# Initial Development Setup + +## Repository + +1. Clone repo to development host + +2. Create and activate virtualenv + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +3. Update and install tools + +```bash +pip install -u pip pip-tools setuptools wheel +``` + +4. Install depends + +```bash +pip install -r requirements.txt +``` + +## Building and Packaging + +Refer to DEPLOYMENT.md diff --git a/LICENSE b/LICENSE index 766f769..4ca20da 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2021 Josh Schmelzle +Copyright 2022 Josh Schmelzle Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in index 1bfbfba..9eed4ce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ -include profiler/config.ini \ No newline at end of file +include profiler/config.ini +include extras.txt +include requirements.txt \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3cd5099..7501863 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,12 @@ -Unreleased - -- TBD +Release 1.0.16 + +- Chipset lookup via heuristics +- Profile VHT Beamformee STS Capability +- Profile HE Beamformee STS Capability +- Fix crash in OUI profiling (caused by certain Wi-Fi 7 clients) +- Add basic Wi-Fi 7 profiling (presence of EHT IEs) +- Add Profiler Vendor IE with TLVs for profiler version and system version +- Switch dependency on manuf to manuf2 fork Release 1.0.15 diff --git a/coverage.svg b/coverage.svg index 4f8c185..f9eb6b4 100644 --- a/coverage.svg +++ b/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 50% - 50% + 55% + 55% diff --git a/debian/changelog b/debian/changelog index 1daf3ce..e17697c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,16 @@ -wlanpi-profiler (1.0.15) UNRELEASED; urgency=medium +wlanpi-profiler (1.0.16) unstable; urgency=medium + + * Chipset lookup via heuristics + * Profile VHT Beamformee STS Capability + * Profile HE Beamformee STS Capability + * Fix crash in OUI profiling (caused by certain Wi-Fi 7 clients) + * Add basic Wi-Fi 7 profiling (presence of EHT IEs) + * Add Profiler Vendor IE with TLVs for profiler version and system version + * Switch dependency on manuf to manuf2 fork + + -- Josh Schmelzle Fri, 16 Feb 2024 10:12:11 -0400 + +wlanpi-profiler (1.0.15) unstable; urgency=medium * Handle traceback when config.ini is corrupt * Minor cosmetic changes diff --git a/debian/compat b/debian/compat index 3cacc0b..9d60796 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -12 \ No newline at end of file +11 \ No newline at end of file diff --git a/debian/control b/debian/control index 1c78c27..c20d821 100644 --- a/debian/control +++ b/debian/control @@ -12,12 +12,12 @@ Build-Depends: debhelper (>= 11), python3-distutils, python3-venv Standards-Version: 4.6.0 -X-Python3-Version: >= 3.7 +X-Python3-Version: >= 3.9 Homepage: https://github.com/WLAN-Pi/profiler Package: wlanpi-profiler Architecture: any -Pre-Depends: dpkg (>= 1.16.1), python3 (>=3.7), python3-distutils, ${misc:Pre-Depends} +Pre-Depends: dpkg (>= 1.16.1), python3 (>=3.9), python3-distutils, ${misc:Pre-Depends} Depends: ${misc:Depends} Description: WLAN Pi - Wi-Fi client capabilities profiler wlanpi-profiler creates a fake AP for the collection and analysis of station association frames. \ No newline at end of file diff --git a/debian/copyright b/debian/copyright index 2a82e6b..b4f89cf 100644 --- a/debian/copyright +++ b/debian/copyright @@ -3,7 +3,7 @@ Upstream-Name: wlanpi-profiler Source: https://www.github.com/wlan-pi/wlanpi-profiler Files: * -Copyright: 2021 Josh Schmelzle +Copyright: 2024 Josh Schmelzle License: 3-clause BSD Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/debian/rules b/debian/rules index 8660930..b02e4cc 100755 --- a/debian/rules +++ b/debian/rules @@ -9,7 +9,7 @@ SNAKE=/usr/bin/python3 PACKAGE=$(shell dh_listpackages) VERSION=$(shell parsechangelog | grep ^Version: | sed -re 's/[^0-9]+([^-]+).*/\1/') SDIST_DIR=debian/$(PACKAGE)-$(VERSION) -EXTRA_REQUIREMENTS=--upgrade-pip --preinstall "setuptools>=57" --preinstall "wheel>=0.36" +EXTRA_REQUIREMENTS=--upgrade-pip-to 23.2 --preinstall "setuptools==68.0.0" --preinstall "wheel==0.40.0" DH_VENV_ARGS=--builtin-venv --python ${SNAKE} $(EXTRA_REQUIREMENTS) \ --extra-pip-arg=--progress-bar=on diff --git a/debian/wlanpi-profiler.1 b/debian/wlanpi-profiler.1 index 22293e2..f93a43c 100644 --- a/debian/wlanpi-profiler.1 +++ b/debian/wlanpi-profiler.1 @@ -173,7 +173,7 @@ Bugs and issues can be reported on GitHub: https://github.com/wlan-pi/profiler .SH COPYRIGHT .PP -Copyright \[co] 2021 Josh Schmelzle. +Copyright \[co] 2024 Josh Schmelzle. License BSD-3-Clause. .SH SEE ALSO .PP diff --git a/debian/wlanpi-profiler.1.md b/debian/wlanpi-profiler.1.md index 86875fd..edd9c30 100644 --- a/debian/wlanpi-profiler.1.md +++ b/debian/wlanpi-profiler.1.md @@ -129,7 +129,7 @@ https://github.com/wlan-pi/profiler # COPYRIGHT -Copyright © 2021 Josh Schmelzle. License BSD-3-Clause. +Copyright © 2024 Josh Schmelzle. License BSD-3-Clause. # SEE ALSO diff --git a/etc/wlanpi-profiler/config.ini b/etc/wlanpi-profiler/config.ini index 067bdd8..146cf2c 100644 --- a/etc/wlanpi-profiler/config.ini +++ b/etc/wlanpi-profiler/config.ini @@ -1,33 +1,35 @@ -# ___ _ _ ___ _ -# ___ ___ ___| _|_| |___ ___ ___ ___ ___| _|_|___ -# | . | _| . | _| | | -_| _| | _| . | | _| | . | -# | _|_| |___|_| |_|_|___|_| |___|___|_|_|_| |_|_ | -# |_| |___| -# +# _ _ __ _ _ +# __ _| | __ _ _ __ _ __ (_) _ __ _ __ ___ / _(_) | ___ _ __ +# \ \ /\ / / |/ _` | '_ \| '_ \| |_____| '_ \| '__/ _ \| |_| | |/ _ \ '__| +# \ V V /| | (_| | | | | |_) | |_____| |_) | | | (_) | _| | | __/ | +# \_/\_/ |_|\__,_|_| |_| .__/|_| | .__/|_| \___/|_| |_|_|\___|_| +# |_| |_| # -# The following settings define the default config. -# NOTE: Command-line options will take precedence over this config. -# WARNING: Changing the section names or keys *will* break things. +# The following configuration controls the default behavior for the profiler +# Note command-line arguments supersede defaults found here +# WARNING: Changing section or key names (channel, interface, SSID) will break profiler [GENERAL] # channel for profiler AP channel: 36 -# SSID name for profiler AP -# blank means SSID will be `Profiler xxx` where `xxx` is the last 3 characters of the eth0 MAC. -ssid: - -# interface used by profiler AP +# interface for profiler AP interface: wlan0 +# SSID for profiler AP (no value after : means SSID will be `Profiler xxx` where xxx is the last 3 characters of the eth0 MAC) +ssid: + # enable or disable 802.11r information elements (True/False) ft_disabled: False # enable or disable 802.11ax information elements (True/False) he_disabled: False -# disables beacons and instead only listens for assoc req frames (True/False) +# enable or disable 802.11be information elements (True/False) +be_disabled: False + +# disables beacons and instead only listens for assoc req. frames (True/False) listen_only: False # use the system's hostname as the profiler AP SSID (True/False) diff --git a/extras.in b/extras.in new file mode 100644 index 0000000..4095b21 --- /dev/null +++ b/extras.in @@ -0,0 +1,10 @@ +black +isort +autoflake +mypy +flake8 +pytest +pytest-cov +pytest-mock +tox +coverage-badge \ No newline at end of file diff --git a/extras.txt b/extras.txt index 699c8fa..00e73d8 100644 --- a/extras.txt +++ b/extras.txt @@ -1,10 +1,99 @@ -black -isort -autoflake -mypy -flake8 -pytest -pytest-cov -pytest-mock -tox==3.27.0 -coverage-badge==1.1.0 \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile extras.in +# +--extra-index-url https://www.piwheels.org/simple + +autoflake==2.2.1 + # via -r extras.in +black==23.9.1 + # via -r extras.in +cachetools==5.3.1 + # via tox +chardet==5.2.0 + # via tox +click==8.1.7 + # via black +colorama==0.4.6 + # via tox +coverage[toml]==7.3.1 + # via + # coverage-badge + # pytest-cov +coverage-badge==1.1.0 + # via -r extras.in +distlib==0.3.7 + # via virtualenv +exceptiongroup==1.1.3 + # via pytest +filelock==3.12.4 + # via + # tox + # virtualenv +flake8==6.1.0 + # via -r extras.in +iniconfig==2.0.0 + # via pytest +isort==5.12.0 + # via -r extras.in +mccabe==0.7.0 + # via flake8 +mypy==1.5.1 + # via -r extras.in +mypy-extensions==1.0.0 + # via + # black + # mypy +packaging==23.1 + # via + # black + # pyproject-api + # pytest + # tox +pathspec==0.11.2 + # via black +platformdirs==3.10.0 + # via + # black + # tox + # virtualenv +pluggy==1.3.0 + # via + # pytest + # tox +pycodestyle==2.11.0 + # via flake8 +pyflakes==3.1.0 + # via + # autoflake + # flake8 +pyproject-api==1.6.1 + # via tox +pytest==7.4.2 + # via + # -r extras.in + # pytest-cov + # pytest-mock +pytest-cov==4.1.0 + # via -r extras.in +pytest-mock==3.11.1 + # via -r extras.in +tomli==2.0.1 + # via + # autoflake + # black + # coverage + # mypy + # pyproject-api + # pytest + # tox +tox==4.11.3 + # via -r extras.in +typing-extensions==4.7.1 + # via + # black + # mypy +virtualenv==20.24.5 + # via tox diff --git a/profiler/__main__.py b/profiler/__main__.py index 516f1cc..d31e132 100644 --- a/profiler/__main__.py +++ b/profiler/__main__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -35,10 +35,10 @@ def init(): "{0} only works on Linux... exiting...".format(os.path.basename(__file__)) ) - # hard set no support for python < v3.7 - if sys.version_info < (3, 7): + # hard set no support for python < v3.9 + if sys.version_info < (3, 9): sys.exit( - "{0} requires Python version 3.7 or higher...\nyou are trying to run with Python version {1}...\nexiting...".format( + "{0} requires Python version 3.9 or higher...\nyou are trying to run with Python version {1}...\nexiting...".format( os.path.basename(__file__), platform.python_version() ) ) diff --git a/profiler/__version__.py b/profiler/__version__.py index 6b76d80..2e9f3c2 100644 --- a/profiler/__version__.py +++ b/profiler/__version__.py @@ -12,6 +12,6 @@ __url__ = "https://github.com/wlan-pi/profiler" __author__ = "Josh Schmelzle" __author_email__ = "josh@joshschmelzle.com" -__version__ = "1.0.15" -__status__ = "alpha" +__version__ = "1.0.16" +__status__ = "beta" __license__ = "BSD-3-Clause" diff --git a/profiler/config.ini b/profiler/config.ini index 455293d..146cf2c 100644 --- a/profiler/config.ini +++ b/profiler/config.ini @@ -26,6 +26,9 @@ ft_disabled: False # enable or disable 802.11ax information elements (True/False) he_disabled: False +# enable or disable 802.11be information elements (True/False) +be_disabled: False + # disables beacons and instead only listens for assoc req. frames (True/False) listen_only: False diff --git a/profiler/constants.py b/profiler/constants.py index 8aa9801..b627d41 100644 --- a/profiler/constants.py +++ b/profiler/constants.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -41,6 +41,9 @@ HE_OPERATION_IE_EXT_TAG = 36 # 802.11ax HE Operation IE HE_SPATIAL_REUSE_IE_EXT_TAG = 39 # 802.11ax Spatial Reuse Paramater IE HE_6_GHZ_BAND_CAP_IE_EXT_TAG = 59 # 802.11ax 6 GHz capabilities IE +MLE_EXT_TAG = 107 # 802.11be Multi-Link Element +EHT_CAPABILITIES_IE_EXT_TAG = 108 # 802.11be EHT Capabilities IE +EHT_OPERATION_IE_EXT_TAG = 109 # 802.11be EHT Operation IE CHANNELS = { "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], diff --git a/profiler/fakeap.py b/profiler/fakeap.py index 35ecd3c..5e65b50 100644 --- a/profiler/fakeap.py +++ b/profiler/fakeap.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -33,7 +33,12 @@ from scapy.all import Dot11ProbeResp # type: ignore from scapy.all import Dot11, Dot11Auth, RadioTap, Scapy_Exception # type: ignore from scapy.all import conf as scapyconf # type: ignore - from scapy.all import get_if_hwaddr, get_if_raw_hwaddr, sniff # type: ignore + from scapy.all import ( # type: ignore + get_if_hwaddr, + get_if_raw_hwaddr, + hexdump, + sniff, + ) except ModuleNotFoundError as error: if error.name == "scapy": print("required module scapy not found.") @@ -41,9 +46,10 @@ print(f"{error}") sys.exit(signal.SIGABRT) +from .__version__ import __version__ + # app imports from .constants import ( - CHANNELS, DOT11_SUBTYPE_ASSOC_REQ, DOT11_SUBTYPE_AUTH_REQ, DOT11_SUBTYPE_BEACON, @@ -52,24 +58,75 @@ DOT11_SUBTYPE_REASSOC_REQ, DOT11_TYPE_MANAGEMENT, ) +from .helpers import get_wlanpi_version class _Utils: """Fake AP helper functions""" - @staticmethod - def build_fake_frame_ies(config) -> Dot11Elt: - """Build base frame for beacon and probe resp""" - ssid: "str" = config.get("GENERAL").get("ssid") - channel = int(config.get("GENERAL").get("channel")) + def build_wlanpi_vendor_ie_type_0(testing): + """ + OUI type 0 will follow a type-length-value (TLV) encoding like so <221>[[] ...] + + | Byte Offset | Field Length | Field Name | Description | + | ----------- | ------------- | ---------- | ----------------------------- | + | 0 | 1 Bytes | Subtype | Type identifier for attribute | + + Followed by TLVs: + + Type 0 + + | Field Length | Field Name | Description | + | 1 Bytes | Type | | + | 1 Bytes | Profiler version length | Length of profiler version data field | + | N Bytes | Profiler version data | Profiler version | + + Type 1 + + | Field Length | Field Name | Description | + | 1 Bytes | Type | | + | 1 Bytes | WLAN Pi system version length | Length of WLAN Pi system version data field | + | N Bytes | WLAN Pi system version data | WLAN Pi system version | + """ + oui = b"\x31\x41\x59" + subtype = b"\x00" + + profiler_version = __version__ + if testing: + profiler_version = "6.6.6" + profiler_version_type = int(0).to_bytes(1, "big") + profiler_version_data = bytes(f"{profiler_version}".encode("ascii")) + profiler_version_length = len(profiler_version_data).to_bytes(1, "big") + profiler_version_tlv = ( + profiler_version_type + profiler_version_length + profiler_version_data + ) - is_6ghz = False - if channel in CHANNELS["6G"]: - is_6ghz = True + system_version = get_wlanpi_version() + if testing: + system_version = "9.9.9" + system_version_type = int(1).to_bytes(1, "big") + system_version_data = bytes(f"{system_version}".encode("ascii")) + system_version_length = len(system_version_data).to_bytes(1, "big") + system_version_tlv = ( + system_version_type + system_version_length + system_version_data + ) - ft_disabled: "bool" = config.get("GENERAL").get("ft_disabled") - he_disabled: "bool" = config.get("GENERAL").get("he_disabled") + wlanpi_vendor_data = oui + subtype + profiler_version_tlv + system_version_tlv + return Dot11Elt(ID=0xDD, info=wlanpi_vendor_data) + @staticmethod + def build_fake_frame_ies_2ghz_5ghz( + ssid, + mac, + channel, + ft_disabled, + he_disabled, + be_disabled, + wpa3_personal_transition, + wpa3_personal, + testing + ) -> Dot11Elt: + """Build base frame for beacon and probe resp""" ssid_bytes: "bytes" = bytes(ssid, "utf-8") essid = Dot11Elt(ID="SSID", info=ssid_bytes) @@ -86,11 +143,21 @@ def build_fake_frame_ies(config) -> Dot11Elt: ht_capabilities = Dot11Elt(ID=0x2D, info=ht_cap_data) if ft_disabled: - rsn_data = b"\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x02\x80\x00" + akm = b"\x01\x00\x00\x0f\xac\x02\x80\x00" + if wpa3_personal_transition: + akm = b"\x02\x00\x00\x0f\xac\x02\x00\x0f\xac\x08\x80\x00" + if wpa3_personal: + akm = b"\x01\x00\x00\x0f\xac\x08\x90\x00" + rsn_data = b"\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04" + akm else: mobility_domain_data = b"\x45\xc2\x00" mobility_domain = Dot11Elt(ID=0x36, info=mobility_domain_data) - rsn_data = b"\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x02\x00\x00\x0f\xac\x02\x00\x0f\xac\x04\x8c\x00" + akm = b"\x02\x00\x00\x0f\xac\x02\x00\x0f\xac\x04\x8c\x00" + if wpa3_personal_transition: + akm = b"\x04\x00\x00\x0f\xac\x02\x00\x0f\xac\x04\x00\x0f\xac\x08\x00\x0f\xac\x09\x8c\x00" + if wpa3_personal: + akm = b"\x02\x00\x00\x0f\xac\x08\x00\x0f\xac\x09\x9c\x00" + rsn_data = b"\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04" + akm rsn = Dot11Elt(ID=0x30, info=rsn_data) @@ -127,19 +194,31 @@ def build_fake_frame_ies(config) -> Dot11Elt: mu_edca_data = b"\x26\x09\x03\xa4\x28\x27\xa4\x28\x42\x73\x28\x62\x72\x28" mu_edca = Dot11Elt(ID=0xFF, info=mu_edca_data) - six_ghz_cap_data = b"\x3b\x00\x00" - six_ghz_cap = Dot11Elt(ID=0xFF, info=six_ghz_cap_data) - - # reduced_neighbor_report_data = b"\x02" - # reduced_neighbor_report = Dot11Elt(ID=0xFF, info=reduced_neighbor_report_data) - - # custom_hash = {"pver": f"{__version__}", "sver": get_wlanpi_version()} - # custom_data = bytes(f"{custom_hash}", "utf-8") - # custom = Dot11Elt(ID=0xDE, info=custom_data) + eht_cap_data = b"\x6c\x00\x00\xe2\xff\xdb\x00\x18\x36\xd8\x1e\x00\x44\x44\x44\x44\x44\x44\x44\x44\x44" + eht_capabilities = Dot11Elt(ID=0xFF, info=eht_cap_data) + + # EHT CBW + # 0111 7 or 1100 C - 320 MHz + # 0011 3 or 1011 B - 160 MHz + # 0010 2 or 1010 A - 80 MHz + # 0001 1 or 1001 9 - 40 MHz + # 0000 0 or 1000 8 - 20 MHz + # eht_op_data = b"\x6a\x05\x11\x00\x00\x00\xf8\x4f\x3f" # 20 MHz CBW + # eht_op_data24 = b"\x6a\x05\x11\x00\x00\x00\xf9\x4f\x3f" # 40 MHz CBW + # eht_op_data = b"\x6a\x05\x11\x00\x00\x00\xfa\x4f\x3f" # 80 MHz CBW + eht_op_data5 = b"\x6a\x05\x11\x00\x00\x00\xfb\x4f\x3f" # 160 MHz CBW + # eht_op_data6 = b"\x6a\x05\x11\x00\x00\x00\xfc\x4f\x3f" # 320 MHz CBW + + eht_operation = Dot11Elt(ID=0xFF, info=eht_op_data5) + + mac = mac.replace(":", "") + # mle_data = b"\x6b\xb0\x01\x0d" + b"\x40\xed\x00\xad\xaa\x1b" + b"\x02\x00\x01\x00\x41\x00" + mle_data = ( + b"\x6b\xb0\x01\x0d" + bytes.fromhex(mac) + b"\x02\x00\x01\x00\x41\x00" + ) + mle = Dot11Elt(ID=0xFF, info=mle_data) - if is_6ghz: - frame = essid / rates / dsset / dtim / rsn / rm_enabled_cap / extended - elif ft_disabled: + if ft_disabled: frame = ( essid / rates @@ -169,20 +248,145 @@ def build_fake_frame_ies(config) -> Dot11Elt: / vht_operation ) if he_disabled: - frame = frame / wmm + pass else: - frame = ( - frame - # / reduced_neighbor_report - / he_capabilities - / he_operation - / spatial_reuse - / mu_edca - / six_ghz_cap - / wmm - # / custom - ) + frame = frame / he_capabilities / he_operation / spatial_reuse / mu_edca + if be_disabled: + pass + else: + frame = frame / eht_operation / eht_capabilities + # frame = frame / mle / eht_operation / eht_capabilities + + # Add WLAN Pi vendor IE and WMM last + frame = frame / _Utils.build_wlanpi_vendor_ie_type_0(testing) / wmm + # frame = frame / wmm + return frame + + @staticmethod + def build_fake_frame_ies_6ghz(ssid, channel, testing) -> Dot11Elt: + """Build base frame for beacon and probe resp""" + log = logging.getLogger(inspect.stack()[0][1].split("/")[-1]) + log.debug("building 6 GHz frame") + ssid_bytes: "bytes" = bytes(ssid, "utf-8") + essid = Dot11Elt(ID="SSID", info=ssid_bytes) + + rates_data = [140, 18, 152, 36, 176, 72, 96, 108] + rates = Dot11Elt(ID="Rates", info=bytes(rates_data)) + + channel = bytes([channel]) # type: ignore + dsset = Dot11Elt(ID="DSset", info=channel) + + dtim_data = b"\x05\x04\x00\x03\x00\x00" + dtim = Dot11Elt(ID="TIM", info=dtim_data) + + rsn_data = b"\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x02\x00\x00\x0f\xac\x08\x00\x0f\xac\x09\xe8\x00" + + mobility_domain_data = b"\x45\xc2\x00" + mobility_domain = Dot11Elt(ID=0x36, info=mobility_domain_data) + + rsn = Dot11Elt(ID=0x30, info=rsn_data) + rm_enabled_data = b"\x02\x00\x00\x00\x00" + rm_enabled_cap = Dot11Elt(ID=0x46, info=rm_enabled_data) + + extended_data = b"\x04\x00\x08\x00\x00\x00\x00\x40\x00\x40\x09" + extended = Dot11Elt(ID=0x7F, info=extended_data) + + txpowerenv1_data = b"\x58\x2e" + txpowerenv1 = Dot11Elt(ID=0xC3, info=txpowerenv1_data) + + txpowerenv2_data = b"\x18\xfe" + txpowerenv2 = Dot11Elt(ID=0xC3, info=txpowerenv2_data) + + wmm_data = b"\x00\x50\xf2\x02\x01\x01\x8a\x00\x03\xa4\x00\x00\x27\xa4\x00\x00\x42\x43\x5e\x00\x62\x32\x2f\x00" + wmm = Dot11Elt(ID=0xDD, info=wmm_data) + + he_cap_data = b"\x23\x0d\x01\x00\x02\x40\x00\x04\x70\x0c\x89\x7f\x03\x80\x04\x00\x00\x00\xaa\xaa\xaa\xaa\x7b\x1c\xc7\x71\x1c\xc7\x71\x1c\xc7\x71\x1c\xc7\x71" + he_capabilities = Dot11Elt(ID=0xFF, info=he_cap_data) + + he_op_data = b"\x24\xf4\x3f\x00\x19\xfc\xff" + he_operation = Dot11Elt(ID=0xFF, info=he_op_data) + + spatial_reuse_data = b"\x27\x05\x00" + spatial_reuse = Dot11Elt(ID=0xFF, info=spatial_reuse_data) + + mu_edca_data = b"\x26\x09\x03\xa4\x28\x27\xa4\x28\x42\x73\x28\x62\x72\x28" + mu_edca = Dot11Elt(ID=0xFF, info=mu_edca_data) + + six_ghz_cap_data = b"\x3b\x00\x00" + six_ghz_cap = Dot11Elt(ID=0xFF, info=six_ghz_cap_data) + + eht_cap_data = b"\x6c\x00\x00\xe2\xff\xdb\x00\x18\x36\xd8\x1e\x00\x44\x44\x44\x44\x44\x44\x44\x44\x44" + eht_capabilities = Dot11Elt(ID=0xFF, info=eht_cap_data) + + # EHT CBW + # 0111 7 or 1100 C - 320 MHz + # 0011 3 or 1011 B - 160 MHz + # 0010 2 or 1010 A - 80 MHz + # 0001 1 or 1001 9 - 40 MHz + # 0000 0 or 1000 8 - 20 MHz + # eht_op_data = b"\x6a\x05\x11\x00\x00\x00\xf8\x4f\x3f" # 20 MHz CBW + # eht_op_data24 = b"\x6a\x05\x11\x00\x00\x00\xf9\x4f\x3f" # 40 MHz CBW + # eht_op_data = b"\x6a\x05\x11\x00\x00\x00\xfa\x4f\x3f" # 80 MHz CBW + # eht_op_data5 = b"\x6a\x05\x11\x00\x00\x00\xfb\x4f\x3f" # 160 MHz CBW + eht_op_data6 = b"\x6a\x05\x11\x00\x00\x00\xfc\x4f\x3f" # 320 MHz CBW + eht_operation = Dot11Elt(ID=0xFF, info=eht_op_data6) + + rsnex_data = b"\x20" + rsnex = Dot11Elt(ID=0xF4, info=rsnex_data) + + return ( + essid + / rates + / dtim + / rsn + / mobility_domain + / rm_enabled_cap + / extended + / txpowerenv1 + / txpowerenv2 + / he_capabilities + / he_operation + / spatial_reuse + / mu_edca + / six_ghz_cap + / eht_capabilities + / eht_operation + / rsnex + / _Utils.build_wlanpi_vendor_ie_type_0(testing) + / wmm + ) + + @staticmethod + def build_fake_frame_ies(config, mac, testing=False) -> Dot11Elt: + """Build base frame for beacon and probe resp""" + logging.getLogger(inspect.stack()[0][1].split("/")[-1]) + ssid: "str" = config.get("GENERAL").get("ssid") + mac = mac + channel: int = int(config.get("GENERAL").get("channel")) + frequency: int = int(config.get("GENERAL").get("frequency")) + ft_disabled: "bool" = config.get("GENERAL").get("ft_disabled") + he_disabled: "bool" = config.get("GENERAL").get("he_disabled") + be_disabled: "bool" = config.get("GENERAL").get("be_disabled") + wpa3_personal_transition: "bool" = config.get("GENERAL").get( + "wpa3_personal_transition" + ) + wpa3_personal: "bool" = config.get("GENERAL").get("wpa3_personal") + + if frequency > 5950: + frame = _Utils.build_fake_frame_ies_6ghz(ssid, channel, testing) + else: + frame = _Utils.build_fake_frame_ies_2ghz_5ghz( + ssid, + mac, + channel, + ft_disabled, + he_disabled, + be_disabled, + wpa3_personal_transition, + wpa3_personal, + testing + ) # for gathering data to validate tests: # # frame_bytes = bytes(frame) @@ -256,10 +460,10 @@ def __init__( addr3=self.mac, ) dot11beacon = Dot11Beacon(cap=0x1111) - beacon_frame_ies = _Utils.build_fake_frame_ies(self.config) + beacon_frame_ies = _Utils.build_fake_frame_ies(self.config, self.mac) self.beacon_frame = RadioTap() / dot11 / dot11beacon / beacon_frame_ies - # self.log.debug(f"origin beacon hexdump {hexdump(self.beacon_frame)}") + self.log.debug(f"origin beacon hexdump {hexdump(self.beacon_frame)}") self.log.info("starting beacon transmissions") self.every(self.beacon_interval, self.beacon) @@ -365,8 +569,8 @@ def __init__( self.dot11_assoc_request_cb = self.assoc_req self.dot11_auth_cb = self.auth with lock: - probe_resp_ies = _Utils.build_fake_frame_ies(self.config) self.mac = _Utils.get_mac(self.interface) + probe_resp_ies = _Utils.build_fake_frame_ies(self.config, self.mac) self.probe_response_frame = ( RadioTap() / Dot11( @@ -380,7 +584,6 @@ def __init__( / Dot11(subtype=DOT11_SUBTYPE_AUTH_REQ, addr2=self.mac, addr3=self.mac) / Dot11Auth(seqnum=0x02) ) - try: sniff( iface=self.interface, @@ -407,12 +610,13 @@ def received_frame(self, packet) -> None: """Handle incoming packets for profiling""" if packet.subtype == DOT11_SUBTYPE_AUTH_REQ: # auth if packet.addr1 == self.mac: # if we are the receiver + self.log.debug("rx auth sent from MAC %s", packet.addr2) self.dot11_auth_cb(packet.addr2) - elif packet.subtype == DOT11_SUBTYPE_PROBE_REQ: + elif packet.subtype == DOT11_SUBTYPE_PROBE_REQ: # probe request if Dot11Elt in packet: ssid = packet[Dot11Elt].info - # self.log.debug("probe req for %s by MAC %s", ssid, packet.addr) - if ssid == self.ssid or packet[Dot11Elt].len == 0: + self.log.debug("rx probe req for %s by MAC %s", ssid, packet.addr2) + if ssid == self.ssid.encode() or packet[Dot11Elt].len == 0: self.dot11_probe_request_cb(packet) elif ( packet.subtype == DOT11_SUBTYPE_ASSOC_REQ @@ -422,6 +626,10 @@ def received_frame(self, packet) -> None: self.dot11_assoc_request_cb(packet) if self.listen_only: self.dot11_assoc_request_cb(packet) + ssid = packet[Dot11Elt].info + self.log.debug( + "assoc req seen for %s (%s) by MAC %s", ssid, packet.addr1, packet.addr2 + ) def probe_response(self, probe_request) -> None: """Send probe resp to assist with profiler discovery""" @@ -438,7 +646,7 @@ def probe_response(self, probe_request) -> None: "probe_response(): network is down or no such device ... exiting ..." ) sys.exit(signal.SIGALRM) - # self.log.debug("sent probe resp to %s", probe_request.addr2) + self.log.debug("tx probe resp to %s", probe_request.addr2) def assoc_req(self, frame) -> None: """Put association request on queue for the Profiler""" @@ -455,7 +663,7 @@ def auth(self, receiver) -> None: _Utils.next_sequence_number(self.sequence_number) - 1 ) - # self.log.debug("sending authentication (0x0B) to %s", receiver) + self.log.debug("tx authentication (0x0B) to %s", receiver) try: self.l2socket.send(frame) # type: ignore diff --git a/profiler/helpers.py b/profiler/helpers.py index f2e5e8f..7f1d2bc 100644 --- a/profiler/helpers.py +++ b/profiler/helpers.py @@ -1,7 +1,7 @@ # -* coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -32,10 +32,10 @@ # third party imports try: - import manuf # type: ignore + import manuf2 # type: ignore except ModuleNotFoundError as error: - if error.name == "manuf": - print("required module manuf not found.") + if error.name == "manuf2": + print("required module manuf2 not found.") else: print(f"{error}") sys.exit(signal.SIGABRT) @@ -70,13 +70,7 @@ def setup_logger(args) -> None: """Configure and set logging levels""" - if args.logging: - if args.logging == "debug": - logging_level = logging.DEBUG - if args.logging == "warning": - logging_level = logging.WARNING - else: - logging_level = logging.INFO + logging_level = logging.INFO if args.debug: logging_level = logging.DEBUG @@ -192,12 +186,6 @@ def setup_parser() -> argparse.ArgumentParser: default=False, help="enable debug logging output", ) - parser.add_argument( - "--logging", - help="change logging output", - nargs="?", - choices=("debug", "warning"), - ) parser.add_argument( "--noprep", dest="no_interface_prep", @@ -242,6 +230,36 @@ def setup_parser() -> argparse.ArgumentParser: default=False, help="turn off 802.11ax High Efficiency (HE) reporting", ) + dot11be_group = parser.add_mutually_exclusive_group() + dot11be_group.add_argument( + "--11be", + dest="be_enabled", + action="store_true", + default=False, + help=argparse.SUPPRESS, # "turn on 802.11be Extremely High Throughput (EHT) reporting (override --config )", + ) + dot11be_group.add_argument( + "--no11be", + dest="be_disabled", + action="store_true", + default=False, + help="turn off 802.11be Extremely High Throughput (EHT) reporting", + ) + wpa_group = parser.add_mutually_exclusive_group() + wpa_group.add_argument( + "--wpa3_personal_transition", + dest="wpa3_personal_transition", + action="store_true", + default=False, + help="enable WPA3 Personal Transition in the RSNE for 2.4 / 5 GHz", + ) + wpa_group.add_argument( + "--wpa3_personal", + dest="wpa3_personal", + action="store_true", + default=False, + help="enable WPA3 Personal only in the RSNE for 2.4 / 5 GHz", + ) parser.add_argument( "--clean", dest="clean", @@ -344,15 +362,15 @@ def get_data_from_iproute2(intf) -> NetworkInterface: return iface -def get_eth0_mac(): - """Check iproute2 output for eth0 and return a MAC with a format like 000000111111""" - eth0_data = get_data_from_iproute2("eth0") - eth0_mac = None - if eth0_data: - if eth0_data.mac: - eth0_mac = eth0_data.mac.replace(":", "") - if eth0_mac: - return eth0_mac +def get_iface_mac(iface: str): + """Check iproute2 output for and return a MAC with a format like 000000111111""" + iface_data = get_data_from_iproute2(iface) + iface_mac = None + if iface_data: + if iface_data.mac: + iface_mac = iface_data.mac.replace(":", "") + if iface_mac: + return iface_mac return "" @@ -381,7 +399,7 @@ def setup_config(args): config["GENERAL"]["channel"] = 36 if "ssid" not in config["GENERAL"] or config["GENERAL"].get("ssid", "") == "": - last_3_of_eth0_mac = f" {get_eth0_mac()[-3:]}" + last_3_of_eth0_mac = f" {get_iface_mac('eth0')[-3:]}" config["GENERAL"]["ssid"] = f"Profiler{last_3_of_eth0_mac}" if "interface" not in config["GENERAL"]: @@ -416,6 +434,14 @@ def setup_config(args): config["GENERAL"]["he_disabled"] = False if args.he_disabled: config["GENERAL"]["he_disabled"] = args.he_disabled + if args.be_enabled: + config["GENERAL"]["be_disabled"] = False + if args.be_disabled: + config["GENERAL"]["be_disabled"] = args.be_disabled + if args.wpa3_personal: + config["GENERAL"]["wpa3_personal"] = args.wpa3_personal + if args.wpa3_personal_transition: + config["GENERAL"]["wpa3_personal_transition"] = args.wpa3_personal_transition if args.listen_only: config["GENERAL"]["listen_only"] = args.listen_only if args.pcap_analysis: @@ -552,28 +578,28 @@ def run_command(cmd: list, suppress_output=False) -> str: return "completed process return code is non-zero with no stdout or stderr" -def update_manuf() -> bool: - """Manuf wrapper to update manuf OUI flat file from Internet""" +def update_manuf2() -> bool: + """manuf2 wrapper to update manuf2 OUI flat file from Internet""" log = logging.getLogger(inspect.stack()[0][3]) try: - flat_file = os.path.join(manuf.__path__[0], "manuf") - manuf_location = f"{sys.prefix}/bin/manuf" + flat_file = os.path.join(manuf2.__path__[0], "manuf") + manuf2_location = f"{sys.prefix}/bin/manuf2" log.info("OUI database is located at %s", flat_file) - log.info("manuf is located at %s", manuf_location) + log.info("manuf2 is located at %s", manuf2_location) log.info( - "manuf file last modified at: %s", + "manuf2 file last modified at: %s", ctime(os.path.getmtime(flat_file)), ) - log.info("running 'sudo manuf --update'") - out = run_command(["sudo", manuf_location, "--update"]) + log.info("running 'sudo manuf2 --update'") + out = run_command(["sudo", manuf2_location, "--update"]) log.info("%s", str(out)) if "URLError" not in out: log.info( - "manuf file last modified at: %s", + "manuf2 file last modified at: %s", ctime(os.path.getmtime(flat_file)), ) except OSError: - log.exception("problem updating manuf. make sure manuf is installed...") + log.exception("problem updating manuf2. make sure manuf2 is installed...") print("exiting...") return False return True diff --git a/profiler/interface.py b/profiler/interface.py index 2b2588a..d4c0d87 100644 --- a/profiler/interface.py +++ b/profiler/interface.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com diff --git a/profiler/manager.py b/profiler/manager.py index 879b0ed..4696729 100644 --- a/profiler/manager.py +++ b/profiler/manager.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -110,7 +110,7 @@ def start(args: argparse.Namespace): if args.oui_update: # run manuf oui update and exit - sys.exit(0) if helpers.update_manuf() else sys.exit(-1) + sys.exit(0) if helpers.update_manuf2() else sys.exit(-1) config = helpers.setup_config(args) diff --git a/profiler/profiler.py b/profiler/profiler.py index 9b74d89..9451662 100644 --- a/profiler/profiler.py +++ b/profiler/profiler.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # profiler : a Wi-Fi client capability analyzer tool -# Copyright : (c) 2020-2021 Josh Schmelzle +# Copyright : (c) 2024 Josh Schmelzle # License : BSD-3-Clause # Maintainer : josh@joshschmelzle.com @@ -26,13 +26,14 @@ from typing import Dict, List, Tuple # third party imports -from manuf import manuf # type: ignore +from manuf2 import manuf # type: ignore from scapy.all import Dot11, RadioTap, wrpcap # type: ignore # app imports from .__version__ import __version__ from .constants import ( _20MHZ_FREQUENCY_CHANNEL_MAP, + EHT_CAPABILITIES_IE_EXT_TAG, EXT_CAPABILITIES_IE_TAG, FT_CAPABILITIES_IE_TAG, HE_6_GHZ_BAND_CAP_IE_EXT_TAG, @@ -79,6 +80,7 @@ def __init__(self, config=None, queue=None): self.pcap_analysis = config.get("GENERAL").get("pcap_analysis") self.ft_disabled = config.get("GENERAL").get("ft_disabled") self.he_disabled = config.get("GENERAL").get("he_disabled") + self.be_disabled = config.get("GENERAL").get("be_disabled") self.reports_dir = os.path.join(self.files_path, "reports") self.clients_dir = os.path.join(self.files_path, "clients") self.csv_file = os.path.join( @@ -143,6 +145,7 @@ def profile(self, frame) -> None: --------------------------------------------- - Client MAC: 6e:1d:8a:28:32:51 - OUI manufacturer lookup: Apple (Randomized MAC) + - Chipset lookup: Broadcom - Frequency band: Unknown - Capture channel: 0 --------------------------------------------- @@ -158,14 +161,14 @@ def profile(self, frame) -> None: if freq > 2411 and freq < 2485: band = "2.4GHz" elif freq > 5100 and freq < 5900: - band = "5.8GHz" + band = "5.0GHz" elif freq > 5900 and freq < 7120: band = "6.0GHz" is_6ghz = True else: band = "unknown" - ssid, oui_manuf, capabilities = self.analyze_assoc_req(frame, is_6ghz) + ssid, oui_manuf, chipset, capabilities = self.analyze_assoc_req(frame, is_6ghz) analysis_hash = hash(f"{frame.addr2}: {capabilities}") if analysis_hash in self.analyzed_hash.keys(): self.log.info( @@ -195,6 +198,7 @@ def profile(self, frame) -> None: # generate text report text_report = self.generate_text_report( text_report_oui_manuf, + chipset, capabilities, frame.addr2, channel, @@ -216,6 +220,7 @@ def profile(self, frame) -> None: capabilities, frame, oui_manuf, + chipset, randomized, band, channel, @@ -231,6 +236,7 @@ def profile(self, frame) -> None: @staticmethod def generate_text_report( oui_manuf: str, + chipset: str, capabilities: list, client_mac: str, channel: int, @@ -245,6 +251,7 @@ def generate_text_report( text_report += f"\n - SSID: {ssid}" text_report += f"\n - Client MAC: {client_mac}" text_report += f"\n - OUI manufacturer lookup: {oui_manuf or 'Unknown'}" + text_report += f"\n - Chipset lookup: {chipset or 'Unknown'}" band_label = "" if band[0] == "2": band_label = "2.4 GHz" @@ -275,6 +282,7 @@ def write_analysis_to_file_system( capabilities, frame, oui_manuf, + chipset, randomized: bool, band, channel, @@ -298,6 +306,7 @@ def write_analysis_to_file_system( data["mac"] = client_mac data["is_laa"] = randomized data["manuf"] = oui_manuf + data["chipset"] = chipset if band[0] == "2": band_db = 2 elif band[0] == "5": @@ -397,7 +406,6 @@ def write_analysis_to_file_system( # check if csv file exists if not os.path.exists(self.csv_file): - # create file with csv headers with open(self.csv_file, mode="w") as file_obj: csv_writer = csv.DictWriter(file_obj, fieldnames=out_fieldnames) @@ -483,7 +491,12 @@ def resolve_oui_manuf(self, mac: str, dot11_elt_dict) -> str: # vendor OUI that we possibly want to check for a more clear OUI match low_quality = "muratama" - sanitize = {"intelwir": "Intel", "intelcor": "Intel", "samsunge": "Samsung"} + sanitize = { + "intelwir": "Intel", + "intelcor": "Intel", + "samsunge": "Samsung", + "samsungelect": "Samsung", + } if ( oui_manuf is None @@ -495,26 +508,63 @@ def resolve_oui_manuf(self, mac: str, dot11_elt_dict) -> str: # of the client is the vendor that maps to that OUI if VENDOR_SPECIFIC_IE_TAG in dot11_elt_dict.keys(): for element_data in dot11_elt_dict[VENDOR_SPECIFIC_IE_TAG]: - vendor_mac = "{0:02X}:{1:02X}:{2:02X}:00:00:00".format( - element_data[0], element_data[1], element_data[2] - ) - oui_manuf_vendor = self.lookup.get_manuf(vendor_mac) - if oui_manuf_vendor is not None: - # Matches are vendor specific IEs we know are client specific - # e.g. Apple vendor specific IEs can only be found in Apple devices - # Samsung may follow similar logic based on S10 5G testing and S21 5G Ultra but unsure of consistency - matches = ("apple", "samsung", "intel") - if oui_manuf_vendor.lower().startswith(matches): - if oui_manuf_vendor.lower() in sanitize: - oui_manuf = sanitize.get( - oui_manuf_vendor.lower(), oui_manuf_vendor - ) - else: - oui_manuf = oui_manuf_vendor + try: + vendor_mac = "{0:02X}:{1:02X}:{2:02X}:00:00:00".format( + element_data[0], element_data[1], element_data[2] + ) + oui_manuf_vendor = self.lookup.get_manuf(vendor_mac) + if oui_manuf_vendor is not None: + # Matches are vendor specific IEs we know are client specific + # e.g. Apple vendor specific IEs can only be found in Apple devices + # Samsung may follow similar logic based on S10 5G testing and S21 5G Ultra but unsure of consistency + matches = ("apple", "samsung", "intel") + if oui_manuf_vendor.lower().startswith(matches): + if oui_manuf_vendor.lower() in sanitize: + oui_manuf = sanitize.get( + oui_manuf_vendor.lower(), oui_manuf_vendor + ) + else: + oui_manuf = oui_manuf_vendor + except IndexError: + log.debug("IndexError in %s" % VENDOR_SPECIFIC_IE_TAG) log.debug("finished oui lookup for %s: %s", mac, oui_manuf) return oui_manuf + def resolve_vendor_specific_tag_chipset(self, dot11_elt_dict) -> str: + """Resolve client's chipset via heuristics of vendor specific tags""" + # Broadcom + # MediaTek + # Qualcomm + # Infineon AG + # Intel Wireless Network Group + log = logging.getLogger(inspect.stack()[0][3]) + chipset = None + manufs = [] + + if VENDOR_SPECIFIC_IE_TAG in dot11_elt_dict.keys(): + for element_data in dot11_elt_dict[VENDOR_SPECIFIC_IE_TAG]: + try: + oui = "{0:02X}:{1:02X}:{2:02X}:00:00:00".format( + element_data[0], element_data[1], element_data[2] + ) + manufs.append(self.lookup.get_manuf(oui)) + except IndexError: + log.debug("IndexError for %s" % VENDOR_SPECIFIC_IE_TAG) + + matches = ["broadcom", "qualcomm", "mediatek", "intel", "infineon"] + _break = False + for manuf in manufs: + for match in matches: + if manuf.lower().startswith(match): + chipset = match.title() + _break = True + break + if _break: + break + + return chipset + @staticmethod def analyze_ssid_ie(dot11_elt_dict) -> str: """Check the SSID parameter to determine network name""" @@ -538,12 +588,10 @@ def analyze_ht_capabilities_ie(dot11_elt_dict) -> List: dot11n_nss = Capability(db_key="dot11n_nss", db_value=0) if HT_CAPABILITIES_IE_TAG in dot11_elt_dict.keys(): - spatial_streams = 0 # mcs octets 1 - 4 indicate # streams supported (up to 4 streams only) for mcs_octet in range(3, 7): - mcs_octet_value = dot11_elt_dict[HT_CAPABILITIES_IE_TAG][mcs_octet] if mcs_octet_value & 255: @@ -565,6 +613,7 @@ def analyze_vht_capabilities_ie(dot11_elt_dict) -> List: dot11ac_mcs = Capability(db_key="dot11ac_mcs", db_value="") dot11ac_su_bf = Capability(db_key="dot11ac_su_bf", db_value=0) dot11ac_mu_bf = Capability(db_key="dot11ac_mu_bf", db_value=0) + dot11ac_bf_sts = Capability(db_key="dot11ac_bf_sts", db_value=0) dot11ac_160_mhz = Capability(db_key="dot11ac_160_mhz", db_value=0) if VHT_CAPABILITIES_IE_TAG in dot11_elt_dict.keys(): @@ -601,6 +650,7 @@ def analyze_vht_capabilities_ie(dot11_elt_dict) -> List: # check for SU & MU beam formee support mu_octet = dot11_elt_dict[VHT_CAPABILITIES_IE_TAG][2] su_octet = dot11_elt_dict[VHT_CAPABILITIES_IE_TAG][1] + bf_sts_octet = dot11_elt_dict[VHT_CAPABILITIES_IE_TAG][1] onesixty = dot11_elt_dict[VHT_CAPABILITIES_IE_TAG][0] # 160 MHz @@ -627,6 +677,16 @@ def analyze_vht_capabilities_ie(dot11_elt_dict) -> List: else: dot11ac.value += ", [ ] MU BF" + # BF STS + vht_bf_sts_binary_string = "{0}{1}{2}".format( + int(get_bit(bf_sts_octet, 5)), + int(get_bit(bf_sts_octet, 6)), + int(get_bit(bf_sts_octet, 7)), + ) + vht_bf_sts_value = int(vht_bf_sts_binary_string, base=2) + dot11ac_bf_sts.db_value = vht_bf_sts_value + dot11ac.value += f", Beamformee STS={vht_bf_sts_value}" + return [ dot11ac, dot11ac_nss, @@ -634,6 +694,7 @@ def analyze_vht_capabilities_ie(dot11_elt_dict) -> List: dot11ac_mcs, dot11ac_su_bf, dot11ac_mu_bf, + dot11ac_bf_sts, ] @staticmethod @@ -675,12 +736,10 @@ def analyze_ext_capabilities_ie(dot11_elt_dict) -> List: ) if EXT_CAPABILITIES_IE_TAG in dot11_elt_dict.keys(): - ext_cap_list = dot11_elt_dict[EXT_CAPABILITIES_IE_TAG] # check octet 3 exists if 3 <= len(ext_cap_list): - # bit 4 of octet 3 in the extended capabilites field octet3 = ext_cap_list[2] bss_trans_support = int("00001000", 2) @@ -700,7 +759,6 @@ def analyze_rsn_capabilities_ie(dot11_elt_dict) -> List: ) if RSN_CAPABILITIES_IE_TAG in dot11_elt_dict.keys(): - rsn_cap_list = dot11_elt_dict[RSN_CAPABILITIES_IE_TAG] rsn_len = len(rsn_cap_list) - 2 pmf_oct = rsn_cap_list[rsn_len] @@ -729,7 +787,6 @@ def analyze_power_capability_ie(dot11_elt_dict) -> List: ) if POWER_MIN_MAX_IE_TAG in dot11_elt_dict.keys(): - # octet 3 of power capabilites max_power = dot11_elt_dict[POWER_MIN_MAX_IE_TAG][1] min_power = dot11_elt_dict[POWER_MIN_MAX_IE_TAG][0] @@ -768,7 +825,6 @@ def analyze_supported_channels_ie(dot11_elt_dict, is_6ghz: bool) -> List: is_5ghz = False while channel_sets_list: - start_channel = channel_sets_list.pop(0) channel_range = channel_sets_list.pop(0) @@ -844,8 +900,10 @@ def analyze_operating_classes(dot11_elt_dict) -> List: return [six_ghz_operating_class_cap] @staticmethod - def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: - """Check for 802.11ax support""" + def analyze_extension_ies( + dot11_elt_dict, he_disabled: bool, be_disabled: bool + ) -> List: + """Check for 802.11ax and 802.11be support""" dot11ax = Capability( name="802.11ax", value="Not supported", @@ -865,6 +923,9 @@ def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: dot11ax_he_su_beamformee = Capability( db_key="dot11ax_he_su_beamformee", db_value=0 ) + dot11ax_he_beamformee_sts = Capability( + db_key="dot11ax_he_beamformee_sts", db_value=0 + ) dot11ax_nss = Capability(db_key="dot11ax_nss", db_value=0) dot11ax_mcs = Capability(db_key="dot11ax_mcs", db_value="") dot11ax_twt = Capability(db_key="dot11ax_twt", db_value=0) @@ -879,7 +940,6 @@ def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: else: if IE_EXT_TAG in dot11_elt_dict.keys(): for element_data in dot11_elt_dict[IE_EXT_TAG]: - ext_ie_id = int(str(element_data[0])) if ext_ie_id == HE_CAPABILITIES_IE_EXT_TAG: @@ -982,6 +1042,18 @@ def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: dot11ax_he_su_beamformee.db_value = 0 dot11ax.value += ", [ ] SU Beamformee" + # BF STS + he_bf_sts_octet = element_data[11] + + he_bf_sts_binary_string = "{0}{1}{2}".format( + int(get_bit(he_bf_sts_octet, 2)), + int(get_bit(he_bf_sts_octet, 3)), + int(get_bit(he_bf_sts_octet, 4)), + ) + he_bf_sts_value = int(he_bf_sts_binary_string, base=2) + dot11ax_he_beamformee_sts.db_value = he_bf_sts_value + dot11ax.value += f", Beamformee STS={he_bf_sts_value}" + he_er_su_ppdu_octet = element_data[15] he_er_su_ppdu_octet_binary_string = "" for bit_position in range(8): @@ -1044,6 +1116,63 @@ def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: dot11ax_six_ghz.value = "Supported" dot11ax_six_ghz.db_value = 1 + if ext_ie_id == HE_CAPABILITIES_IE_EXT_TAG: + # dot11ax is supported + dot11ax.value = "Supported" + dot11ax.db_value = 1 + + dot11be = Capability( + name="802.11be", + value="Not supported", + db_key="dot11be", + db_value=0, + ) + dot11be_nss = Capability( + db_key="dot11be_nss", + db_value=0, + ) + dot11be_mcs = Capability( + db_key="dot11be_mcs", + db_value="", + ) + dot11be_320_mhz = Capability(db_key="dot11be_320_mhz", db_value=0) + + if be_disabled: + dot11be.value = "Reporting disabled (--no11be option used)" + else: + if IE_EXT_TAG in dot11_elt_dict.keys(): + for element_data in dot11_elt_dict[IE_EXT_TAG]: + ext_ie_id = int(str(element_data[0])) + + if ext_ie_id == EHT_CAPABILITIES_IE_EXT_TAG: + # dot11ax is supported + dot11be.value = "Supported" + dot11be.db_value = 1 + + element_data[1] + element_data[2] + eht_phy_cap_1 = element_data[3] + element_data[4] + element_data[5] + element_data[6] + element_data[7] + element_data[8] + element_data[9] + element_data[10] + element_data[11] + element_data[12] + element_data[13] + element_data[14] + element_data[15] + element_data[16] + element_data[17] + + if get_bit(eht_phy_cap_1, 2): + dot11be.value += ", [X] 320 MHz" + dot11be_320_mhz.db_value = 1 + else: + dot11be.value += ", [ ] 320 MHz" + return [ dot11ax, dot11ax_nss, @@ -1054,9 +1183,14 @@ def analyze_extension_ies(dot11_elt_dict, he_disabled: bool) -> List: dot11ax_punctured_preamble, dot11ax_he_su_beamformer, dot11ax_he_su_beamformee, + dot11ax_he_beamformee_sts, dot11ax_he_er_su_ppdu, dot11ax_six_ghz, dot11ax_160_mhz, + dot11be, + dot11be_nss, + dot11be_mcs, + dot11be_320_mhz, ] def analyze_assoc_req(self, frame, is_6ghz: bool) -> Tuple[str, str, list]: @@ -1090,6 +1224,9 @@ def analyze_assoc_req(self, frame, is_6ghz: bool) -> Tuple[str, str, list]: # resolve manufacturer oui_manuf = self.resolve_oui_manuf(frame.addr2, dot11_elt_dict) + # parse chipset + chipset = self.resolve_vendor_specific_tag_chipset(dot11_elt_dict) + ssid = self.analyze_ssid_ie(dot11_elt_dict) # dictionary to store capabilities as we decode them @@ -1115,8 +1252,10 @@ def analyze_assoc_req(self, frame, is_6ghz: bool) -> Tuple[str, str, list]: # check for 11ac support capabilities += self.analyze_vht_capabilities_ie(dot11_elt_dict) - # check for ext tags (e.g. 802.11ax draft support) - capabilities += self.analyze_extension_ies(dot11_elt_dict, self.he_disabled) + # check for ext tags (e.g. 802.11ax support, 802.11be draft support) + capabilities += self.analyze_extension_ies( + dot11_elt_dict, self.he_disabled, self.be_disabled + ) # check supported operating classes for 6 GHz capabilities += self.analyze_operating_classes(dot11_elt_dict) @@ -1127,4 +1266,4 @@ def analyze_assoc_req(self, frame, is_6ghz: bool) -> Tuple[str, str, list]: # check supported channels capabilities += self.analyze_supported_channels_ie(dot11_elt_dict, is_6ghz) - return ssid, oui_manuf, capabilities + return ssid, oui_manuf, chipset, capabilities diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..79002a3 --- /dev/null +++ b/requirements.in @@ -0,0 +1,2 @@ +scapy +manuf2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8df494f..7ff40db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,12 @@ -scapy==2.4.5 -manuf==1.1.5 \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile requirements.in +# +--extra-index-url https://www.piwheels.org/simple + +manuf2==2.0.0 + # via -r requirements.in +scapy==2.5.0 + # via -r requirements.in diff --git a/setup.py b/setup.py index 2f03875..87a4288 100644 --- a/setup.py +++ b/setup.py @@ -20,22 +20,27 @@ packages = ["profiler"] -extras = { - "testing": [ - "tox==3.27.0", - "black", - "isort", - "autoflake", - "mypy", - "flake8", - "pytest", - "pytest-cov", - "coverage-badge==1.1.0", - "pytest-mock", - ], -} +def parse_requires(_list): + requires = list() + trims = ["#", "piwheels.org"] + for require in _list: + if any(match in require for match in trims): + continue + requires.append(require) + requires = list(filter(None, requires)) # remove "" from list + return requires + +with open("extras.txt") as f: + testing = f.read().splitlines() + +testing = parse_requires(testing) + +extras = {"testing": testing} -requires = ["scapy==2.4.5", "manuf==1.1.5"] +with open("requirements.txt") as f: + requires = f.read().splitlines() + +requires = parse_requires(requires) setup( name=about["__title__"], @@ -46,12 +51,12 @@ author=about["__author__"], author_email=about["__author_email__"], url=about["__url__"], - python_requires="~=3.7,", + python_requires="~=3.9,", license=about["__license__"], classifiers=[ "Natural Language :: English", "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.9", "Intended Audience :: System Administrators", "Topic :: Utilities", ], diff --git a/tests/pcaps/iPad11_4th_Gen_UK_82-8b-75-2d-f2-c0_5.8GHz.pcap b/tests/pcaps/iPad11_4th_Gen_UK_82-8b-75-2d-f2-c0_5.8GHz.pcap new file mode 100644 index 0000000000000000000000000000000000000000..954e26f8bc586b5268f12fce9eac691c9605f1f9 GIT binary patch literal 329 zcmca|c+)~A1{MYw`2U}Qp&rPYF*P#TN`{F+k&yw685k@W^c@@)C~z=3ut0H8Nc A= 58.0.0 - pip >= 21.0.0 - virtualenv >= 20.9.0 +envlist = py39,py310 +requires = setuptools == 65.5.0 + pip == 22.3 + virtualenv == 20.16.6 [testenv] description = run the test driver @@ -15,6 +15,7 @@ deps = coverage mock pytest + manuf2 commands = coverage run --source profiler -m pytest -vv --capture=sys {posargs} # coverage combine diff --git a/utils/anonymizer.py b/utils/anonymizer.py index 35d2993..67e273d 100755 --- a/utils/anonymizer.py +++ b/utils/anonymizer.py @@ -123,8 +123,8 @@ def anonymize_file(input_file: str, output_file: str) -> None: has_fcs = False for frame in reader: - if frame.haslayer(Dot11): + logger.debug(frame.show()) if frame.haslayer(Dot11FCS): has_fcs = True frame_fcs = frame.fcs